Compare commits

..

2 Commits

Author SHA1 Message Date
ramki-bruno
d4fbca2759 Perf improvements in Response-preview with useMemo 2025-03-25 23:16:01 +05:30
ramki-bruno
7622a4aaae Revert "Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings"
This reverts commit 56581b3641.
2025-03-25 22:22:43 +05:30
409 changed files with 5733 additions and 29126 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: helloanoop

View File

@@ -27,9 +27,7 @@ body:
required: false
- label: annoying
required: false
- label: this feature was working in a previous version but is broken in the current release.
required: false
- type: input
attributes:
label: Bruno version

View File

@@ -28,11 +28,6 @@ jobs:
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
- name: Lint Check
run: npm run lint
# tests
- name: Test Package bruno-js
@@ -50,8 +45,6 @@ jobs:
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
@@ -78,8 +71,6 @@ jobs:
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
- name: Run tests
run: |
@@ -91,43 +82,5 @@ jobs:
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: CLI Test Results
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
e2e-test:
name: Playwright E2E Tests
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: v22.11.x
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
npm ci --legacy-peer-deps
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
- name: Build libraries
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

6
.gitignore vendored
View File

@@ -46,8 +46,4 @@ yarn-error.log*
#dev editor
bruno.iml
.idea
.vscode
# Playwright
/blob-report/
.idea

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -15,15 +15,15 @@
| [正體中文](docs/contributing/contributing_zhtw.md)
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
| [Dutch](docs/contributing/contributing_nl.md)
| [Nederlands](docs/contributing/contributing_nl.md)
## Let's make Bruno better, together!!
We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer.
We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer.
### Technology Stack
Bruno is built using React and Electron.
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use
@@ -37,76 +37,38 @@ Libraries we use
- Filesystem Watcher - chokidar
- i18n - i18next
> [!IMPORTANT]
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
### Dependencies
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
## Development
Bruno is a desktop app. Below are the instructions to run Bruno.
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
> Note: We use React for the frontend and rsbuild for build and dev server.
## Install Dependencies
### Local Development
```bash
# use nodejs 22 version
# use nodejs 20 version
nvm use
# install deps
npm i --legacy-peer-deps
```
### Local Development
#### Build packages
##### Option 1
```bash
# build packages
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
# install dependencies and setup
npm run setup
```
#### Run the app
##### Option 1
```bash
# run react app (terminal 1)
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
##### Option 2
```bash
# run electron and react app concurrently
npm run dev
```
#### Customize Electron `userData` path
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
e.g.
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
### Troubleshooting
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
@@ -125,28 +87,7 @@ find . -type f -name "package-lock.json" -delete
```bash
# run bruno-schema tests
npm run test --workspace=packages/bruno-schema
# run bruno-query tests
npm run test --workspace=packages/bruno-query
# run bruno-common tests
npm run test --workspace=packages/bruno-common
# run bruno-converters tests
npm run test --workspace=packages/bruno-converters
# run bruno-app tests
npm run test --workspace=packages/bruno-app
# run bruno-electron tests
npm run test --workspace=packages/bruno-electron
# run bruno-lang tests
npm run test --workspace=packages/bruno-lang
# run bruno-toml tests
npm run test --workspace=packages/bruno-toml
npm test --workspace=packages/bruno-schema
# run tests over all workspaces
npm test --workspaces --if-present

View File

@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
### Abhängigkeiten
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
### Abhängigkeiten
- NodeJS v22
- NodeJS v18
### Lokales Entwickeln
```bash
# use nodejs 22 version
# use nodejs 18 version
nvm use
# install deps

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# run next app (terminal 1)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# next 앱 실행 (1번 터미널)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# draai next app (terminal 1)
npm run dev:web

View File

@@ -42,8 +42,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# spustite ďalšiu aplikáciu (terminál 1)
npm run dev:web

View File

@@ -1,5 +0,0 @@
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();
});

View File

@@ -1,31 +0,0 @@
import { test, expect } from '../../playwright';
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Name').press('Tab');
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
await page.getByLabel('Safe ModeBETA').check();
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.getByPlaceholder('Request URL').click();
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
await page.getByRole('button', { name: 'Create' }).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();
await expect(page.getByRole('main')).toContainText('200 OK');
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByText('GETr1').click();
await page.getByRole('button', { name: 'Clear response' }).click();
await page.locator('body').press('ControlOrMeta+Enter');
await expect(page.getByRole('main')).toContainText('200 OK');
});

View File

@@ -1,4 +0,0 @@
{
"maximized": true,
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
}

View File

@@ -1,49 +0,0 @@
import { test, expect } from '../../playwright';
test.describe.parallel('Run Testbench Requests', () => {
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Developer Mode(use only if').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
});
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Safe ModeBETA').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
});
});

View File

@@ -1,27 +0,0 @@
import { test, expect } from '../../playwright';
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
// Open Preferences
await page.getByLabel('Open Preferences').click();
// Verify Support tab
await page.getByRole('tab', { name: 'Support' }).click();
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
});

View File

@@ -1,41 +0,0 @@
// eslint.config.js
const { defineConfig } = require("eslint/config");
const globals = require("globals");
module.exports = defineConfig([
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.browser,
...globals.jest,
global: false,
require: false,
Buffer: false,
process: false
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
"no-undef": "error",
},
},
{
files: ["packages/bruno-electron/**/*.{js}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
},
rules: {
"no-undef": "error",
},
}
]);

5483
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,31 +6,24 @@
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-tests",
"packages/bruno-toml",
"packages/bruno-graphql-docs",
"packages/bruno-requests"
"packages/bruno-graphql-docs"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
"@playwright/test": "^1.27.1",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"concurrently": "^8.2.2",
"eslint": "^9.26.0",
"fs-extra": "^11.1.1",
"globals": "^16.1.0",
"husky": "^8.0.3",
"jest": "^29.2.0",
"lodash-es": "^4.17.21",
"playwright": "^1.51.1",
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
@@ -38,17 +31,13 @@
},
"scripts": {
"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\"",
"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",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"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-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",
"build:electron": "node ./scripts/build-electron.js",
@@ -58,18 +47,12 @@
"build:electron:deb": "./scripts/build-electron.sh deb",
"build:electron:rpm": "./scripts/build-electron.sh rpm",
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
"prepare": "husky install"
},
"overrides": {
"rollup": "3.29.5",
"electron-store": {
"conf": {
"json-schema-typed": "8.0.1"
}
}
"rollup": "3.29.5"
}
}
}

View File

@@ -1,9 +0,0 @@
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {
runtime: 'automatic'
}]
],
plugins: ['babel-plugin-styled-components']
};

View File

@@ -12,14 +12,5 @@ module.exports = {
},
clearMocks: true,
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/(?!(nanoid|xml-formatter)/)'
],
testMatch: [
'<rootDir>/src/**/*.spec.[jt]s?(x)'
]
testEnvironment: 'node'
};

View File

@@ -1,7 +1,6 @@
{
"name": "@usebruno/app",
"version": "2.0.0",
"license": "MIT",
"version": "1.39.0",
"private": true,
"scripts": {
"dev": "rsbuild dev",
@@ -12,6 +11,7 @@
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@babel/preset-env": "^7.26.0",
"@fontsource/inter": "^5.0.15",
"@prantlf/jsonlint": "^16.0.0",
"@reduxjs/toolkit": "^1.8.0",
@@ -82,28 +82,19 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "^1.2.0",
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"autoprefixer": "10.4.20",
"babel-jest": "^29.7.0",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"babel-plugin-styled-components": "^2.1.4",
"cross-env": "^7.0.3",
"css-loader": "7.1.2",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"jest-environment-jsdom": "^29.7.0",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "8.4.47",
"style-loader": "^3.3.1",
@@ -111,4 +102,4 @@
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
}
}
}

View File

@@ -19,7 +19,7 @@ export default defineConfig({
})
],
source: {
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
},
html: {
title: 'Bruno'
@@ -34,16 +34,6 @@ export default defineConfig({
},
},
},
ignoreWarnings: [
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
],
// Add externals configuration to exclude Node.js libraries
externals: {
// List specific Node.js modules you want to exclude
// Format: 'module-name': 'commonjs module-name'
'worker_threads': 'commonjs worker_threads',
// 'path': 'commonjs path'
}
},
}
});

View File

@@ -102,13 +102,6 @@ const StyledWrapper = styled.div`
.cm-s-default span.cm-variable {
color: #397d13 !important;
}
//matching bracket fix
.CodeMirror-matchingbracket {
background: #5cc0b48c !important;
text-decoration:unset;
}
`;
export default StyledWrapper;

View File

@@ -35,7 +35,6 @@ if (!SERVER_RENDERED) {
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
'res.setBody(data)',
'res.getResponseTime()',
'req',
'req.url',
@@ -58,7 +57,6 @@ if (!SERVER_RENDERED) {
'req.getTimeout()',
'req.setTimeout(timeout)',
'req.getExecutionMode()',
'req.getName()',
'bru',
'bru.cwd()',
'bru.getEnvName()',
@@ -81,14 +79,12 @@ if (!SERVER_RENDERED) {
'bru.getAssertionResults()',
'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getCollectionName()',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
'bru.runner.stopExecution()',
'bru.interpolate(str)'
'bru.runner.stopExecution()'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
@@ -366,7 +362,7 @@ export default class CodeEditor extends React.Component {
let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
defineCodeMirrorBrunoVariablesMode(variables, mode);
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
onModeChange('oauth2');
}}
>
OAuth 2.0
Oauth2
</div>
<div
className="dropdown-item"

View File

@@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: region,
profileName: awsv4Auth.profileName
}
})
);
@@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: profileName
}
})
);

View File

@@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => {
mode: 'basic',
collectionUid: collection.uid,
content: {
username: username || '',
password: basicAuth.password || ''
username: username,
password: basicAuth.password
}
})
);
@@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => {
mode: 'basic',
collectionUid: collection.uid,
content: {
username: basicAuth.username || '',
password: password || ''
username: basicAuth.username,
password: password
}
})
);

View File

@@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => {
mode: 'digest',
collectionUid: collection.uid,
content: {
username: username || '',
password: digestAuth.password || ''
username: username,
password: digestAuth.password
}
})
);
@@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => {
mode: 'digest',
collectionUid: collection.uid,
content: {
username: digestAuth.username || '',
password: password || ''
username: digestAuth.username,
password: password
}
})
);

View File

@@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: username || '',
password: ntlmAuth.password || '',
domain: ntlmAuth.domain || ''
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
@@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username || '',
password: password || '',
domain: ntlmAuth.domain || ''
username: ntlmAuth.username,
password: password,
domain: ntlmAuth.domain
}
})
);
@@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username || '',
password: ntlmAuth.password || '',
domain: domain || ''
username: ntlmAuth.username,
password: ntlmAuth.password,
domain: domain
}
})
);

View File

@@ -11,12 +11,6 @@ const Wrapper = styled.div`
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
.inherit-mode-text {
color: ${(props) => props.theme.colors.text.yellow};
}
.auth-mode-label {
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default Wrapper;
export default Wrapper;

View File

@@ -0,0 +1,120 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = get(collection, 'root.request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendCollectionOauth2Request(collection.uid));
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
state,
pkce,
[key]: value
}
})
);
};
const handlePKCEToggle = (e) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
state,
pkce: !Boolean(oAuth?.['pkce'])
}
})
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>
);
})}
<div className="flex flex-row w-full gap-4" key="pkce">
<label className="block font-medium">Use PKCE</label>
<input
className="cursor-pointer"
type="checkbox"
checked={Boolean(oAuth?.['pkce'])}
onChange={handlePKCEToggle}
/>
</div>
<div className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@@ -0,0 +1,33 @@
const inputsConfig = [
{
key: 'callbackUrl',
label: 'Callback URL'
},
{
key: 'authorizationUrl',
label: 'Authorization URL'
},
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',
label: 'Scope'
},
{
key: 'state',
label: 'State'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-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};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
const OAuth2ClientCredentials = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = get(collection, 'root.request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendCollectionOauth2Request(collection.uid));
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'client_credentials',
accessTokenUrl,
clientId,
clientSecret,
scope,
[key]: value
}
})
);
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2ClientCredentials;

View File

@@ -0,0 +1,21 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,54 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
.grant-type-mode-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
.dropdown {
width: fit-content;
div[data-tippy-root] {
width: fit-content;
}
.tippy-box {
width: fit-content;
max-width: none !important;
.tippy-content: {
width: fit-content;
max-width: none !important;
}
}
}
.grant-type-label {
width: fit-content;
color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
.label-item {
padding: 0.2rem 0.6rem !important;
}
}
.caret {
color: rgb(140, 140, 140);
fill: rgb(140 140 140);
}
label {
font-size: 0.8125rem;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,98 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { IconCaretDown } from '@tabler/icons';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { humanizeGrantType } from 'utils/collections';
import { useEffect } from 'react';
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
const GrantTypeSelector = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = get(collection, 'root.request.auth.oauth2', {});
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const onGrantTypeChange = (grantType) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType
}
})
);
};
useEffect(() => {
// initialize redux state with a default oauth2 grant type
// authorization_code - default option
!oAuth?.grantType &&
dispatch(
updateCollectionAuthMode({
mode: 'oauth2',
collectionUid: collection.uid
})
);
!oAuth?.grantType &&
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code'
}
})
);
}, [oAuth]);
return (
<StyledWrapper>
<label className="block font-medium mb-2">Grant Type</label>
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('password');
}}
>
Password Credentials
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('authorization_code');
}}
>
Authorization Code
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('client_credentials');
}}
>
Client Credentials
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default GrantTypeSelector;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-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};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,72 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = get(collection, 'root.request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendCollectionOauth2Request(collection.uid));
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'password',
accessTokenUrl,
username,
password,
clientId,
clientSecret,
scope,
[key]: value
}
})
);
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@@ -0,0 +1,29 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'username',
label: 'Username'
},
{
key: 'password',
label: 'Password'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -1,33 +1,21 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
const GrantTypeComponentMap = ({collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveCollectionRoot(collection.uid));
};
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
const grantTypeComponentMap = (grantType, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
return <OAuth2PasswordCredentials collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
return <OAuth2AuthorizationCode collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
return <OAuth2ClientCredentials collection={collection} />;
break;
default:
return <div>TBD</div>;
@@ -36,12 +24,12 @@ const GrantTypeComponentMap = ({collection }) => {
};
const OAuth2 = ({ collection }) => {
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
const oAuth = get(collection, 'root.request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
<GrantTypeComponentMap collection={collection} />
<GrantTypeSelector collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, collection)}
</StyledWrapper>
);
};

View File

@@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => {
mode: 'wsse',
collectionUid: collection.uid,
content: {
username: username || '',
password: wsseAuth.password || ''
username,
password: wsseAuth.password
}
})
);
@@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => {
mode: 'wsse',
collectionUid: collection.uid,
content: {
username: wsseAuth.username || '',
password: password || ''
username: wsseAuth.username,
password
}
})
);

View File

@@ -25,10 +25,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
passphrase: ''
},
validationSchema: Yup.object({
domain: Yup.string()
.required()
.trim()
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
domain: Yup.string().required(),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
@@ -48,7 +45,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
let relevantValues = {};
if (values.type === 'cert') {
relevantValues = {
domain: values.domain?.trim(),
domain: values.domain,
type: values.type,
certFilePath: values.certFilePath,
keyFilePath: values.keyFilePath,
@@ -56,7 +53,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
};
} else {
relevantValues = {
domain: values.domain?.trim(),
domain: values.domain,
type: values.type,
pfxFilePath: values.pfxFilePath,
passphrase: values.passphrase
@@ -130,20 +127,15 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<label className="settings-label" htmlFor="domain">
Domain
</label>
<div className="relative flex items-center">
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
https://
</div>
<input
id="domain"
type="text"
name="domain"
placeholder="example.org"
className="block textbox non-passphrase-input !pl-[60px]"
onChange={formik.handleChange}
value={formik.values.domain || ''}
/>
</div>
<input
id="domain"
type="text"
name="domain"
placeholder="*.example.org"
className="block textbox non-passphrase-input"
onChange={formik.handleChange}
value={formik.values.domain || ''}
/>
{formik.touched.domain && formik.errors.domain ? (
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
) : null}

View File

@@ -119,6 +119,6 @@ This documentation supports Markdown formatting! You can use:
- **Bold** and *italic* text
- \`code blocks\` and syntax highlighting
- Tables and lists
- [Links](https://usebruno.com)
- [Links](https://example.com)
- And more!
`;

View File

@@ -72,7 +72,7 @@ const Info = ({ collection }) => {
</div>
</div>
</div>
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
</div>
</div>
</div>

View File

@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const auth = get(collection, 'root.request.auth', {}).mode;
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
{authMode !== 'none' && <ContentIndicator />}
{auth !== 'none' && <ContentIndicator />}
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
Script

View File

@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
@@ -13,18 +13,11 @@ import { variableNameRegex } from 'utils/common/regex';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { Tooltip } from 'react-tooltip';
import { getGlobalEnvironmentVariables } from 'utils/collections';
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
let _collection = cloneDeep(collection);
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
const formik = useFormik({
enableReinitialize: true,
@@ -167,7 +160,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
theme={storedTheme}
collection={_collection}
collection={collection}
name={`${index}.value`}
value={variable.value}
isSecret={variable.secret}

View File

@@ -28,10 +28,7 @@ const ImportEnvironment = ({ collection, onClose }) => {
.then(() => {
toast.success('Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.then(() => {

View File

@@ -1,226 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
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 GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
import AuthMode from '../AuthMode';
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
let request = get(folder, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
case 'authorization_code':
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} />;
default:
return <div>TBD</div>;
}
};
const Auth = ({ collection, folder }) => {
const dispatch = useDispatch();
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const getTreePathFromCollectionToFolder = (collection, _folder) => {
let path = [];
let item = findItemInCollection(collection, _folder?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const folderAuth = get(parentFolder, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: parentFolder.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
const getAuthView = () => {
switch (authMode) {
case 'basic': {
return (
<BasicAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'bearer': {
return (
<BearerAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'digest': {
return (
<DigestAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'ntlm': {
return (
<NTLMAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'wsse': {
return (
<WsseAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'apikey': {
return (
<ApiKeyAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'awsv4': {
return (
<AwsV4Auth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'oauth2': {
return (
<>
<GrantTypeSelector
request={request}
updateAuth={updateFolderAuth}
collection={collection}
item={folder}
/>
<GrantTypeComponentMap collection={collection} folder={folder} />
</>
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
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>
</>
);
}
case 'none': {
return null;
}
default:
return null;
}
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">
Configures authentication for the entire folder. This applies to all requests using the{' '}
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
</div>
<div className="flex flex-grow justify-start items-center mb-4">
<AuthMode collection={collection} folder={folder} />
</div>
{getAuthView()}
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Auth;

View File

@@ -1,16 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.auth-mode-selector {
border: 1px solid ${({ theme }) => theme.colors.border};
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8125rem;
}
.auth-mode-label {
color: ${({ theme }) => theme.colors.text};
}
`;
export default StyledWrapper;

View File

@@ -1,134 +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 { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
import { humanizeRequestAuthMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const AuthMode = ({ collection, folder }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(folder, 'root.request.auth.mode');
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(
updateFolderAuthMode({
mode: value,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('awsv4');
}}
>
AWS Sig v4
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('basic');
}}
>
Basic Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('bearer');
}}
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('ntlm');
}}
>
NTLM Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('wsse');
}}
>
WSSE Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('apikey');
}}
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('inherit');
}}
>
Inherit
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Auth
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default AuthMode;

View File

@@ -8,9 +8,7 @@ import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
import Documentation from './Documentation';
import Auth from './Auth';
import DotIcon from 'components/Icons/Dot';
import get from 'lodash/get';
const ContentIndicator = () => {
return (
@@ -28,7 +26,7 @@ const FolderSettings = ({ collection, folder }) => {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const folderRoot = folder?.root;
const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
const hasTests = folderRoot?.request?.tests;
@@ -39,9 +37,6 @@ const FolderSettings = ({ collection, folder }) => {
const responseVars = folderRoot?.request?.vars?.res || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const auth = get(folderRoot, 'request.auth.mode');
const hasAuth = auth && auth !== 'none';
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
@@ -66,9 +61,6 @@ const FolderSettings = ({ collection, folder }) => {
case 'vars': {
return <Vars collection={collection} folder={folder} />;
}
case 'auth': {
return <Auth collection={collection} folder={folder} />;
}
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
@@ -101,10 +93,6 @@ const FolderSettings = ({ collection, folder }) => {
Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
{hasAuth && <ContentIndicator />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>

View File

@@ -34,10 +34,7 @@ const ImportEnvironment = ({ onClose }) => {
.then(() => {
toast.success('Global Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.then(() => {

View File

@@ -8,7 +8,7 @@ const DotIcon = ({ width }) => {
className='inline-block'
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
</svg>
);
};

View File

@@ -130,7 +130,7 @@ class MultiLineEditor extends Component {
addOverlay = (variables) => {
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -125,7 +125,7 @@ const General = ({ close }) => {
className="mousetrap mr-0"
/>
<label className="block ml-2 select-none" htmlFor="customCaCertificateEnabled">
Use Custom CA Certificate
Use custom CA Certificate
</label>
</div>
{formik.values.customCaCertificate.filePath ? (
@@ -183,7 +183,7 @@ const General = ({ close }) => {
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
htmlFor="keepDefaultCaCertificatesEnabled"
>
Keep Default CA Certificates
Keep default CA Certificates
</label>
</div>
<div className="flex items-center mt-2">

View File

@@ -5,23 +5,21 @@ import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useTheme } from 'providers/Theme';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
const ApiKeyAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const apikeyAuth = get(request, 'auth.apikey', {});
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const Icon = forwardRef((props, ref) => {
return (
@@ -92,7 +90,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
dropdownTippyRef.current.hide();
handleAuthChange('placement', 'header');
}}
>
@@ -101,11 +99,11 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
dropdownTippyRef.current.hide();
handleAuthChange('placement', 'queryparams');
}}
>
Query Param
Query Params
</div>
</Dropdown>
</div>

View File

@@ -8,17 +8,14 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import { update } from 'lodash';
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const awsv4Auth = get(request, 'auth.awsv4', {});
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleAccessKeyIdChange = (accessKeyId) => {
dispatch(

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
const BasicAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = get(request, 'auth.basic', {});
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -7,18 +7,16 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
const BearerAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
// Use the request prop directly like OAuth2ClientCredentials does
const bearerToken = get(request, 'auth.bearer.token', '');
const bearerToken = item.draft
? get(item, 'draft.request.auth.bearer.token', '')
: get(item, 'request.auth.bearer.token', '');
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleTokenChange = (token) => {
dispatch(

View File

@@ -3,20 +3,18 @@ import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
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';
const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
const DigestAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = get(request, 'auth.digest', {});
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
const NTLMAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = get(request, 'auth.ntlm', {});
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -29,6 +26,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
);

View File

@@ -11,47 +11,6 @@ const Wrapper = styled.div`
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;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,63 +1,28 @@
import React, { useRef, forwardRef } from 'react';
import React 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 { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = get(request, 'auth.oauth2', {});
const {
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsPlacement,
state,
pkce,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
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 = () => dispatch(saveRequest(item.uid, collection.uid));
const CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const handleSave = () => { save(); };
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
@@ -75,15 +40,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
state,
scope,
pkce,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value,
[key]: value
}
})
);
@@ -104,35 +61,30 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
clientSecret,
state,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
autoFetchToken,
pkce: !Boolean(oAuth?.['pkce'])
}
})
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} 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="single-line-editor-wrapper flex-1">
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -147,33 +99,8 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
</div>
);
})}
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</div>
</div>
<div className="flex flex-row w-full gap-4" key="pkce">
<label className="block">Use PKCE</label>
<label className="block font-medium">Use PKCE</label>
<input
className="cursor-pointer"
type="checkbox"
@@ -181,154 +108,16 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
onChange={handlePKCEToggle}
/>
</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 className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</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="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
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');
}}
>
Header
</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-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-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 text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-2.5 mt-4">
<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">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</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 you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</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 refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;
export default OAuth2AuthorizationCode;

View File

@@ -11,47 +11,6 @@ const Wrapper = styled.div`
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;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,61 +1,26 @@
import React, { useRef, forwardRef } from 'react';
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const OAuth2ClientCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = get(request, 'auth.oauth2', {});
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const {
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleSave = () => { save(); };
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 CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
@@ -69,14 +34,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
@@ -85,21 +42,12 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} 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="single-line-editor-wrapper flex-1">
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -114,179 +62,9 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
</div>
);
})}
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</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="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
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 w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'header');
}}
>
Header
</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-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-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 text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-2.5 mt-4">
<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">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</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 you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</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 refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};

View File

@@ -3,20 +3,18 @@ import get from 'lodash/get';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { IconCaretDown, IconKey } from '@tabler/icons';
import { IconCaretDown } from '@tabler/icons';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { humanizeGrantType } from 'utils/collections';
import { useEffect } from 'react';
import { useState } from 'react';
const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
const GrantTypeSelector = ({ item, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const oAuth = get(request, 'auth.oauth2', {});
const [valuesCache, setValuesCache] = useState({
...oAuth
});
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
@@ -26,19 +24,13 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
});
const onGrantTypeChange = (grantType) => {
let updatedValues = {
...valuesCache,
...oAuth,
grantType
};
setValuesCache(updatedValues);
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
...updatedValues
grantType
}
})
);
@@ -54,18 +46,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code',
accessTokenUrl: '',
username: '',
password: '',
clientId: '',
clientSecret: '',
scope: '',
credentialsPlacement: 'body',
credentialsId: 'credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: 'Bearer',
tokenQueryKey: 'access_token',
grantType: 'authorization_code'
}
})
);
@@ -73,14 +54,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
return (
<StyledWrapper>
<div className="flex items-center gap-2.5 my-4">
<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">
Grant Type
</span>
</div>
<label className="block font-medium mb-2">Grant Type</label>
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
@@ -115,4 +89,4 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
</StyledWrapper>
);
};
export default GrantTypeSelector;
export default GrantTypeSelector;

View File

@@ -1,95 +0,0 @@
import { useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import toast from 'react-hot-toast';
import { cloneDeep, find } from 'lodash';
import { IconLoader2 } from '@tabler/icons';
import { interpolate } from '@usebruno/common';
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
import { getAllVariables } from "utils/collections/index";
const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {
const { uid: collectionUid } = collection;
const dispatch = useDispatch();
const [fetchingToken, toggleFetchingToken] = useState(false);
const [refreshingToken, toggleRefreshingToken] = useState(false);
const interpolatedAccessTokenUrl = useMemo(() => {
const variables = getAllVariables(collection, item);
return interpolate(accessTokenUrl, variables);
}, [collection, item, accessTokenUrl]);
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
const creds = credentialsData?.credentials || {};
const handleFetchOauth2Credentials = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleFetchingToken(true);
try {
const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleFetchingToken(false);
if (credentials?.access_token) {
toast.success('token fetched successfully!');
}
else {
toast.error('An error occurred while fetching token!');
}
}
catch (error) {
console.error('could not fetch the token!');
console.error(error);
toggleFetchingToken(false);
toast.error('An error occurred while fetching token!');
}
}
const handleRefreshAccessToken = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleRefreshingToken(true);
try {
const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleRefreshingToken(false);
if (credentials?.access_token) {
toast.success('token refreshed successfully!');
}
else {
toast.error('An error occurred while refreshing token!');
}
}
catch(error) {
console.error(error);
toggleRefreshingToken(false);
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');
})
.catch((err) => {
toast.error(err.message);
});
};
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`}>
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
</button>
{creds?.refresh_token ? <button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
</button> : null}
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
)
}
export default Oauth2ActionButtons;

View File

@@ -1,12 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
ol[role="tree"] {
overflow: hidden;
}
ol[role="group"] span {
line-break: anywhere;
}
`;
export default Wrapper;

View File

@@ -1,172 +0,0 @@
import { useState, useEffect, useMemo } from "react";
import { find } from "lodash";
import StyledWrapper from "./StyledWrapper";
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
import { getAllVariables } from 'utils/collections/index';
import { interpolate } from '@usebruno/common';
const TokenSection = ({ title, token }) => {
if (!token) return null;
const [isExpanded, setIsExpanded] = useState(false);
const [decodedToken, setDecodedToken] = useState(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (token) {
try {
const parts = token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(atob(parts[1]));
setDecodedToken(payload);
}
} catch (err) {
console.error('Error decoding token:', err);
}
}
}, [token]);
const handleCopy = async (text) => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="mb-2 border dark:border-gray-700 rounded-lg overflow-hidden">
<div
className="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center space-x-2 w-full">
{isExpanded ?
<IconChevronDown size={18} className="text-gray-500" /> :
<IconChevronRight size={18} className="text-gray-500" />
}
<div className="flex flex-row justify-between w-full">
<h3 className="text-sm font-medium">{title}</h3>
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
</div>
</div>
</div>
{isExpanded && (
<div className="p-3 text-sm">
<div className="relative group">
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleCopy(token)}
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
title="Copy token"
>
{copied ?
<IconCheck size={16} className="text-green-700" /> :
<IconCopy size={16} className="text-gray-500" />
}
</button>
</div>
<div className="font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all">
{token}
</div>
</div>
{decodedToken && (
<div className="mt-3">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Decoded Payload</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
{Object.entries(decodedToken).map(([key, value]) => (
<div key={key} className="overflow-hidden text-ellipsis">
<span className="font-medium text-xs">{key}: </span>
<span className="text-xs text-gray-600 dark:text-gray-300">
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};
const formatExpiryTime = (seconds) => {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
};
const ExpiryTimer = ({ expiresIn }) => {
if (!expiresIn) return null;
const calculateTimeLeft = () => Math.max(0, Math.floor(expiresIn - Date.now() / 1000));
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);
useEffect(() => {
setTimeLeft(calculateTimeLeft());
const timer = setInterval(() => {
setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => clearInterval(timer);
}, [expiresIn]);
return (
<div
className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30
? "bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400"
: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
}`}
>
{timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}
</div>
);
};
const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {
const { uid: collectionUid } = collection;
const interpolatedUrl = useMemo(() => {
const variables = getAllVariables(collection, item);
return interpolate(url, variables);
}, [collection, item, url]);
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
const creds = credentialsData?.credentials || {};
return (
<StyledWrapper className="relative w-auto h-fit mt-2">
{Object.keys(creds)?.length ? (
creds?.error ? (
<pre className="text-red-600 dark:text-red-400">Error fetching token. Check network logs for more details.</pre>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm">
<TokenSection title="Access Token" token={creds.access_token} />
<TokenSection title="Refresh Token" token={creds.refresh_token} />
<TokenSection title="ID Token" token={creds.id_token} />
{(creds.token_type || creds.scope) ? <div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
<div className="grid grid-cols-2 gap-2">
{creds.token_type ? <div className="flex items-center space-x-1">
<span className="font-medium">Token Type:</span>
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
</div> : null}
{creds?.scope ? <div className="flex items-center space-x-1 min-w-0">
<span className="font-medium flex-shrink-0">Scope:</span>
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
{creds.scope}
</span>
</div> : null}
</div>
</div> : null}
</div>
)
) : (
<div className="text-sm text-gray-500 dark:text-gray-400">No token found</div>
)}
</StyledWrapper>
);
};
export default Oauth2TokenViewer;

View File

@@ -11,47 +11,6 @@ const Wrapper = styled.div`
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;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,62 +1,26 @@
import React, { useRef, forwardRef } from 'react';
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = get(request, 'auth.oauth2', {});
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const {
accessTokenUrl,
username,
password,
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleSave = () => { save(); }
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 CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
@@ -72,14 +36,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
@@ -88,21 +44,12 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} 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="single-line-editor-wrapper flex-1">
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -117,179 +64,11 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
</div>
);
})}
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</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="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
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');
}}
>
Header
</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-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-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 text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-2.5 mt-4">
<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">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</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 you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</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 refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2PasswordCredentials;
export default OAuth2AuthorizationCode;

View File

@@ -5,34 +5,17 @@ import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
const GrantTypeComponentMap = ({ item, collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveRequest(item.uid, collection.uid));
};
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
return <OAuth2PasswordCredentials item={item} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
return <OAuth2AuthorizationCode item={item} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
return <OAuth2ClientCredentials item={item} collection={collection} />;
break;
default:
return <div>TBD</div>;
@@ -41,12 +24,12 @@ const GrantTypeComponentMap = ({ item, collection }) => {
};
const OAuth2 = ({ item, collection }) => {
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
<GrantTypeComponentMap item={item} collection={collection} />
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
</StyledWrapper>
);
};

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
const WsseAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const wsseAuth = get(request, 'auth.wsse', {});
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUserChange = (username) => {
dispatch(
@@ -58,7 +55,6 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
onChange={(val) => handleUserChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -71,8 +67,6 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -7,109 +7,71 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import NTLMAuth from './NTLMAuth';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import OAuth2 from './OAuth2/index';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Create a request object to pass to the auth components
const request = item.draft
? get(item, 'draft.request', {})
: get(item, 'request', {});
// Save function for request level
const save = () => {
return saveRequest(item.uid, collection.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 collectionRoot = get(collection, 'root', {});
const collectionAuth = get(collectionRoot, 'request.auth');
const getAuthView = () => {
switch (authMode) {
case 'awsv4': {
return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <AwsV4Auth collection={collection} item={item} />;
}
case 'basic': {
return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <BasicAuth collection={collection} item={item} />;
}
case 'bearer': {
return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <BearerAuth collection={collection} item={item} />;
}
case 'digest': {
return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <DigestAuth collection={collection} item={item} />;
}
case 'ntlm': {
return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <NTLMAuth collection={collection} item={item} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <OAuth2 collection={collection} item={item} />;
}
case 'wsse': {
return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <WsseAuth collection={collection} item={item} />;
}
case 'apikey': {
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <ApiKeyAuth collection={collection} item={item} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
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>
</>
<div className="flex flex-row w-full mt-2 gap-2">
{collectionAuth?.mode === 'oauth2' ? (
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-1">
<div>Collection level auth is: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</div>
<div className="text-sm opacity-50">
Note: You need to use scripting to set the access token in the request headers.
</div>
</div>
) : (
<>
<div>Auth inherited from the Collection: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</>
)}
</div>
);
}
}
};
return (
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
<StyledWrapper className="w-full mt-1">
<div className="flex flex-grow justify-start items-center">
<AuthMode item={item} collection={collection} />
</div>

View File

@@ -7,10 +7,8 @@ import Dropdown from '../../Dropdown';
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');
const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };
const request = item.draft ? item.draft.request : item.request;
let {
schema,

View File

@@ -64,10 +64,9 @@ const GraphQLVariables = ({ variables, item, collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="application/json"
mode="javascript"
onRun={onRun}
onSave={onSave}
enableVariableHighlighting={true}
/>
</>
);

View File

@@ -152,7 +152,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && (
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
<ErrorIndicator /> :
<ContentIndicator />
)}

View File

@@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
</div>
</div>
{generateCodeItemModalOpen && (
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
</StyledWrapper>
);

View File

@@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
item={item}
item={item}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
@@ -58,14 +58,13 @@ const RequestBody = ({ item, collection }) => {
onRun={onRun}
onSave={onSave}
mode={codeMirrorMode[bodyMode]}
enableVariableHighlighting={true}
/>
</StyledWrapper>
);
}
if (bodyMode === 'file') {
return <FileBody item={item} collection={collection} />;
return <FileBody item={item} collection={collection}/>
}
if (bodyMode === 'formUrlEncoded') {
@@ -78,4 +77,4 @@ const RequestBody = ({ item, collection }) => {
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;
export default RequestBody;

View File

@@ -1,42 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const FolderNotFound = ({ folderUid }) => {
const dispatch = useDispatch();
const [showErrorMessage, setShowErrorMessage] = useState(false);
const closeTab = useCallback(() => {
dispatch(
closeTabs({
tabUids: [folderUid]
})
);
}, [dispatch, folderUid]);
useEffect(() => {
setTimeout(() => {
setShowErrorMessage(true);
}, 300);
}, []);
if (!showErrorMessage) {
return null;
}
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
<div>Folder no longer exists.</div>
<div className="mt-2">
This can happen when the folder was renamed or deleted on your filesystem.
</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab
</button>
</div>
);
};
export default FolderNotFound;

View File

@@ -25,7 +25,6 @@ import { produce } from 'immer';
import CollectionOverview from 'components/CollectionSettings/Overview';
import RequestNotLoaded from './RequestNotLoaded';
import RequestIsLoading from './RequestIsLoading';
import FolderNotFound from './FolderNotFound';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
@@ -164,10 +163,6 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'folder-settings') {
const folder = findItemInCollection(collection, focusedTab.folderUid);
if (!folder) {
return <FolderNotFound folderUid={focusedTab.folderUid} />;
}
return <FolderSettings collection={collection} folder={folder} />;
}

View File

@@ -76,9 +76,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
onMouseUp={handleMouseUp} // Add middle-click behavior here
>
{tab.type === 'folder-settings' && !folder ? (
<RequestTabNotFound handleCloseClick={handleCloseClick} />
) : tab.type === 'folder-settings' ? (
{tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
@@ -263,13 +261,13 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collectionUid={collection.uid} onClose={() => setShowAddNewRequestModal(false)} />
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collectionUid={collection.uid}
collection={collection}
onClose={() => setShowCloneRequestModal(false)}
/>
)}

View File

@@ -79,7 +79,7 @@ const RequestTabs = () => {
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && (
<NewRequest collectionUid={activeCollection?.uid} onClose={() => setNewRequestModalOpen(false)} />
<NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />
)}
{collectionRequestTabs && collectionRequestTabs.length ? (
<>

View File

@@ -1,8 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
font-size: 0.8125rem;
color: ${(props) => props.theme.requestTabPanel.responseStatus};
`;
export default StyledWrapper;

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { clearRequestTimeline } from 'providers/ReduxStore/slices/collections/index';
const ClearTimeline = ({ collection, item }) => {
const dispatch = useDispatch();
const clearResponse = () =>
dispatch(
clearRequestTimeline({
itemUid: item.uid,
collectionUid: collection.uid
})
);
return (
<StyledWrapper className="ml-2 flex items-center">
<button onClick={clearResponse} className='text-link hover:underline' title="Clear Timeline">
Clear Timeline
</button>
</StyledWrapper>
);
};
export default ClearTimeline;

View File

@@ -17,7 +17,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
<StopWatch startTime={item?.requestStartTime} />
<StopWatch requestTimestamp={item?.requestSent?.timestamp} />
</div>
</div>
<IconRefresh size={24} className="loading-icon" />

View File

@@ -13,7 +13,7 @@ import { useTheme } from 'providers/Theme/index';
import { getEncoding, uuid } from 'utils/common/index';
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
if (data === undefined || !dataBuffer || !mode) {
if (data === undefined || !dataBuffer) {
return '';
}
@@ -65,7 +65,7 @@ const formatErrorMessage = (error) => {
const remoteMethodError = "Error invoking remote method 'send-http-request':";
if (error?.includes(remoteMethodError)) {
if (error.includes(remoteMethodError)) {
const parts = error.split(remoteMethodError);
return parts[1]?.trim() || error;
}
@@ -92,9 +92,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
// Always show raw
const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
if (!mode || !contentType) return allowedPreviewModes;
if (mode?.includes('html') && typeof data === 'string') {
if (mode.includes('html') && typeof data === 'string') {
allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
} else if (mode.includes('image')) {
allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
@@ -142,7 +140,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
return (
<StyledWrapper
className="w-full h-full relative flex"
className="w-full h-full relative"
style={{ maxWidth: width }}
queryFilterEnabled={queryFilterEnabled}
>

View File

@@ -1,110 +0,0 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import ResponseSize from './index';
// Create minimal theme with only the properties needed for the component
const theme = {
requestTabPanel: {
responseStatus: '#666'
}
};
// Wrap component with theme provider for styled-components
const renderWithTheme = (component) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
describe('ResponseSize', () => {
describe('Invalid or excluded size values', () => {
it('should not render when size is undefined', () => {
const { container } = renderWithTheme(<ResponseSize size={undefined} />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is null', () => {
const { container } = renderWithTheme(<ResponseSize size={null} />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is NaN', () => {
const { container } = renderWithTheme(<ResponseSize size={NaN} />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is Infinity', () => {
const { container } = renderWithTheme(<ResponseSize size={Infinity} />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is -Infinity', () => {
const { container } = renderWithTheme(<ResponseSize size={-Infinity} />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is a string', () => {
const { container } = renderWithTheme(<ResponseSize size="1024" />);
expect(container).toBeEmptyDOMElement();
});
it('should not render when size is an object', () => {
const { container } = renderWithTheme(<ResponseSize size={{value: 1024}} />);
expect(container).toBeEmptyDOMElement();
});
});
describe('Valid size values', () => {
it('should handle zero bytes', () => {
renderWithTheme(<ResponseSize size={0} />);
const element = screen.getByText(/0B/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^0B$/);
expect(element).toHaveAttribute('title', '0B');
});
it('should render bytes when size is less than 1024', () => {
renderWithTheme(<ResponseSize size={500} />);
const element = screen.getByText(/500B/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^500B$/);
expect(element).toHaveAttribute('title', '500B');
});
it('should handle exactly 1024 bytes as size', () => {
renderWithTheme(<ResponseSize size={1024} />);
const element = screen.getByText(/1024B/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^1024B$/);
expect(element).toHaveAttribute('title', '1,024B');
});
it('should render kilobytes when size is greater than 1024', () => {
renderWithTheme(<ResponseSize size={1500} />);
const element = screen.getByText(/1\.46KB/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
expect(element).toHaveAttribute('title', '1,500B');
});
it('should handle large size numbers', () => {
renderWithTheme(<ResponseSize size={10240} />);
const element = screen.getByText(/10\.0KB/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
expect(element).toHaveAttribute('title', '10,240B');
});
it('should handle decimal size numbers', () => {
renderWithTheme(<ResponseSize size={1126.5} />);
const element = screen.getByText(/1\.10KB/);
expect(element).toBeInTheDocument();
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
expect(element).toHaveAttribute('title', '1,126.5B');
});
});
});

View File

@@ -2,20 +2,14 @@ import React from 'react';
import StyledWrapper from './StyledWrapper';
const ResponseSize = ({ size }) => {
if (!Number.isFinite(size)) {
return null;
}
let sizeToDisplay = '';
// If size is greater than 1024 bytes, format as KB
if (size > 1024) {
// size is greater than 1kb
let kb = Math.floor(size / 1024);
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
sizeToDisplay = kb + '.' + decimal + 'KB';
} else {
// If size is less than or equal to 1024 bytes, display as bytes (B)
sizeToDisplay = size + 'B';
}

View File

@@ -1,24 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.line {
white-space: pre-line;
word-wrap: break-word;
word-break: break-all;
font-family: Inter, sans-serif !important;
.arrow {
opacity: 0.5;
}
&.request {
color: ${(props) => props.theme.colors.text.green};
}
&.response {
color: ${(props) => props.theme.colors.text.purple};
}
}
`;
export default StyledWrapper;

View File

@@ -1,61 +0,0 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import { safeStringifyJSON } from 'utils/common';
import StyledWrapper from './StyledWrapper';
const RunnerTimeline = ({ request, response }) => {
const requestHeaders = [];
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
request = request || {};
response = response || {};
forOwn(request.headers, (value, key) => {
requestHeaders.push({
name: key,
value
});
});
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
return (
<StyledWrapper className="pb-4 w-full">
<div>
<pre className="line request font-bold">
<span className="arrow">{'>'}</span> {request.method} {request.url}
</pre>
{requestHeaders.map((h) => {
return (
<pre className="line request" key={h.name}>
<span className="arrow">{'>'}</span> {h.name}: {h.value}
</pre>
);
})}
{requestData ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
</pre>
) : null}
</div>
<div className="mt-4">
<pre className="line response font-bold">
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
</pre>
{responseHeaders.map((h) => {
return (
<pre className="line response" key={h[0]}>
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
</pre>
);
})}
</div>
</StyledWrapper>
);
};
export default RunnerTimeline;

View File

@@ -1,11 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
padding-top: 20%;
width: 100%;
.send-icon {
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
}
`;
export default StyledWrapper;

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { IconCircleOff } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const SkippedRequest = () => {
return (
<StyledWrapper>
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
<IconCircleOff size={150} strokeWidth={1} />
</div>
<div className="flex mt-4 justify-center" style={{ fontSize: 25 }}>
Request skipped
</div>
</StyledWrapper>
);
};
export default SkippedRequest;

View File

@@ -1,18 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: ${(props) => props.theme.text};
.test-summary {
transition: background-color 0.2s;
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
color: ${(props) => props.theme.text};
&:hover {
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
}
.test-success {
color: ${(props) => props.theme.colors.text.green};
}
@@ -21,25 +9,9 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.danger};
}
.test-success-count {
color: ${(props) => props.theme.colors.text.green};
}
.test-failure-count {
color: ${(props) => props.theme.colors.text.danger};
}
.error-message {
color: ${(props) => props.theme.colors.text.muted};
}
.test-results-list {
transition: all 0.3s ease;
}
.dropdown-icon {
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
}
`;
export default StyledWrapper;

Some files were not shown because too many files have changed in this diff Show More