mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 18:08:32 +00:00
Compare commits
2 Commits
oauth2_add
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3a99a4d85 | ||
|
|
bbfa2b39a0 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
|
||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -30,7 +30,6 @@ 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
|
||||
@@ -81,7 +80,6 @@ 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: |
|
||||
@@ -127,7 +125,6 @@ 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();
|
||||
});
|
||||
});
|
||||
@@ -12,10 +12,10 @@ test('Create new collection and add a simple HTTP request', async ({ page, creat
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('http://localhost:8081');
|
||||
await page.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('#request-url .CodeMirror').click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
persistent-env-test: persistent-env-test-value
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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");
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
|
||||
]
|
||||
}
|
||||
855
package-lock.json
generated
855
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,15 +14,13 @@
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests",
|
||||
"packages/bruno-filestore"
|
||||
"packages/bruno-requests"
|
||||
],
|
||||
"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",
|
||||
@@ -42,7 +40,6 @@
|
||||
"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",
|
||||
@@ -51,7 +48,6 @@
|
||||
"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",
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-pdf": "9.1.1",
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
@@ -111,10 +110,5 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"overrides": {
|
||||
"httpsnippet": {
|
||||
"form-data": "4.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,17 +187,17 @@ export default class CodeEditor extends React.Component {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
// Setup AutoComplete Helper for all modes
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: this.props.showHintsFor,
|
||||
getAllVariables: getAllVariablesHandler
|
||||
showHintsFor: this.props.showHintsFor
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -135,7 +131,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -144,7 +140,6 @@ 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,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -59,7 +55,7 @@ const BasicAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -68,7 +64,6 @@ const BasicAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -34,7 +30,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 flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -43,7 +39,6 @@ const BearerAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -59,7 +55,7 @@ const DigestAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -68,7 +64,6 @@ const DigestAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -20,8 +18,6 @@ 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));
|
||||
|
||||
@@ -86,7 +82,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -95,7 +91,6 @@ const NTLMAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/in
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
@@ -30,9 +29,6 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'implicit':
|
||||
return <OAuth2Implicit save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -59,16 +55,14 @@ const WsseAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<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,48 +38,6 @@ 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,10 +132,7 @@ 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">
|
||||
<span className="protocol-placeholder">
|
||||
<span className="protocol-https">https://</span>
|
||||
<span className="protocol-grpcs">grpcs://</span>
|
||||
</span>
|
||||
https://
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
|
||||
@@ -46,7 +46,7 @@ const Docs = ({ collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full relative flex flex-col">
|
||||
<StyledWrapper className="mt-1 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} />
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
@@ -1,263 +0,0 @@
|
||||
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, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
@@ -7,30 +7,19 @@ import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
deleteCollectionHeader
|
||||
} 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(
|
||||
@@ -74,22 +63,6 @@ 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">
|
||||
@@ -168,14 +141,9 @@ const Headers = ({ collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -53,7 +53,7 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Requests</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
<div className="mt-1 text-sm text-muted font-mono">
|
||||
{
|
||||
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { 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 { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
@@ -17,15 +15,10 @@ const PresetsSettings = ({ collection }) => {
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
|
||||
requestType: 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));
|
||||
@@ -69,23 +62,6 @@ 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">
|
||||
@@ -98,7 +74,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
placeholder='Request URL'
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -111,7 +87,6 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
|
||||
@@ -13,16 +13,21 @@ 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 DotIcon from 'components/Icons/Dot';
|
||||
import Overview from './Overview/index';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
<sup className="ml-[.125rem] opacity-80 font-medium">
|
||||
<DotIcon width="10"></DotIcon>
|
||||
</sup>
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -48,7 +53,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);
|
||||
@@ -125,9 +130,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'grpc': {
|
||||
return <Grpc collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,9 +140,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-hidden">
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
|
||||
<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')}>
|
||||
@@ -153,35 +155,29 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{authMode !== 'none' && <StatusDot />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
{hasScripts && <StatusDot />}
|
||||
{hasScripts && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
|
||||
Tests
|
||||
{hasTests && <StatusDot />}
|
||||
{hasTests && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
{Object.keys(proxyConfig).length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
{clientCertConfig.length > 0 && <ContentIndicator />}
|
||||
</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-auto">{getTabPanel(tab)}</section>
|
||||
<section className="mt-4 h-full">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
overflow: hidden;
|
||||
|
||||
.debug-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.debug-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.error-count {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.debug-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${(props) => props.theme.console.emptyColor};
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 40px 20px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.errors-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.errors-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 200px 120px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.errors-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.error-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 200px 120px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.error-location {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.error-time {
|
||||
color: ${(props) => props.theme.console.timestampColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconBug } from '@tabler/icons';
|
||||
import {
|
||||
setSelectedError,
|
||||
clearDebugErrors
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ErrorRow = ({ error, isSelected, onClick }) => {
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
};
|
||||
|
||||
const getShortMessage = (message, maxLength = 80) => {
|
||||
if (!message) return 'Unknown error';
|
||||
return message.length > maxLength ? message.substring(0, maxLength) + '...' : message;
|
||||
};
|
||||
|
||||
const getLocation = (error) => {
|
||||
if (error.filename) {
|
||||
const filename = error.filename.split('/').pop(); // Get just the filename
|
||||
if (error.lineno && error.colno) {
|
||||
return `${filename}:${error.lineno}:${error.colno}`;
|
||||
} else if (error.lineno) {
|
||||
return `${filename}:${error.lineno}`;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`error-row ${isSelected ? 'selected' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="error-message" title={error.message}>
|
||||
{getShortMessage(error.message)}
|
||||
</div>
|
||||
|
||||
<div className="error-location" title={error.filename}>
|
||||
{getLocation(error)}
|
||||
</div>
|
||||
|
||||
<div className="error-time">
|
||||
{formatTime(error.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DebugTab = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { debugErrors, selectedError } = useSelector(state => state.logs);
|
||||
|
||||
const handleErrorClick = (error) => {
|
||||
dispatch(setSelectedError(error));
|
||||
};
|
||||
|
||||
const handleClearErrors = () => {
|
||||
dispatch(clearDebugErrors());
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="debug-content">
|
||||
{debugErrors.length === 0 ? (
|
||||
<div className="debug-empty">
|
||||
<IconBug size={48} strokeWidth={1} />
|
||||
<p>No errors</p>
|
||||
<span>console.error() calls will appear here</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="errors-container">
|
||||
<div className="errors-header">
|
||||
<div>Message</div>
|
||||
<div>Location</div>
|
||||
<div className="text-right">Time</div>
|
||||
</div>
|
||||
|
||||
<div className="errors-list">
|
||||
{debugErrors.map((error, index) => (
|
||||
<ErrorRow
|
||||
key={error.id}
|
||||
error={error}
|
||||
isSelected={selectedError?.id === error.id}
|
||||
onClick={() => handleErrorClick(error)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebugTab;
|
||||
@@ -1,228 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
border-left: 1px solid ${(props) => props.theme.console.border};
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
width: 40%;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.error-time {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.console.checkboxColor};
|
||||
border-bottom-color: ${(props) => props.theme.console.checkboxColor};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message-full {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
|
||||
.file-path {
|
||||
color: ${(props) => props.theme.console.checkboxColor} !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.report-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.report-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.checkboxColor};
|
||||
color: white;
|
||||
border-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.stack-trace-container,
|
||||
.arguments-container {
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stack-trace,
|
||||
.arguments {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
background: transparent;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,268 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
IconX,
|
||||
IconBug,
|
||||
IconFileText,
|
||||
IconCode,
|
||||
IconStack,
|
||||
IconBrandGithub
|
||||
} from '@tabler/icons';
|
||||
import { clearSelectedError } from 'providers/ReduxStore/slices/logs';
|
||||
import { useApp } from 'providers/App';
|
||||
import platformLib from 'platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ErrorInfoTab = ({ error }) => {
|
||||
const { version } = useApp();
|
||||
|
||||
const formatTimestamp = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const generateGitHubIssueUrl = () => {
|
||||
const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`;
|
||||
|
||||
const body = `## Bug Report
|
||||
|
||||
### Error Details
|
||||
- **Message**: ${error.message}
|
||||
- **File**: ${error.filename || 'Unknown'}
|
||||
- **Line**: ${error.lineno || 'Unknown'}:${error.colno || 'Unknown'}
|
||||
- **Timestamp**: ${formatTimestamp(error.timestamp)}
|
||||
|
||||
### Environment
|
||||
- **Bruno Version**: ${version}
|
||||
- **OS**: ${platformLib.os.family} ${platformLib.os.version || ''}
|
||||
- **Browser**: ${platformLib.name} ${platformLib.version || ''}
|
||||
|
||||
### Stack Trace
|
||||
\`\`\`
|
||||
${error.stack || 'No stack trace available'}
|
||||
\`\`\`
|
||||
|
||||
### Arguments
|
||||
\`\`\`
|
||||
${error.args ? error.args.map((arg, index) => {
|
||||
if (arg && typeof arg === 'object' && arg.__type === 'Error') {
|
||||
return `[${index}]: Error: ${arg.message}`;
|
||||
}
|
||||
return `[${index}]: ${typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)}`;
|
||||
}).join('\n') : 'No arguments'}
|
||||
\`\`\`
|
||||
|
||||
### Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
|
||||
### Additional Context
|
||||
|
||||
`;
|
||||
|
||||
const encodedTitle = encodeURIComponent(title);
|
||||
const encodedBody = encodeURIComponent(body);
|
||||
|
||||
return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`;
|
||||
};
|
||||
|
||||
const handleReportIssue = () => {
|
||||
const url = generateGitHubIssueUrl();
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Error Information</h4>
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<label>Message:</label>
|
||||
<span className="error-message-full">{error.message || 'No message available'}</span>
|
||||
</div>
|
||||
|
||||
{error.filename && (
|
||||
<div className="info-item">
|
||||
<label>File:</label>
|
||||
<span className="file-path">{error.filename}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error.lineno && (
|
||||
<div className="info-item">
|
||||
<label>Line:</label>
|
||||
<span>{error.lineno}{error.colno ? `:${error.colno}` : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="info-item">
|
||||
<label>Timestamp:</label>
|
||||
<span>{formatTimestamp(error.timestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h4>Report Issue</h4>
|
||||
<div className="report-section">
|
||||
<p>Found a bug? Help us improve Bruno by reporting this error on GitHub.</p>
|
||||
<button
|
||||
className="report-button"
|
||||
onClick={handleReportIssue}
|
||||
title="Report this error on GitHub"
|
||||
>
|
||||
<IconBrandGithub size={16} strokeWidth={1.5} />
|
||||
<span>Report Issue on GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StackTraceTab = ({ error }) => {
|
||||
const formatStackTrace = (stack) => {
|
||||
if (!stack) return 'Stack trace not available';
|
||||
|
||||
return stack
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Stack Trace</h4>
|
||||
<div className="stack-trace-container">
|
||||
<pre className="stack-trace">
|
||||
{formatStackTrace(error.stack)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ArgumentsTab = ({ error }) => {
|
||||
const formatArguments = (args) => {
|
||||
if (!args || args.length === 0) return 'No arguments available';
|
||||
|
||||
try {
|
||||
return args.map((arg, index) => {
|
||||
// Handle special Error object format
|
||||
if (arg && typeof arg === 'object' && arg.__type === 'Error') {
|
||||
return `[${index}]: Error: ${arg.message}\n Name: ${arg.name}\n Stack: ${arg.stack || 'No stack trace'}`;
|
||||
}
|
||||
|
||||
if (typeof arg === 'object' && arg !== null) {
|
||||
return `[${index}]: ${JSON.stringify(arg, null, 2)}`;
|
||||
}
|
||||
|
||||
return `[${index}]: ${String(arg)}`;
|
||||
}).join('\n\n');
|
||||
} catch (e) {
|
||||
return 'Arguments could not be formatted';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Arguments</h4>
|
||||
<div className="arguments-container">
|
||||
<pre className="arguments">
|
||||
{formatArguments(error.args)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorDetailsPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { selectedError } = useSelector(state => state.logs);
|
||||
const [activeTab, setActiveTab] = useState('info');
|
||||
|
||||
if (!selectedError) return null;
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(clearSelectedError());
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const getTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'info':
|
||||
return <ErrorInfoTab error={selectedError} />;
|
||||
case 'stack':
|
||||
return <StackTraceTab error={selectedError} />;
|
||||
case 'args':
|
||||
return <ArgumentsTab error={selectedError} />;
|
||||
default:
|
||||
return <ErrorInfoTab error={selectedError} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="panel-header">
|
||||
<div className="panel-title">
|
||||
<IconBug size={16} strokeWidth={1.5} />
|
||||
<span>Error Details</span>
|
||||
<span className="error-time">({formatTime(selectedError.timestamp)})</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="close-button"
|
||||
onClick={handleClose}
|
||||
title="Close details panel"
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel-tabs">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('info')}
|
||||
>
|
||||
<IconFileText size={14} strokeWidth={1.5} />
|
||||
Info
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'stack' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('stack')}
|
||||
>
|
||||
<IconStack size={14} strokeWidth={1.5} />
|
||||
Stack
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'args' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('args')}
|
||||
>
|
||||
<IconCode size={14} strokeWidth={1.5} />
|
||||
Args
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel-content">
|
||||
{getTabContent()}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorDetailsPanel;
|
||||
@@ -1,293 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
overflow: hidden;
|
||||
|
||||
.network-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.network-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.request-count {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.network-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* Important for proper flex behavior */
|
||||
}
|
||||
|
||||
.network-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${(props) => props.theme.console.emptyColor};
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 40px 20px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.requests-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0; /* Important for proper flex behavior */
|
||||
}
|
||||
|
||||
.requests-header {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.requests-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0; /* Important for proper scrolling */
|
||||
}
|
||||
|
||||
.request-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 6px 16px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.request-domain {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.request-path {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.request-time {
|
||||
color: ${(props) => props.theme.console.timestampColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.request-duration {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.request-size {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-width: 250px;
|
||||
background: ${(props) => props.theme.console.dropdownBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-dropdown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.filter-toggle-all {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-options {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.optionHoverBg};
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0 8px 0 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-option-label {
|
||||
color: ${(props) => props.theme.console.optionLabelColor};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.filter-option-count {
|
||||
color: ${(props) => props.theme.console.optionCountColor};
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,302 +0,0 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
IconFilter,
|
||||
IconChevronDown,
|
||||
IconNetwork,
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters,
|
||||
setSelectedRequest
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MethodBadge = ({ method }) => {
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className="method-badge"
|
||||
style={{ backgroundColor: getMethodColor(method) }}
|
||||
>
|
||||
{method?.toUpperCase() || 'GET'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBadge = ({ status, statusCode }) => {
|
||||
const getStatusColor = (code) => {
|
||||
if (code >= 200 && code < 300) return '#10b981';
|
||||
if (code >= 300 && code < 400) return '#f59e0b';
|
||||
if (code >= 400 && code < 500) return '#ef4444';
|
||||
if (code >= 500) return '#dc2626';
|
||||
return '#6b7280';
|
||||
};
|
||||
|
||||
const displayStatus = statusCode || status;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="status-badge"
|
||||
style={{ color: getStatusColor(statusCode) }}
|
||||
>
|
||||
{displayStatus}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const allFiltersEnabled = Object.values(filters).every(f => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="filter-dropdown-trigger"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Filter requests by method"
|
||||
>
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
<span className="filter-summary">
|
||||
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
|
||||
</span>
|
||||
<IconChevronDown size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className={`filter-dropdown-menu right`}>
|
||||
<div className="filter-dropdown-header">
|
||||
<span>Filter by Method</span>
|
||||
<button
|
||||
className="filter-toggle-all"
|
||||
onClick={() => onToggleAll(!allFiltersEnabled)}
|
||||
>
|
||||
{allFiltersEnabled ? 'Hide All' : 'Show All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="filter-dropdown-options">
|
||||
{Object.keys(filters).map(method => (
|
||||
<label key={method} className="filter-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[method]}
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<MethodBadge method={method} />
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestRow = ({ request, isSelected, onClick }) => {
|
||||
const { data } = request;
|
||||
const { request: req, response: res, timestamp } = data;
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
};
|
||||
|
||||
const formatDuration = (duration) => {
|
||||
if (!duration) return '-';
|
||||
if (duration < 1000) return `${Math.round(duration)}ms`;
|
||||
return `${(duration / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
const formatSize = (size) => {
|
||||
if (!size) return '-';
|
||||
if (size < 1024) return `${size}B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`;
|
||||
return `${(size / (1024 * 1024)).toFixed(1)}MB`;
|
||||
};
|
||||
|
||||
const getUrl = () => {
|
||||
return req?.url || 'Unknown URL';
|
||||
};
|
||||
|
||||
const getDomain = () => {
|
||||
try {
|
||||
const url = new URL(getUrl());
|
||||
return url.hostname;
|
||||
} catch {
|
||||
return getUrl();
|
||||
}
|
||||
};
|
||||
|
||||
const getPath = () => {
|
||||
try {
|
||||
const url = new URL(getUrl());
|
||||
return url.pathname + url.search;
|
||||
} catch {
|
||||
return getUrl();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`request-row ${isSelected ? 'selected' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="request-method">
|
||||
<MethodBadge method={req?.method} />
|
||||
</div>
|
||||
|
||||
<div className="request-status">
|
||||
<StatusBadge status={res?.status} statusCode={res?.statusCode} />
|
||||
</div>
|
||||
|
||||
<div className="request-domain" title={getDomain()}>
|
||||
{getDomain()}
|
||||
</div>
|
||||
|
||||
<div className="request-path" title={getPath()}>
|
||||
{getPath()}
|
||||
</div>
|
||||
|
||||
<div className="request-time">
|
||||
{formatTime(timestamp)}
|
||||
</div>
|
||||
|
||||
<div className="request-duration">
|
||||
{formatDuration(res?.duration)}
|
||||
</div>
|
||||
|
||||
<div className="request-size">
|
||||
{formatSize(res?.size)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkTab = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { networkFilters, selectedRequest } = useSelector(state => state.logs);
|
||||
const collections = useSelector(state => state.collections.collections);
|
||||
|
||||
const allRequests = useMemo(() => {
|
||||
const requests = [];
|
||||
|
||||
collections.forEach(collection => {
|
||||
if (collection.timeline) {
|
||||
collection.timeline
|
||||
.filter(entry => entry.type === 'request')
|
||||
.forEach(entry => {
|
||||
requests.push({
|
||||
...entry,
|
||||
collectionName: collection.name,
|
||||
collectionUid: collection.uid
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return requests.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}, [collections]);
|
||||
|
||||
const filteredRequests = useMemo(() => {
|
||||
return allRequests.filter(request => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
return networkFilters[method];
|
||||
});
|
||||
}, [allRequests, networkFilters]);
|
||||
|
||||
const requestCounts = useMemo(() => {
|
||||
return allRequests.reduce((counts, request) => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
counts[method] = (counts[method] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}, [allRequests]);
|
||||
|
||||
const handleFilterToggle = (method, enabled) => {
|
||||
dispatch(updateNetworkFilter({ method, enabled }));
|
||||
};
|
||||
|
||||
const handleToggleAllFilters = (enabled) => {
|
||||
dispatch(toggleAllNetworkFilters(enabled));
|
||||
};
|
||||
|
||||
const handleRequestClick = (request) => {
|
||||
dispatch(setSelectedRequest(request));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="network-content">
|
||||
{filteredRequests.length === 0 ? (
|
||||
<div className="network-empty">
|
||||
<IconNetwork size={48} strokeWidth={1} />
|
||||
<p>No network requests</p>
|
||||
<span>Requests will appear here as you make API calls</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="requests-container">
|
||||
<div className="requests-header">
|
||||
<div>Method</div>
|
||||
<div>Status</div>
|
||||
<div>Domain</div>
|
||||
<div>Path</div>
|
||||
<div>Time</div>
|
||||
<div className="text-right">Duration</div>
|
||||
<div className="text-right">Size</div>
|
||||
</div>
|
||||
|
||||
<div className="requests-list">
|
||||
{filteredRequests.map((request, index) => (
|
||||
<RequestRow
|
||||
key={`${request.collectionUid}-${request.itemUid}-${request.timestamp}-${index}`}
|
||||
request={request}
|
||||
isSelected={selectedRequest?.timestamp === request.timestamp && selectedRequest?.itemUid === request.itemUid}
|
||||
onClick={() => handleRequestClick(request)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkTab;
|
||||
@@ -1,347 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
border-left: 1px solid ${(props) => props.theme.console.border};
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
width: 40%;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.request-time {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.console.checkboxColor};
|
||||
border-bottom-color: ${(props) => props.theme.console.checkboxColor};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-height: min-content;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
padding: 4px 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
}
|
||||
|
||||
.headers-table,
|
||||
.timeline-table {
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
max-height: 300px;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
|
||||
thead {
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-name,
|
||||
.timeline-phase {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.header-value,
|
||||
.timeline-message {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.timeline-duration {
|
||||
color: ${(props) => props.theme.console.timestampColor};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 400px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: ${(props) => props.theme.console.emptyColor};
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.response-body-container {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.w-full.h-full.relative.flex {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
background: ${(props) => props.theme.console.headerBg} !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
div[role="tablist"] {
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
display: flex !important;
|
||||
gap: 8px !important;
|
||||
flex-wrap: wrap !important;
|
||||
align-items: center !important;
|
||||
min-height: 40px !important;
|
||||
flex-shrink: 0 !important;
|
||||
|
||||
> div {
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
font-size: 12px !important;
|
||||
padding: 6px 12px !important;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
white-space: nowrap !important;
|
||||
min-width: auto !important;
|
||||
height: auto !important;
|
||||
line-height: 1.2 !important;
|
||||
font-weight: 500 !important;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.console.checkboxColor};
|
||||
color: white;
|
||||
border-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
.response-filter {
|
||||
position: absolute !important;
|
||||
bottom: 8px !important;
|
||||
right: 8px !important;
|
||||
left: 8px !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs-container {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
|
||||
.network-logs {
|
||||
background: ${(props) => props.theme.console.contentBg} !important;
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
height: 100% !important;
|
||||
max-height: 400px !important;
|
||||
|
||||
pre {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-size: 11px !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,242 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
IconX,
|
||||
IconFileText,
|
||||
IconArrowRight,
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import { clearSelectedRequest } from 'providers/ReduxStore/slices/logs';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import Network from 'components/ResponsePane/Timeline/TimelineItem/Network';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common/index';
|
||||
|
||||
const RequestTab = ({ request, response }) => {
|
||||
const formatHeaders = (headers) => {
|
||||
if (!headers) return [];
|
||||
if (Array.isArray(headers)) return headers;
|
||||
return Object.entries(headers).map(([key, value]) => ({ name: key, value }));
|
||||
};
|
||||
|
||||
const formatBody = (body) => {
|
||||
if (!body) return 'No body';
|
||||
if (typeof body === 'string') return body;
|
||||
return JSON.stringify(body, null, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>General</h4>
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<span className="label">Request URL:</span>
|
||||
<span className="value">{request?.url || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Request Method:</span>
|
||||
<span className="value">{request?.method || 'GET'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h4>Request Headers</h4>
|
||||
{formatHeaders(request?.headers).length > 0 ? (
|
||||
<div className="headers-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formatHeaders(request.headers).map((header, index) => (
|
||||
<tr key={index}>
|
||||
<td className="header-name">{header.name}</td>
|
||||
<td className="header-value">{header.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">No headers</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{request?.body && (
|
||||
<div className="section">
|
||||
<h4>Request Body</h4>
|
||||
<pre className="code-block">{formatBody(request.body)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResponseTab = ({ response, request, collection }) => {
|
||||
const formatHeaders = (headers) => {
|
||||
if (!headers) return [];
|
||||
if (Array.isArray(headers)) return headers;
|
||||
return Object.entries(headers).map(([key, value]) => ({ name: key, value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Response Headers</h4>
|
||||
{formatHeaders(response?.headers).length > 0 ? (
|
||||
<div className="headers-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formatHeaders(response.headers).map((header, index) => (
|
||||
<tr key={index}>
|
||||
<td className="header-name">{header.name}</td>
|
||||
<td className="header-value">{header.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">No headers</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h4>Response Body</h4>
|
||||
<div className="response-body-container">
|
||||
{response?.data || response?.dataBuffer ? (
|
||||
<QueryResult
|
||||
item={{ uid: uuid()}}
|
||||
collection={collection}
|
||||
data={response.data}
|
||||
dataBuffer={response.dataBuffer}
|
||||
headers={response.headers}
|
||||
error={response.error}
|
||||
disableRunEventListener={true}
|
||||
/>
|
||||
) : (
|
||||
<div className="empty-state">No response data</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkTab = ({ response }) => {
|
||||
const timeline = response?.timeline || [];
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Network Logs</h4>
|
||||
<div className="network-logs-container">
|
||||
{timeline.length > 0 ? (
|
||||
<Network logs={timeline} />
|
||||
) : (
|
||||
<div className="empty-state">No network logs available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestDetailsPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { selectedRequest } = useSelector(state => state.logs);
|
||||
const collections = useSelector(state => state.collections.collections);
|
||||
const [activeTab, setActiveTab] = useState('request');
|
||||
|
||||
if (!selectedRequest) return null;
|
||||
|
||||
const { data } = selectedRequest;
|
||||
const { request, response } = data;
|
||||
|
||||
const collection = collections.find(c => c.uid === selectedRequest.collectionUid);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(clearSelectedRequest());
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const getTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'request':
|
||||
return <RequestTab request={request} response={response} />;
|
||||
case 'response':
|
||||
return <ResponseTab response={response} request={request} collection={collection} />;
|
||||
case 'network':
|
||||
return <NetworkTab response={response} />;
|
||||
default:
|
||||
return <RequestTab request={request} response={response} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="panel-header">
|
||||
<div className="panel-title">
|
||||
<IconFileText size={16} strokeWidth={1.5} />
|
||||
<span>Request Details</span>
|
||||
<span className="request-time">({formatTime(selectedRequest.timestamp)})</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="close-button"
|
||||
onClick={handleClose}
|
||||
title="Close details panel"
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel-tabs">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('request')}
|
||||
>
|
||||
<IconArrowRight size={14} strokeWidth={1.5} />
|
||||
Request
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'response' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('response')}
|
||||
>
|
||||
<IconFileText size={14} strokeWidth={1.5} />
|
||||
Response
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'network' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('network')}
|
||||
>
|
||||
<IconNetwork size={14} strokeWidth={1.5} />
|
||||
Network
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel-content">
|
||||
{getTabContent()}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestDetailsPanel;
|
||||
@@ -1,520 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.console.bg};
|
||||
border-top: 1px solid ${(props) => props.theme.console.border};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.console-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.console-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.console-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.console.checkboxColor};
|
||||
border-bottom-color: ${(props) => props.theme.console.checkboxColor};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
}
|
||||
}
|
||||
|
||||
.console-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.console-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.log-count {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.network-with-details {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.network-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.debug-with-details {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 8px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
|
||||
.action-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.border};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
max-width: 250px;
|
||||
background: ${(props) => props.theme.console.dropdownBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
|
||||
&.right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.filter-toggle-all {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-options {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.optionHoverBg};
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0 8px 0 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-option-label {
|
||||
color: ${(props) => props.theme.console.optionLabelColor};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.filter-option-count {
|
||||
color: ${(props) => props.theme.console.optionCountColor};
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.console-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${(props) => props.theme.console.emptyColor};
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 40px 20px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 4px 16px;
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
border-left: 2px solid transparent;
|
||||
transition: background-color 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-left-color: #f14c4c;
|
||||
|
||||
.log-level {
|
||||
background: #f14c4c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
color: #f14c4c;
|
||||
}
|
||||
}
|
||||
|
||||
&.warn {
|
||||
border-left-color: #ffcc02;
|
||||
|
||||
.log-level {
|
||||
background: #ffcc02;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
color: #ffcc02;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
border-left-color: #0078d4;
|
||||
|
||||
.log-level {
|
||||
background: #0078d4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
color: #0078d4;
|
||||
}
|
||||
}
|
||||
|
||||
&.debug {
|
||||
border-left-color: #9b59b6;
|
||||
|
||||
.log-level {
|
||||
background: #9b59b6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
color: #9b59b6;
|
||||
}
|
||||
}
|
||||
|
||||
&.log {
|
||||
border-left-color: #6a6a6a;
|
||||
|
||||
.log-level {
|
||||
background: #6a6a6a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
color: #6a6a6a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: ${(props) => props.theme.console.timestampColor};
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
|
||||
.log-object {
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
|
||||
.react-json-view {
|
||||
background: transparent !important;
|
||||
|
||||
.object-key-val {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.object-key {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.object-value {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
}
|
||||
|
||||
.string-value {
|
||||
color: ${(props) => props.theme.colors?.text?.green || (props.theme.console.messageColor)} !important;
|
||||
}
|
||||
|
||||
.number-value {
|
||||
color: ${(props) => props.theme.colors?.text?.purple || (props.theme.console.messageColor)} !important;
|
||||
}
|
||||
|
||||
.boolean-value {
|
||||
color: ${(props) => props.theme.colors?.text?.yellow || (props.theme.console.messageColor)} !important;
|
||||
}
|
||||
|
||||
.null-value {
|
||||
color: ${(props) => props.theme.colors?.text?.danger || (props.theme.console.messageColor)} !important;
|
||||
}
|
||||
|
||||
.object-size {
|
||||
color: ${(props) => props.theme.console.timestampColor} !important;
|
||||
}
|
||||
|
||||
.brace, .bracket {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
}
|
||||
|
||||
.collapsed-icon, .expanded-icon {
|
||||
color: ${(props) => props.theme.console.checkboxColor} !important;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
color: ${(props) => props.theme.console.checkboxColor} !important;
|
||||
}
|
||||
|
||||
.click-to-expand, .click-to-collapse {
|
||||
color: ${(props) => props.theme.console.checkboxColor} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,531 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
IconX,
|
||||
IconTrash,
|
||||
IconFilter,
|
||||
IconAlertTriangle,
|
||||
IconAlertCircle,
|
||||
IconBug,
|
||||
IconCode,
|
||||
IconChevronDown,
|
||||
IconTerminal2,
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
closeConsole,
|
||||
clearLogs,
|
||||
updateFilter,
|
||||
toggleAllFilters,
|
||||
setActiveTab,
|
||||
clearDebugErrors,
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
import NetworkTab from './NetworkTab';
|
||||
import RequestDetailsPanel from './RequestDetailsPanel';
|
||||
// import DebugTab from './DebugTab';
|
||||
import ErrorDetailsPanel from './ErrorDetailsPanel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LogIcon = ({ type }) => {
|
||||
const iconProps = { size: 16, strokeWidth: 1.5 };
|
||||
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return <IconAlertCircle className="log-icon error" {...iconProps} />;
|
||||
case 'warn':
|
||||
return <IconAlertTriangle className="log-icon warn" {...iconProps} />;
|
||||
case 'info':
|
||||
return <IconAlertTriangle className="log-icon info" {...iconProps} />;
|
||||
// case 'debug':
|
||||
// return <IconBug className="log-icon debug" {...iconProps} />;
|
||||
default:
|
||||
return <IconCode className="log-icon log" {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const LogTimestamp = ({ timestamp }) => {
|
||||
const date = new Date(timestamp);
|
||||
const time = date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
|
||||
return <span className="log-timestamp">{time}</span>;
|
||||
};
|
||||
|
||||
const LogMessage = ({ message, args }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const formatMessage = (msg, originalArgs) => {
|
||||
if (originalArgs && originalArgs.length > 0) {
|
||||
return originalArgs.map((arg, index) => {
|
||||
if (typeof arg === 'object' && arg !== null) {
|
||||
return (
|
||||
<div key={index} className="log-object">
|
||||
<ReactJson
|
||||
src={arg}
|
||||
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
iconStyle="triangle"
|
||||
indentWidth={2}
|
||||
collapsed={1}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
enableClipboard={false}
|
||||
name={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return String(arg);
|
||||
});
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
const formattedMessage = formatMessage(message, args);
|
||||
|
||||
return (
|
||||
<span className="log-message">
|
||||
{Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
|
||||
<span key={index}>{item} </span>
|
||||
)) : formattedMessage}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const allFiltersEnabled = Object.values(filters).every(f => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="filter-dropdown-trigger"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Filter logs by type"
|
||||
>
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
<span className="filter-summary">
|
||||
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
|
||||
</span>
|
||||
<IconChevronDown size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className={`filter-dropdown-menu right`}>
|
||||
<div className="filter-dropdown-header">
|
||||
<span>Filter by Type</span>
|
||||
<button
|
||||
className="filter-toggle-all"
|
||||
onClick={() => onToggleAll(!allFiltersEnabled)}
|
||||
>
|
||||
{allFiltersEnabled ? 'Hide All' : 'Show All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="filter-dropdown-options">
|
||||
{Object.entries(filters).map(([filterType, enabled]) => (
|
||||
<label key={filterType} className="filter-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => onFilterToggle(filterType, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<LogIcon type={filterType} />
|
||||
<span className="filter-option-label">{filterType}</span>
|
||||
<span className="filter-option-count">({logCounts[filterType] || 0})</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const allFiltersEnabled = Object.values(filters).every(f => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="filter-dropdown-trigger"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Filter requests by method"
|
||||
>
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
<span className="filter-summary">
|
||||
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
|
||||
</span>
|
||||
<IconChevronDown size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className={`filter-dropdown-menu right`}>
|
||||
<div className="filter-dropdown-header">
|
||||
<span>Filter by Method</span>
|
||||
<button
|
||||
className="filter-toggle-all"
|
||||
onClick={() => onToggleAll(!allFiltersEnabled)}
|
||||
>
|
||||
{allFiltersEnabled ? 'Hide All' : 'Show All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="filter-dropdown-options">
|
||||
{Object.entries(filters).map(([method, enabled]) => (
|
||||
<label key={method} className="filter-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<span className="method-badge" style={{ backgroundColor: getMethodColor(method) }}>
|
||||
{method}
|
||||
</span>
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {
|
||||
const logsEndRef = useRef(null);
|
||||
const prevLogsCountRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Only scroll when new logs are added, not when switching tabs
|
||||
if (logsEndRef.current && logs.length > prevLogsCountRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'auto' });
|
||||
}
|
||||
prevLogsCountRef.current = logs.length;
|
||||
}, [logs]);
|
||||
|
||||
const filteredLogs = logs.filter(log => filters[log.type]);
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="tab-content-area">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="console-empty">
|
||||
<IconTerminal2 size={48} strokeWidth={1} />
|
||||
<p>No logs to display</p>
|
||||
<span>Logs will appear here as your application runs</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="logs-container">
|
||||
{filteredLogs.map((log) => (
|
||||
<div key={log.id} className={`log-entry ${log.type}`}>
|
||||
<div className="log-meta">
|
||||
<LogTimestamp timestamp={log.timestamp} />
|
||||
<LogIcon type={log.type} />
|
||||
</div>
|
||||
<LogMessage message={log.message} args={log.args} />
|
||||
</div>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Console = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector(state => state.logs);
|
||||
const collections = useSelector(state => state.collections.collections);
|
||||
const consoleRef = useRef(null);
|
||||
|
||||
const logCounts = logs.reduce((counts, log) => {
|
||||
counts[log.type] = (counts[log.type] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
|
||||
const allRequests = React.useMemo(() => {
|
||||
const requests = [];
|
||||
|
||||
collections.forEach(collection => {
|
||||
if (collection.timeline) {
|
||||
collection.timeline
|
||||
.filter(entry => entry.type === 'request')
|
||||
.forEach(entry => {
|
||||
requests.push({
|
||||
...entry,
|
||||
collectionName: collection.name,
|
||||
collectionUid: collection.uid
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return requests.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}, [collections]);
|
||||
|
||||
const filteredLogs = logs.filter(log => filters[log.type]);
|
||||
const filteredRequests = allRequests.filter(request => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
return networkFilters[method];
|
||||
});
|
||||
|
||||
const requestCounts = allRequests.reduce((counts, request) => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
counts[method] = (counts[method] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
|
||||
const handleFilterToggle = (filterType, enabled) => {
|
||||
dispatch(updateFilter({ filterType, enabled }));
|
||||
};
|
||||
|
||||
const handleNetworkFilterToggle = (method, enabled) => {
|
||||
dispatch(updateNetworkFilter({ method, enabled }));
|
||||
};
|
||||
|
||||
const handleClearLogs = () => {
|
||||
dispatch(clearLogs());
|
||||
};
|
||||
|
||||
const handleClearDebugErrors = () => {
|
||||
dispatch(clearDebugErrors());
|
||||
};
|
||||
|
||||
const handlecloseConsole = () => {
|
||||
dispatch(closeConsole());
|
||||
};
|
||||
|
||||
const handleToggleAllFilters = (enabled) => {
|
||||
dispatch(toggleAllFilters(enabled));
|
||||
};
|
||||
|
||||
const handleToggleAllNetworkFilters = (enabled) => {
|
||||
dispatch(toggleAllNetworkFilters(enabled));
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
dispatch(setActiveTab(tab));
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'console':
|
||||
return (
|
||||
<ConsoleTab
|
||||
logs={logs}
|
||||
filters={filters}
|
||||
logCounts={logCounts}
|
||||
onFilterToggle={handleFilterToggle}
|
||||
onToggleAll={handleToggleAllFilters}
|
||||
onClearLogs={handleClearLogs}
|
||||
/>
|
||||
);
|
||||
case 'network':
|
||||
return <NetworkTab />;
|
||||
// case 'debug':
|
||||
// return <DebugTab />;
|
||||
default:
|
||||
return (
|
||||
<ConsoleTab
|
||||
logs={logs}
|
||||
filters={filters}
|
||||
logCounts={logCounts}
|
||||
onFilterToggle={handleFilterToggle}
|
||||
onToggleAll={handleToggleAllFilters}
|
||||
onClearLogs={handleClearLogs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTabControls = () => {
|
||||
switch (activeTab) {
|
||||
case 'console':
|
||||
return (
|
||||
<div className="tab-controls">
|
||||
<div className="filter-controls">
|
||||
<FilterDropdown
|
||||
filters={filters}
|
||||
logCounts={logCounts}
|
||||
onFilterToggle={handleFilterToggle}
|
||||
onToggleAll={handleToggleAllFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="action-controls">
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={handleClearLogs}
|
||||
title="Clear all logs"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'network':
|
||||
return (
|
||||
<div className="tab-controls">
|
||||
<div className="filter-controls">
|
||||
<NetworkFilterDropdown
|
||||
filters={networkFilters}
|
||||
requestCounts={requestCounts}
|
||||
onFilterToggle={handleNetworkFilterToggle}
|
||||
onToggleAll={handleToggleAllNetworkFilters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// case 'debug':
|
||||
// return (
|
||||
// <div className="tab-controls">
|
||||
// <div className="action-controls">
|
||||
// {debugErrors.length > 0 && (
|
||||
// <button
|
||||
// className="control-button"
|
||||
// onClick={handleClearDebugErrors}
|
||||
// title="Clear all errors"
|
||||
// >
|
||||
// <IconTrash size={16} strokeWidth={1.5} />
|
||||
// </button>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper ref={consoleRef}>
|
||||
<div
|
||||
className="console-resize-handle"
|
||||
/>
|
||||
|
||||
<div className="console-header">
|
||||
<div className="console-tabs">
|
||||
<button
|
||||
className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('console')}
|
||||
>
|
||||
<IconTerminal2 size={16} strokeWidth={1.5} />
|
||||
<span>Console</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('network')}
|
||||
>
|
||||
<IconNetwork size={16} strokeWidth={1.5} />
|
||||
<span>Network</span>
|
||||
</button>
|
||||
|
||||
{/* <button
|
||||
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('debug')}
|
||||
>
|
||||
<IconBug size={16} strokeWidth={1.5} />
|
||||
<span>Debug</span>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
<div className="console-controls">
|
||||
{renderTabControls()}
|
||||
<button
|
||||
className="control-button close-button"
|
||||
onClick={handlecloseConsole}
|
||||
title="Close console"
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="console-content">
|
||||
{activeTab === 'network' && selectedRequest ? (
|
||||
<div className="network-with-details">
|
||||
<div className="network-main">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
<RequestDetailsPanel />
|
||||
</div>
|
||||
) : activeTab === 'debug' && selectedError ? (
|
||||
<div className="debug-with-details">
|
||||
<div className="debug-main">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
<ErrorDetailsPanel />
|
||||
</div>
|
||||
) : (
|
||||
renderTabContent()
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Console;
|
||||
@@ -1,88 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Console from './Console';
|
||||
|
||||
const MIN_DEVTOOLS_HEIGHT = 150;
|
||||
const MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7;
|
||||
const DEFAULT_DEVTOOLS_HEIGHT = 300;
|
||||
|
||||
const Devtools = ({ mainSectionRef }) => {
|
||||
const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
|
||||
const [isResizingDevtools, setIsResizingDevtools] = useState(false);
|
||||
|
||||
const handleDevtoolsResizeStart = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
setIsResizingDevtools(true);
|
||||
}, []);
|
||||
|
||||
const handleDevtoolsResize = useCallback((e) => {
|
||||
if (!isResizingDevtools || !mainSectionRef.current) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
const statusBarHeight = 22;
|
||||
const mouseY = e.clientY;
|
||||
|
||||
// Calculate new devtools height - expanding upward from bottom
|
||||
const newHeight = windowHeight - mouseY - statusBarHeight;
|
||||
const clampedHeight = Math.min(MAX_DEVTOOLS_HEIGHT, Math.max(MIN_DEVTOOLS_HEIGHT, newHeight));
|
||||
setDevtoolsHeight(clampedHeight);
|
||||
|
||||
// Update main section height
|
||||
if (mainSectionRef.current) {
|
||||
mainSectionRef.current.style.height = `calc(100vh - 22px - ${clampedHeight}px)`;
|
||||
}
|
||||
}, [isResizingDevtools, mainSectionRef]);
|
||||
|
||||
const handleDevtoolsResizeEnd = useCallback(() => {
|
||||
setIsResizingDevtools(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizingDevtools) {
|
||||
document.addEventListener('mousemove', handleDevtoolsResize);
|
||||
document.addEventListener('mouseup', handleDevtoolsResizeEnd);
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleDevtoolsResize);
|
||||
document.removeEventListener('mouseup', handleDevtoolsResizeEnd);
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}
|
||||
}, [isResizingDevtools, handleDevtoolsResize, handleDevtoolsResizeEnd]);
|
||||
|
||||
// Set initial height
|
||||
useEffect(() => {
|
||||
if (mainSectionRef.current && isDevtoolsOpen) {
|
||||
mainSectionRef.current.style.height = `calc(100vh - 22px - ${devtoolsHeight}px)`;
|
||||
}
|
||||
}, [isDevtoolsOpen, devtoolsHeight, mainSectionRef]);
|
||||
|
||||
if (!isDevtoolsOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onMouseDown={handleDevtoolsResizeStart}
|
||||
style={{
|
||||
height: '4px',
|
||||
cursor: 'row-resize',
|
||||
backgroundColor: isResizingDevtools ? '#0078d4' : 'transparent',
|
||||
transition: 'background-color 0.2s ease',
|
||||
zIndex: 20,
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#0078d4'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'}
|
||||
/>
|
||||
<div style={{ height: `${devtoolsHeight}px`, overflow: 'hidden', position: 'relative' }}>
|
||||
<Console />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Devtools;
|
||||
@@ -16,7 +16,6 @@ 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, ...props }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent }) => {
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
@@ -14,7 +14,6 @@ const Dropdown = ({ icon, children, onCreate, placement, transparent, ...props }
|
||||
interactive={true}
|
||||
trigger="click"
|
||||
appendTo="parent"
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useRef, useEffect } 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';
|
||||
@@ -14,9 +13,7 @@ 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 SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import { sensitiveFields } from './constants';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,50 +26,6 @@ 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 || [],
|
||||
@@ -108,8 +61,6 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
@@ -212,7 +163,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
@@ -223,12 +174,6 @@ 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
|
||||
|
||||
@@ -9,7 +9,7 @@ const ManageSecrets = ({ onClose }) => {
|
||||
<div>
|
||||
<p>In any collection, there are secrets that need to be managed.</p>
|
||||
<p className="mt-2">These secrets can be anything such as API keys, passwords, or tokens.</p>
|
||||
<p className="mt-4">Bruno offers three approaches to manage secrets in collections.</p>
|
||||
<p className="mt-4">Bruno offers two approaches to manage secrets in collections.</p>
|
||||
<p className="mt-2">
|
||||
Read more about it in our{' '}
|
||||
<a
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import React, { Component, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addDebugError } from 'providers/ReduxStore/slices/logs';
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
error: error,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({ hasError: false });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const serializeArgs = (args) => {
|
||||
return args.map(arg => {
|
||||
try {
|
||||
if (arg === null) return 'null';
|
||||
if (arg === undefined) return 'undefined';
|
||||
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
|
||||
return arg;
|
||||
}
|
||||
if (arg instanceof Error) {
|
||||
return {
|
||||
__type: 'Error',
|
||||
name: arg.name,
|
||||
message: arg.message,
|
||||
stack: arg.stack
|
||||
};
|
||||
}
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(arg));
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
} catch (e) {
|
||||
return '[Unserializable]';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to extract file and line info from stack trace
|
||||
const extractFileInfo = (stack) => {
|
||||
if (!stack) return { filename: null, lineno: null, colno: null };
|
||||
|
||||
try {
|
||||
const lines = stack.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line.includes('ErrorCapture') || line.trim() === 'Error') continue;
|
||||
|
||||
const match = line.match(/(?:at\s+.*?\s+)?\(?([^)]+):(\d+):(\d+)\)?/);
|
||||
if (match) {
|
||||
return {
|
||||
filename: match[1],
|
||||
lineno: parseInt(match[2]),
|
||||
colno: parseInt(match[3])
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
|
||||
return { filename: null, lineno: null, colno: null };
|
||||
};
|
||||
|
||||
const useGlobalErrorCapture = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
console.error = (...args) => {
|
||||
const currentStack = new Error().stack;
|
||||
|
||||
originalConsoleError.apply(console, args);
|
||||
|
||||
if (currentStack && currentStack.includes('useIpcEvents.js')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = args.join(' ');
|
||||
if (errorMessage.includes('removeConsoleLogListener')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filename, lineno, colno } = extractFileInfo(currentStack);
|
||||
|
||||
const serializedArgs = serializeArgs(args);
|
||||
|
||||
dispatch(addDebugError({
|
||||
message: errorMessage,
|
||||
stack: currentStack,
|
||||
filename: filename,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
args: serializedArgs,
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.error = originalConsoleError;
|
||||
};
|
||||
}, [dispatch]);
|
||||
};
|
||||
|
||||
const ErrorCapture = ({ children }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useGlobalErrorCapture();
|
||||
|
||||
const handleReactError = (errorData) => {
|
||||
dispatch(addDebugError(errorData));
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary onError={handleReactError}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorCapture;
|
||||
@@ -7,7 +7,6 @@ import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
@@ -36,8 +35,6 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'implicit':
|
||||
return <OAuth2Implicit save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
@@ -77,7 +74,7 @@ const Auth = ({ collection, folder }) => {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import React 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, setFolderHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } 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(
|
||||
@@ -72,22 +62,6 @@ 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">
|
||||
@@ -167,14 +141,9 @@ const Headers = ({ collection, folder }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -9,9 +9,17 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
<sup className="ml-[.125rem] opacity-80 font-medium">
|
||||
<DotIcon width="10"></DotIcon>
|
||||
</sup>
|
||||
);
|
||||
};
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let tab = 'headers';
|
||||
@@ -74,7 +82,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full overflow-auto">
|
||||
<StyledWrapper className="flex flex-col h-full">
|
||||
<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')}>
|
||||
@@ -83,11 +91,11 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
{hasScripts && <StatusDot />}
|
||||
{hasScripts && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
|
||||
Test
|
||||
{hasTests && <StatusDot />}
|
||||
{hasTests && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
@@ -95,13 +103,13 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <StatusDot />}
|
||||
{hasAuth && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
|
||||
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -74,20 +74,18 @@ class MultiLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
getAllVariables: getAllVariablesHandler,
|
||||
getAnywordAutocompleteHints
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { IconBell } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import StyledWrapper from './StyleWrapper';
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal';
|
||||
import { useEffect } from 'react';
|
||||
import { useApp } from 'providers/App';
|
||||
import {
|
||||
@@ -110,7 +109,7 @@ const Notifications = () => {
|
||||
>
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" offset={8}>
|
||||
<IconBell
|
||||
size={16}
|
||||
size={18}
|
||||
aria-hidden
|
||||
strokeWidth={1.5}
|
||||
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
|
||||
@@ -122,7 +121,6 @@ const Notifications = () => {
|
||||
</a>
|
||||
|
||||
{showNotificationsModal && (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="lg"
|
||||
title="Notifications"
|
||||
@@ -201,11 +199,10 @@ const Notifications = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="opacity-50 italic text-xs p-12 flex justify-center">You are all caught up!</div>
|
||||
<div className="opacity-50 italic text-xs p-12 flex justify-center">No Notifications</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
@@ -1,125 +0,0 @@
|
||||
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,7 +7,6 @@ import General from './General';
|
||||
import Proxy from './ProxySettings';
|
||||
import Display from './Display';
|
||||
import Keybindings from './Keybindings';
|
||||
import Beta from './Beta';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -38,10 +37,6 @@ const Preferences = ({ onClose }) => {
|
||||
return <Keybindings close={onClose} />;
|
||||
}
|
||||
|
||||
case 'beta': {
|
||||
return <Beta close={onClose} />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
@@ -51,7 +46,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
|
||||
@@ -68,9 +63,6 @@ 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,16 +6,13 @@ 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 SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import { update } from 'lodash';
|
||||
|
||||
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));
|
||||
|
||||
@@ -147,7 +144,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 flex items-center">
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -158,8 +155,6 @@ 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,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -67,7 +63,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -78,7 +74,6 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -15,8 +13,6 @@ 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));
|
||||
|
||||
@@ -40,7 +36,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 flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -51,7 +47,6 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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,8 +11,6 @@ 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));
|
||||
|
||||
@@ -66,7 +62,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -77,7 +73,6 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -84,7 +80,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -95,7 +91,6 @@ 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>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
@@ -1,306 +0,0 @@
|
||||
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,5 +1,4 @@
|
||||
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';
|
||||
@@ -10,15 +9,13 @@ 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,
|
||||
@@ -36,8 +33,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -87,7 +83,6 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
@@ -115,7 +110,6 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
@@ -135,15 +129,12 @@ 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 flex items-center">
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={value}
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -152,7 +143,6 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -336,13 +326,6 @@ 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,5 +1,4 @@
|
||||
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';
|
||||
@@ -10,15 +9,13 @@ 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 {
|
||||
@@ -33,8 +30,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -81,7 +77,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -101,15 +96,12 @@ 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 flex items-center">
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={value}
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -118,7 +110,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -293,13 +284,7 @@ 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>
|
||||
|
||||
@@ -101,15 +101,6 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('implicit');
|
||||
}}
|
||||
>
|
||||
Implicit
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.oauth2-input-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,233 +0,0 @@
|
||||
import React, { useRef, forwardRef, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Wrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
|
||||
const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
clientId,
|
||||
scope,
|
||||
state,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const interpolatedAuthUrl = useMemo(() => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
return interpolate(authorizationUrl, variables);
|
||||
}, [collection, item, authorizationUrl]);
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'implicit',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
clientId,
|
||||
state,
|
||||
scope,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleAutoFetchTokenToggle = (e) => {
|
||||
handleChange('autoFetchToken', e.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={authorizationUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || 'credentials'}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add Token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Headers
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tokenPlacement == 'header' ? (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-header-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth.tokenHeaderPrefix || 'Bearer'}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-key`}>
|
||||
<label className="block min-w-[140px]">URL Query Key</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth.tokenQueryKey || 'access_token'}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<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">
|
||||
Advanced Options
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={oAuth.autoFetchToken !== false}
|
||||
onChange={handleAutoFetchTokenToggle}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Auto fetch token</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when the current one expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdditionalParams
|
||||
item={item}
|
||||
request={request}
|
||||
collection={collection}
|
||||
updateAuth={updateAuth}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={interpolatedAuthUrl} credentialsId={credentialsId} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2Implicit;
|
||||
@@ -1,24 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'State'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -28,30 +28,20 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
const result = await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
forceGetToken: true
|
||||
}));
|
||||
|
||||
const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleFetchingToken(false);
|
||||
|
||||
// Check if the result contains error or if access_token is missing
|
||||
if (!result || !result.access_token) {
|
||||
const errorMessage = result?.error || 'No access token received from authorization server';
|
||||
console.error(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
|
||||
toast.success('Token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error(error?.message || 'An error occurred while fetching token!');
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,36 +51,26 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
const result = await dispatch(refreshOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
forceGetToken: true
|
||||
}));
|
||||
|
||||
const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
|
||||
// Check if the result contains error or if access_token is missing
|
||||
if (!result || !result.access_token) {
|
||||
const errorMessage = result?.error || 'No access token received from authorization server';
|
||||
console.error(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
|
||||
toast.success('Token refreshed successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error(error?.message || 'An error occurred while refreshing token!');
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('Cleared cache successfully');
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
@@ -99,22 +79,12 @@ 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`}
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
<button onClick={handleFetchOauth2Credentials} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
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`}
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
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`}>
|
||||
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,5 +1,4 @@
|
||||
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';
|
||||
@@ -10,16 +9,14 @@ 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,
|
||||
@@ -35,8 +32,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -84,7 +80,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -104,15 +99,12 @@ 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 flex items-center">
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={value}
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -121,7 +113,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -296,13 +287,6 @@ 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>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2Implicit from './Implicit/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -32,9 +31,6 @@ const GrantTypeComponentMap = ({ item, collection }) => {
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
case 'implicit':
|
||||
return <OAuth2Implicit item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -14,8 +12,6 @@ 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));
|
||||
|
||||
@@ -67,7 +63,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -78,7 +74,6 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" message={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ const Auth = ({ item, collection }) => {
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
@@ -109,7 +109,7 @@ const Auth = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-auto">
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -102,9 +101,6 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -156,9 +152,6 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
@@ -1,354 +0,0 @@
|
||||
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;
|
||||
@@ -1,119 +0,0 @@
|
||||
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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,125 +0,0 @@
|
||||
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;
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
@@ -1,124 +0,0 @@
|
||||
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;
|
||||
@@ -7,6 +7,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Auth from 'components/RequestPane/Auth';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
@@ -16,8 +17,22 @@ import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { useEffect } from 'react';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
<sup className="ml-[.125rem] opacity-80 font-medium">
|
||||
<DotIcon width="10"></DotIcon>
|
||||
</sup>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorIndicator = () => {
|
||||
return (
|
||||
<sup className="ml-[.125rem] opacity-80 font-medium text-red-500">
|
||||
<DotIcon width="10" ></DotIcon>
|
||||
</sup>
|
||||
);
|
||||
};
|
||||
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -62,9 +77,6 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -101,7 +113,6 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
const auth = getPropertyFromDraftOrRequest('request.auth');
|
||||
const tags = getPropertyFromDraftOrRequest('tags');
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
@@ -125,7 +136,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
{body.mode !== 'none' && <StatusDot />}
|
||||
{body.mode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
@@ -133,7 +144,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot />}
|
||||
{auth.mode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
@@ -142,9 +153,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && (
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
<StatusDot type="error" /> :
|
||||
<StatusDot />
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
<ErrorIndicator /> :
|
||||
<ContentIndicator />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
@@ -153,19 +164,11 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && tests.length > 0 && (
|
||||
item.testScriptErrorMessage ?
|
||||
<StatusDot type="error" /> :
|
||||
<StatusDot />
|
||||
)}
|
||||
{tests && tests.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
{tags && tags.length > 0 && <StatusDot />}
|
||||
{docs && docs.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
|
||||
@@ -20,7 +20,6 @@ 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);
|
||||
@@ -81,17 +80,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
{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} />
|
||||
)}
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
</div>
|
||||
<div
|
||||
id="request-url"
|
||||
className="flex items-center flex-grow input-container h-full"
|
||||
style={{
|
||||
color: 'yellow',
|
||||
|
||||
@@ -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, addHeaderText }) => {
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
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, addHeaderText }) => {
|
||||
</Table>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-action text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ {addHeaderText || 'Add Header'}
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addRequestTag, deleteRequestTag, updateCollectionTagsList } from 'providers/ReduxStore/slices/collections';
|
||||
import TagList from 'components/TagList/index';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const Tags = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
// all tags in the collection
|
||||
const collectionTags = collection.allTags || [];
|
||||
|
||||
// tags for the current request
|
||||
const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);
|
||||
|
||||
// Filter out tags that are already associated with the current request
|
||||
const collectionTagsWithoutCurrentRequestTags = collectionTags?.filter(tag => !tags.includes(tag)) || [];
|
||||
|
||||
const handleAdd = useCallback((tag) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (trimmedTag && !tags.includes(trimmedTag)) {
|
||||
dispatch(
|
||||
addRequestTag({
|
||||
tag: trimmedTag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, tags, item.uid, collection.uid]);
|
||||
|
||||
const handleRemove = useCallback((tag) => {
|
||||
dispatch(
|
||||
deleteRequestTag({
|
||||
tag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}, [dispatch, item.uid, collection.uid]);
|
||||
|
||||
const handleRequestSave = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateCollectionTagsList({ collectionUid: collection.uid }));
|
||||
}, [collection.uid, dispatch]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<TagList
|
||||
tagsHintList={collectionTagsWithoutCurrentRequestTags}
|
||||
handleAddTag={handleAdd}
|
||||
handleRemoveTag={handleRemove}
|
||||
tags={tags}
|
||||
onSave={handleRequestSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const ToggleSelector = ({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
disabled = false,
|
||||
size = 'small' // 'small', 'medium', 'large'
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: {
|
||||
container: 'h-4 w-8',
|
||||
thumb: 'h-3 w-3',
|
||||
translate: checked ? 'translate-x-4' : 'translate-x-1'
|
||||
},
|
||||
medium: {
|
||||
container: 'h-5 w-9',
|
||||
thumb: 'h-3 w-3',
|
||||
translate: checked ? 'translate-x-5' : 'translate-x-1'
|
||||
},
|
||||
large: {
|
||||
container: 'h-6 w-11',
|
||||
thumb: 'h-4 w-4',
|
||||
translate: checked ? 'translate-x-6' : 'translate-x-1'
|
||||
}
|
||||
};
|
||||
|
||||
const currentSize = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
disabled={disabled}
|
||||
className={`
|
||||
relative inline-flex ${currentSize.container} mx-1 items-center rounded-full transition-colors
|
||||
focus:outline-none focus:ring-1 focus:ring-offset-1
|
||||
${disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer'
|
||||
}
|
||||
${checked
|
||||
? 'bg-blue-600 dark:bg-blue-500'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block ${currentSize.thumb} transform rounded-full bg-white transition-transform
|
||||
${currentSize.translate}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{label}
|
||||
</label>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleSelector;
|
||||
@@ -1,50 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconTag } from '@tabler/icons';
|
||||
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
|
||||
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
||||
import Tags from './Tags/index';
|
||||
|
||||
const Settings = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
|
||||
const getPropertyFromDraftOrRequest = (propertyKey) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});
|
||||
|
||||
const { encodeUrl } = getPropertyFromDraftOrRequest('settings');
|
||||
|
||||
const onToggleUrlEncoding = useCallback(() => {
|
||||
dispatch(updateItemSettings({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
settings: { encodeUrl: !encodeUrl }
|
||||
}));
|
||||
}, [encodeUrl, dispatch, collection.uid, item.uid]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-10">
|
||||
<div className='flex flex-col gap-2 max-w-[400px]'>
|
||||
<h3 className="text-xs font-medium text-gray-900 dark:text-gray-100 flex items-center gap-1">
|
||||
<IconTag size={16} />
|
||||
Tags
|
||||
</h3>
|
||||
<div label="Tags">
|
||||
<Tags item={item} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<ToggleSelector
|
||||
checked={encodeUrl}
|
||||
onChange={onToggleUrlEncoding}
|
||||
label="URL Encoding"
|
||||
description="Automatically encode query parameters in the URL"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,13 +1,16 @@
|
||||
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
|
||||
import { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { loadRequest, loadRequestViaWorker } 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 handleLoadLargeRequest = () => {
|
||||
!item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
const handleLoadRequest = () => {
|
||||
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
return <StyledWrapper>
|
||||
@@ -41,14 +44,23 @@ 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={handleLoadLargeRequest}
|
||||
onClick={handleLoadRequest}
|
||||
>
|
||||
Load Request
|
||||
Force load
|
||||
</button>
|
||||
<p>(Uses a regex based parsing approach)</p>
|
||||
<p>(May cause the app to freeze temporarily while it runs)</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@ const StyledWrapper = styled.div`
|
||||
padding: 0;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
|
||||
div.dragbar-handle {
|
||||
display: flex;
|
||||
@@ -46,7 +45,6 @@ const StyledWrapper = styled.div`
|
||||
height: 10px;
|
||||
cursor: row-resize;
|
||||
padding: 0 1rem;
|
||||
position: relative;
|
||||
|
||||
div.dragbar-handle {
|
||||
width: 100%;
|
||||
|
||||
@@ -4,16 +4,13 @@ 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/index';
|
||||
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import VariablesEditor from 'components/VariablesEditor';
|
||||
@@ -183,9 +180,6 @@ 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} />;
|
||||
}
|
||||
@@ -207,7 +201,7 @@ const RequestTabPanel = () => {
|
||||
if (!folder) {
|
||||
return <FolderNotFound folderUid={focusedTab.folderUid} />;
|
||||
}
|
||||
|
||||
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
@@ -215,32 +209,20 @@ 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
|
||||
@@ -251,13 +233,9 @@ const RequestTabPanel = () => {
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
{isGrpcRequest ? (
|
||||
<GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
) : (
|
||||
<QueryUrl 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 overflow-auto`}>
|
||||
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative`}>
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4 h-full"
|
||||
@@ -282,10 +260,6 @@ const RequestTabPanel = () => {
|
||||
{item.type === 'http-request' ? (
|
||||
<HttpRequestPane item={item} collection={collection} />
|
||||
) : null}
|
||||
|
||||
{isGrpcRequest ? (
|
||||
<GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -294,20 +268,7 @@ const RequestTabPanel = () => {
|
||||
</div>
|
||||
|
||||
<section className="response-pane flex-grow overflow-x-auto">
|
||||
{item.type === 'grpc-request' ? (
|
||||
<GrpcResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
|
||||
response={item.response}
|
||||
/>
|
||||
) : (
|
||||
<ResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
response={item.response}
|
||||
/>
|
||||
)}
|
||||
<ResponsePane item={item} collection={collection} response={item.response} />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user