mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
164 Commits
v0.7.1
...
feature/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fc32d035f | ||
|
|
78251c530c | ||
|
|
dea95664b9 | ||
|
|
fbc6e7bff5 | ||
|
|
4884106aaa | ||
|
|
5c15438949 | ||
|
|
b53a9eaee9 | ||
|
|
5899ca446d | ||
|
|
d21e7f6fb5 | ||
|
|
ee8a3eae8c | ||
|
|
fac5109242 | ||
|
|
47dfbd2a64 | ||
|
|
074d72d885 | ||
|
|
8c29d131e2 | ||
|
|
437044bdcd | ||
|
|
2120a562da | ||
|
|
04c3c2dbf1 | ||
|
|
1d03e1d5ea | ||
|
|
2b174e1c60 | ||
|
|
7a2b32069e | ||
|
|
a9e6c3a35c | ||
|
|
e6a754b933 | ||
|
|
ee4509f037 | ||
|
|
c04f0e7a71 | ||
|
|
2f52ce4c71 | ||
|
|
b1edaba1c6 | ||
|
|
3f6fcdd582 | ||
|
|
c745786b1c | ||
|
|
9e30c7b440 | ||
|
|
b87cc7ccae | ||
|
|
1595d736f2 | ||
|
|
b38c25ca70 | ||
|
|
f22858219b | ||
|
|
8044286b80 | ||
|
|
34a2e23dc6 | ||
|
|
224b8c3cc4 | ||
|
|
d58e92205b | ||
|
|
925af1f26f | ||
|
|
d07744d5c2 | ||
|
|
5efb18ad63 | ||
|
|
9cfb54ee9f | ||
|
|
4c9d22d1e0 | ||
|
|
c5d43cc9e6 | ||
|
|
8300830a95 | ||
|
|
2dfc972930 | ||
|
|
4fdfdaf2cb | ||
|
|
a1385ba1e2 | ||
|
|
15804ac293 | ||
|
|
0244b2e1d6 | ||
|
|
7e70d05dc8 | ||
|
|
e60b06e4a4 | ||
|
|
17ded5de4c | ||
|
|
e1b97643bd | ||
|
|
8103554545 | ||
|
|
a425b42615 | ||
|
|
b14f867811 | ||
|
|
013abeaa80 | ||
|
|
9d3762702f | ||
|
|
cac9f9aef4 | ||
|
|
2b63368f2c | ||
|
|
acd980ffc6 | ||
|
|
1a175e4449 | ||
|
|
209f30998e | ||
|
|
e777eed00d | ||
|
|
15fc24679c | ||
|
|
48d26c05d9 | ||
|
|
9d395ded33 | ||
|
|
943e74c327 | ||
|
|
b852d1cc52 | ||
|
|
3d22f77226 | ||
|
|
429ca4093c | ||
|
|
a4f757ee87 | ||
|
|
df4f322024 | ||
|
|
ddd39e630d | ||
|
|
ef8e8bf637 | ||
|
|
7405fa9709 | ||
|
|
242fcac2d3 | ||
|
|
efd15838aa | ||
|
|
c55f9d42da | ||
|
|
2f32f7024e | ||
|
|
aff6499478 | ||
|
|
45ed47ff90 | ||
|
|
27c6c1349a | ||
|
|
c78ffa3a80 | ||
|
|
6b2d335ade | ||
|
|
837e39d870 | ||
|
|
67643c4c48 | ||
|
|
2b384656b6 | ||
|
|
411c06f4cb | ||
|
|
03fa46d8b3 | ||
|
|
d0f2eb27bc | ||
|
|
1b9ec05a58 | ||
|
|
3f74178c81 | ||
|
|
78ca6c5e96 | ||
|
|
5f59a16090 | ||
|
|
3805cef0c4 | ||
|
|
3c1a6ca71e | ||
|
|
d2227b2b05 | ||
|
|
dc03b6a761 | ||
|
|
e22f164cbc | ||
|
|
580d681e0a | ||
|
|
89b721d726 | ||
|
|
1110a4edda | ||
|
|
f69332d9c3 | ||
|
|
6947860204 | ||
|
|
963b0c257f | ||
|
|
33f8900705 | ||
|
|
22a14aa67a | ||
|
|
60c96f7d27 | ||
|
|
c8de57aa51 | ||
|
|
827c480689 | ||
|
|
1c869013c6 | ||
|
|
404a516fef | ||
|
|
e26075060e | ||
|
|
c524f40ab2 | ||
|
|
3e563ea126 | ||
|
|
a0cb53445f | ||
|
|
84bd603e11 | ||
|
|
c3236d4eb1 | ||
|
|
4a4208f272 | ||
|
|
d24f1a1054 | ||
|
|
86200a8f11 | ||
|
|
cf0ede1a83 | ||
|
|
342a39bcb4 | ||
|
|
e7d332c7d7 | ||
|
|
689d886e74 | ||
|
|
7a8e5198ff | ||
|
|
118ceacf46 | ||
|
|
a21615a5fb | ||
|
|
2ee2e270b0 | ||
|
|
9d6ba4691c | ||
|
|
104bd272f9 | ||
|
|
62a184c386 | ||
|
|
4663a1246c | ||
|
|
0efd782bcb | ||
|
|
a0903a5842 | ||
|
|
8202182074 | ||
|
|
ee4d4e3361 | ||
|
|
b76ddcd007 | ||
|
|
6f6dedbb9c | ||
|
|
37b1c043eb | ||
|
|
58bc247c53 | ||
|
|
c5b509115a | ||
|
|
524a59aed4 | ||
|
|
be49ef5f12 | ||
|
|
d4f05fa843 | ||
|
|
adedd08e8a | ||
|
|
7dd0d10a5d | ||
|
|
d9ef1692fe | ||
|
|
b88848f0dc | ||
|
|
abc26e5c5a | ||
|
|
d7733552bf | ||
|
|
6852cc6631 | ||
|
|
8bfb2591c2 | ||
|
|
05a290839b | ||
|
|
80f9e33be5 | ||
|
|
5a78dfa210 | ||
|
|
61caca59ee | ||
|
|
383c5ba782 | ||
|
|
28fbaa3470 | ||
|
|
27dcf78e73 | ||
|
|
25883b84fa | ||
|
|
667811cbd4 | ||
|
|
7839e93a57 |
8
.github/workflows/unit-tests.yml
vendored
8
.github/workflows/unit-tests.yml
vendored
@@ -15,7 +15,15 @@ jobs:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
run: npm run build --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
|
||||
@@ -23,25 +23,7 @@ You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) an
|
||||
|
||||
### Lets start coding
|
||||
|
||||
```bash
|
||||
# clone and cd into bruno
|
||||
# use Node 14.x, Npm 8.x
|
||||
|
||||
# Install deps (note that we use npm workspaces)
|
||||
npm i
|
||||
|
||||
# run next app
|
||||
npm run dev:web
|
||||
|
||||
# run electron app
|
||||
# neededonly if you want to test changes related to electron app
|
||||
# please note that both web and electron use the same code
|
||||
# if it works in web, then it should also work in electron
|
||||
npm run dev:electron
|
||||
|
||||
# open in browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
|
||||
|
||||
### Raising Pull Request
|
||||
|
||||
|
||||
@@ -1,40 +1,53 @@
|
||||
## development
|
||||
## Development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
###
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# run next app
|
||||
npm run dev --workspace=packages/bruno-app
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# run electron app
|
||||
npm run dev --workspace=packages/bruno-electron
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# build next app
|
||||
npm run build --workspace=packages/bruno-app
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### fix
|
||||
### 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.
|
||||
|
||||
### testing
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
10
package.json
10
package.json
@@ -4,8 +4,10 @@
|
||||
"workspaces": [
|
||||
"packages/bruno-app",
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
@@ -13,17 +15,19 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"jest": "^29.2.0",
|
||||
"randomstring": "^1.2.2"
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
@@ -31,4 +35,4 @@
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -17,7 +18,7 @@
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -27,7 +28,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
@@ -44,11 +45,11 @@
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"yup": "^0.32.11"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { get, post, put } from './base';
|
||||
|
||||
// not used. kept as a placeholder for reference while implementing license key stuff
|
||||
const AuthApi = {
|
||||
whoami: () => get('auth/v1/user/whoami'),
|
||||
signup: (params) => post('auth/v1/user/signup', params),
|
||||
login: (params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer
|
||||
.invoke('bruno-account-request', {
|
||||
data: params,
|
||||
method: 'POST',
|
||||
url: `${process.env.NEXT_PUBLIC_BRUNO_SERVER_API}/auth/v1/user/login`
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default AuthApi;
|
||||
@@ -1,30 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
headers: headers
|
||||
};
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
return Promise.reject(error.response ? error.response.data : error);
|
||||
}
|
||||
);
|
||||
|
||||
const { get, post, put, delete: destroy } = apiClient;
|
||||
|
||||
export { get, post, put, destroy };
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
@@ -8,6 +8,12 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
|
||||
@@ -6,12 +6,22 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export default class CodeEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: "overlay",
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-Enter': () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
|
||||
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -63,7 +63,8 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<span>No Environment</span>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className='ml-2'>No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -8,9 +10,36 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}, [environments]);
|
||||
if(selectedEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if(environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [collection, environments, selectedEnvironment]);
|
||||
|
||||
useEffect(() => {
|
||||
// check env add
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
|
||||
if(newEnv){
|
||||
setSelectedEnvironment(newEnv);
|
||||
}
|
||||
}
|
||||
|
||||
// check env delete
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
@@ -25,7 +54,11 @@ const EnvironmentList = ({ collection }) => {
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div key={env.uid} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} onClick={() => setSelectedEnvironment(env)}>
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => setSelectedEnvironment(env)}
|
||||
>
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
* eq : equal to
|
||||
* neq : not equal to
|
||||
* gt : greater than
|
||||
* gte : greater than or equal to
|
||||
* lt : less than
|
||||
* lte : less than or equal to
|
||||
* in : in
|
||||
* notIn : not in
|
||||
* contains : contains
|
||||
* notContains : not contains
|
||||
* length : length
|
||||
* matches : matches
|
||||
* notMatches : not matches
|
||||
* startsWith : starts with
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
* isTruthy : is truthy
|
||||
* isFalsy : is falsy
|
||||
* isJson : is json
|
||||
* isNumber : is number
|
||||
* isString : is string
|
||||
* isBoolean : is boolean
|
||||
*/
|
||||
|
||||
const AssertionOperator = ({ operator, onChange }) => {
|
||||
const operators = [
|
||||
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
|
||||
'contains', 'notContains', 'length', 'matches', 'notMatches',
|
||||
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
|
||||
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
||||
];
|
||||
|
||||
const handleChange = (e) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const getLabel = (operator) => {
|
||||
switch(operator) {
|
||||
case 'eq':
|
||||
return 'equals';
|
||||
case 'neq':
|
||||
return 'notEquals';
|
||||
default:
|
||||
return operator;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<select value={operator} onChange={handleChange} className="mousetrap">
|
||||
{operators.map((operator) => (
|
||||
<option key={operator} value={operator}>
|
||||
{getLabel(operator)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssertionOperator;
|
||||
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import AssertionOperator from '../AssertionOperator';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
* eq : equal to
|
||||
* neq : not equal to
|
||||
* gt : greater than
|
||||
* gte : greater than or equal to
|
||||
* lt : less than
|
||||
* lte : less than or equal to
|
||||
* in : in
|
||||
* notIn : not in
|
||||
* contains : contains
|
||||
* notContains : not contains
|
||||
* length : length
|
||||
* matches : matches
|
||||
* notMatches : not matches
|
||||
* startsWith : starts with
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
* isTruthy : is truthy
|
||||
* isFalsy : is falsy
|
||||
* isJson : is json
|
||||
* isNumber : is number
|
||||
* isString : is string
|
||||
* isBoolean : is boolean
|
||||
*/
|
||||
const parseAssertionOperator = (str = '') => {
|
||||
if(!str || typeof str !== 'string' || !str.length) {
|
||||
return {
|
||||
operator: 'eq',
|
||||
value: str
|
||||
};
|
||||
}
|
||||
|
||||
const operators = [
|
||||
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
|
||||
'contains', 'notContains', 'length', 'matches', 'notMatches',
|
||||
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
|
||||
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
||||
];
|
||||
|
||||
const unaryOperators = [
|
||||
'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
||||
];
|
||||
|
||||
const [operator, ...rest] = str.trim().split(' ');
|
||||
const value = rest.join(' ');
|
||||
|
||||
if(unaryOperators.includes(operator)) {
|
||||
return {
|
||||
operator,
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
|
||||
if(operators.includes(operator)) {
|
||||
return {
|
||||
operator,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
operator: 'eq',
|
||||
value: str
|
||||
};
|
||||
};
|
||||
|
||||
const isUnaryOperator = (operator) => {
|
||||
const unaryOperators = [
|
||||
'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
||||
];
|
||||
|
||||
return unaryOperators.includes(operator);
|
||||
};
|
||||
|
||||
const AssertionRow = ({
|
||||
item, collection, assertion, handleAssertionChange, handleRemoveAssertion,
|
||||
onSave, handleRun
|
||||
}) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const {
|
||||
operator,
|
||||
value
|
||||
} = parseAssertionOperator(assertion.value);
|
||||
|
||||
return (
|
||||
<tr key={assertion.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={assertion.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<AssertionOperator
|
||||
operator={operator}
|
||||
onChange={(op) => handleAssertionChange({
|
||||
target: {
|
||||
value: `${op} ${value}`
|
||||
}
|
||||
}, assertion, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{!isUnaryOperator(operator) ? (
|
||||
<SingleLineEditor
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
readOnly={true}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleAssertionChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, assertion, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className='cursor-default'
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assertion.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssertionRow;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-assertion {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import AssertionRow from './AssertionRow';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Assertions = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
||||
|
||||
const handleAddAssertion = () => {
|
||||
dispatch(
|
||||
addAssertion({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleAssertionChange = (e, _assertion, type) => {
|
||||
const assertion = cloneDeep(_assertion);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
assertion.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
assertion.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
assertion.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateAssertion({
|
||||
assertion: assertion,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveAssertion = (assertion) => {
|
||||
dispatch(
|
||||
deleteAssertion({
|
||||
assertUid: assertion.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Expr</td>
|
||||
<td>Operator</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assertions && assertions.length
|
||||
? assertions.map((assertion) => {
|
||||
return (
|
||||
<AssertionRow
|
||||
key={assertion.uid}
|
||||
assertion={assertion}
|
||||
item={item}
|
||||
collection={collection}
|
||||
handleAssertionChange={handleAssertionChange}
|
||||
handleRemoveAssertion={handleRemoveAssertion}
|
||||
onSave={onSave}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
|
||||
+ Add Assertion
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Assertions;
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,6 +3,7 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -10,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -88,6 +90,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
|
||||
@@ -6,10 +6,16 @@ import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -18,17 +24,20 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||
const variables = item.draft ? get(item, 'draft.request.body.graphql.variables') : get(item, 'request.body.graphql.variables');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url);
|
||||
} = useGraphqlSchema(url, environment);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
@@ -78,9 +87,24 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>;
|
||||
}
|
||||
case 'variables': {
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -108,9 +132,24 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
|
||||
Variables
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? (
|
||||
@@ -118,7 +157,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
) : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||
<span className='ml-1'>Schema</span>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center cursor-pointer hover:underline ml-2'
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const fetchSchema = (endpoint) => {
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
});
|
||||
}
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -40,14 +25,14 @@ const useGraphqlSchema = (endpoint) => {
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchSchema(endpoint)
|
||||
.then((res) => res.json())
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('Graphql Schema loaded successfully');
|
||||
toast.success('GraphQL Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
@@ -55,7 +40,7 @@ const useGraphqlSchema = (endpoint) => {
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading Graphql Schema');
|
||||
toast.error('Error occured while loading GraphQL Schema');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={variables || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLVariables;
|
||||
@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -36,6 +38,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
@@ -67,7 +75,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Params
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
@@ -75,9 +83,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
@@ -89,7 +103,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,8 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,6 +3,7 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -10,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -88,6 +90,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
value={param.value}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
|
||||
@@ -14,6 +14,16 @@ const StyledWrapper = styled.div`
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import MD from 'markdown-it';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -40,9 +40,10 @@ export default class QueryEditor extends React.Component {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: 'brunovariables',
|
||||
mode: 'graphql',
|
||||
// mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getEnvironmentVariables(this.props.collection),
|
||||
variables: getAllVariables(this.props.collection),
|
||||
},
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
@@ -50,6 +51,7 @@ export default class QueryEditor extends React.Component {
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
scrollbarStyle: "overlay",
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
foldGutter: {
|
||||
minFoldSize: 4
|
||||
@@ -159,12 +161,12 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -176,12 +178,14 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: Overlay is messing up with schema hint
|
||||
// Fix this
|
||||
addOverlay = () => {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
// let variables = getAllVariables(this.props.collection);
|
||||
// this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
// defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
// this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -11,6 +12,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
|
||||
const handleAddParam = () => {
|
||||
@@ -66,7 +68,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -91,6 +93,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect} from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
@@ -10,11 +10,18 @@ import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme } = useTheme();
|
||||
const { theme, storedTheme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector(".method-selector-container");
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
@@ -41,10 +48,18 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<div
|
||||
className="flex items-center flex-grow input-container h-full"
|
||||
style={{
|
||||
color: 'yellow',
|
||||
width: `calc(100% - ${methodSelectorWidth}px)`,
|
||||
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
|
||||
}}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
|
||||
@@ -52,7 +52,7 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
onModeChange('formUrlEncoded');
|
||||
}}
|
||||
>
|
||||
Form Url Encoded
|
||||
Form URL Encoded
|
||||
</div>
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
@@ -10,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -63,7 +65,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -88,6 +90,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleHeaderValueChange({
|
||||
target: {
|
||||
|
||||
@@ -2,8 +2,11 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,15 +2,21 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const script = item.draft ? get(item, 'draft.request.script') : get(item, 'request.script');
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const onEdit = (value) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
@@ -20,18 +26,43 @@ const Script = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={script || ''}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1 mt-2'>
|
||||
<div className='mb-1 title text-xs'>Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 mt-6'>
|
||||
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,17 @@ import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestTests({
|
||||
@@ -27,6 +32,7 @@ const Tests = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={tests || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const handleAddVar = () => {
|
||||
dispatch(
|
||||
addVar({
|
||||
type: varType,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
_var.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{ varType === 'request' ? (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var"/>
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var"/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var, index) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleVarChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, _var, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
||||
56
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
56
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Vars = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1 mt-2'>
|
||||
<div className='mb-1 title text-xs'>Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType='request'/>
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType='response'/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
||||
@@ -12,6 +12,7 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -112,6 +113,11 @@ const RequestTabPanel = () => {
|
||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
const showRunner = collection.showRunner;
|
||||
if(showRunner) {
|
||||
return <RunnerResults collection={collection}/>;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import React from 'react';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { IconFiles, IconRun } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import VariablesView from 'components/VariablesView';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionToolBar = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(toggleRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
@@ -13,6 +23,9 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
<VariablesView collection={collection}/>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -76,6 +76,8 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showRunner = activeCollection && activeCollection.showRunner;
|
||||
|
||||
// Todo: Must support ephermal requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
@@ -83,59 +85,61 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
{!showRunner ? (
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -6,13 +6,18 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = ({ item, collection, value, width }) => {
|
||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener }) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onRun = () => {
|
||||
if(disableRunEventListener) {
|
||||
return;
|
||||
}
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
|
||||
@@ -7,10 +7,10 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TestResults = ({ results }) => {
|
||||
if (!results || !results.length) {
|
||||
const TestResults = ({ results, assertionResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
return (
|
||||
<div className="px-3">
|
||||
No tests found
|
||||
@@ -13,6 +15,9 @@ const TestResults = ({ results }) => {
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass');
|
||||
const failedAssertions = assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className='flex flex-col px-3'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
@@ -39,6 +44,31 @@ const TestResults = ({ results }) => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed: {failedAssertions.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{assertionResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if(!results.length && !assertionResults.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
const numberOfTests = results.length;
|
||||
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||
|
||||
const numberOfAssertions = assertionResults.length;
|
||||
const numberOfFailedAssertions = assertionResults.filter(result => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions;
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div>Tests</div>
|
||||
{totalNumberOfFailedTests ? (
|
||||
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||
{totalNumberOfFailedTests}
|
||||
</sup>
|
||||
) : (
|
||||
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||
{totalNumberOfTests}
|
||||
</sup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResultsLabel;
|
||||
@@ -3,12 +3,13 @@ import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Timeline = ({ item }) => {
|
||||
const request = item.requestSent || {};
|
||||
const response = item.response || {};
|
||||
const Timeline = ({ request, response}) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = response.headers || [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
@@ -12,32 +13,9 @@ import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TestResultsLabel = ({ results }) => {
|
||||
if(!results || !results.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
const numberOfTests = results.length;
|
||||
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div>Tests</div>
|
||||
{numberOfFailedTests ? (
|
||||
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||
{numberOfFailedTests}
|
||||
</sup>
|
||||
) : (
|
||||
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||
{numberOfTests}
|
||||
</sup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -62,17 +40,17 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? JSON.stringify(response.data, null, 2) : ''
|
||||
} />;
|
||||
value={response.data ? safeStringifyJSON(response.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline item={item} />;
|
||||
return <Timeline request={item.requestSent} response={item.response}/>;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} />;
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -125,7 +103,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} />
|
||||
<TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const {
|
||||
requestSent,
|
||||
responseReceived,
|
||||
testResults
|
||||
} = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', {});
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
|
||||
const selectTab = (tab) => setSelectedTab(tab);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
disableRunEventListener={true}
|
||||
value={(responseReceived && responseReceived.data) ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
<ResponseTime duration={duration} />
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsePane;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.item-path {
|
||||
.link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
.danger {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.test-summary {
|
||||
color: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
|
||||
/* test results */
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
234
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
234
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import slash from 'utils/common/slash';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
// convert to unix style path
|
||||
fullPath = slash(fullPath);
|
||||
pathname = slash(pathname);
|
||||
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir, name } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
}
|
||||
|
||||
export default function RunnerResults({collection}) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if(!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
item.name = info.name;
|
||||
item.type = info.type;
|
||||
item.filename = info.filename;
|
||||
item.pathname = info.pathname;
|
||||
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||
|
||||
if(item.status !== "error") {
|
||||
if(item.testResults) {
|
||||
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.testStatus = 'pass';
|
||||
}
|
||||
|
||||
if(item.assertionResults) {
|
||||
const failed = item.assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.assertionStatus = 'pass';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
};
|
||||
|
||||
const closeRunner = () => {
|
||||
dispatch(closeCollectionRunner({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter((item) => {
|
||||
return item.status !== "error" && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
});
|
||||
const failedRequests = items.filter((item) => {
|
||||
return item.status !== "error" && item.testStatus === 'fail' || item.assertionStatus === 'fail';
|
||||
});
|
||||
|
||||
if(!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
You have <span className='font-medium'>{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 mb-4 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='flex flex-col flex-1'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== "error" && item.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
<span className={`mr-1 ml-2 ${(item.status == "error" || item.testStatus == 'fail') ? 'danger' : ''}`}>{item.relativePath}</span>
|
||||
{(item.status !== "error" && item.status !== "completed") ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<span className='text-xs link cursor-pointer' onClick={() => setSelectedItem(item)}>
|
||||
(<span className='mr-1'>
|
||||
{get(item.responseReceived, 'status')}
|
||||
</span>
|
||||
<span>
|
||||
{get(item.responseReceived, 'statusText')}
|
||||
</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.status == "error" ? (
|
||||
<div className="error-message pl-8 pt-2 text-xs">
|
||||
{item.error}
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.testResults ? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)): null}
|
||||
{item.assertionResults ? item.assertionResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)): null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='flex flex-1' style={{width: '50%'}}>
|
||||
{selectedItem ? (
|
||||
<div className='flex flex-col w-full overflow-auto'>
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className='mr-2'>{selectedItem.relativePath}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
|
||||
<ResponsePane item={selectedItem} collection={collection}/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -19,8 +20,13 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occured while cloning the request')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.bruno-modal-content {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(showRunnerView({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
|
||||
const items = flattenItems(item ? item.items : collection.items);
|
||||
const requestItems = items.filter((item) => item.type !== 'folder');
|
||||
const recursiveRunLength = requestItems.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title='Collection Runner' hideFooter={true} handleCancel={onClose}>
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Run</span>
|
||||
<span className='ml-1 text-xs'>({runLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will only run the requests in this folder.
|
||||
</div>
|
||||
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Recursive Run</span>
|
||||
<span className='ml-1 text-xs'>({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will run all the requests in this folder and all its subfolders.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className='mr-3'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunCollectionItem;
|
||||
@@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -15,6 +15,7 @@ import RequestMethod from './RequestMethod';
|
||||
import RenameCollectionItem from './RenameCollectionItem';
|
||||
import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
@@ -33,10 +34,11 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'COLLECTION_ITEM',
|
||||
type: `COLLECTION_ITEM_${collection.uid}`,
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
@@ -44,7 +46,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
@@ -84,6 +86,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(hideRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
if (isItemARequest(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
@@ -151,6 +156,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
||||
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
||||
{runCollectionModalOpen && <RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />}
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
@@ -211,6 +217,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setRunCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -52,6 +52,12 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-collection-name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -12,6 +12,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import RunCollectionItem from './CollectionItem/RunCollectionItem';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
@@ -24,6 +25,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [showRunCollectionModal, setShowRunCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -53,19 +55,13 @@ const Collection = ({ collection, searchText }) => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
};
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
};
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||
},
|
||||
@@ -78,6 +74,12 @@ const Collection = ({ collection, searchText }) => {
|
||||
})
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
@@ -97,9 +99,10 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
||||
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
|
||||
{showRunCollectionModal && <RunCollectionItem collection={collection} onClose={() => setShowRunCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div className="flex flex-grow items-center" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
|
||||
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, minWidth:16, color: 'rgb(160 160 160)' }} />
|
||||
<div className="ml-1" id="sidebar-collection-name">{collection.name}</div>
|
||||
</div>
|
||||
<div className="collection-actions">
|
||||
@@ -122,8 +125,16 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
{/* Todo: implement rename collection */}
|
||||
{/* <div
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRunCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
@@ -131,7 +142,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</div> */}
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -84,7 +84,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
@@ -21,6 +22,14 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
|
||||
const handleImportInsomniaCollection = () => {
|
||||
importInsomniaCollection()
|
||||
.then((collection) => {
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
@@ -36,6 +45,12 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
>
|
||||
Postman Collection
|
||||
</div>
|
||||
<div
|
||||
className='text-link hover:underline cursor-pointer mt-2'
|
||||
onClick={handleImportInsomniaCollection}
|
||||
>
|
||||
Insomnia Collection
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -15,12 +15,24 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
folderName: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
folderName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
folderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'folderName',
|
||||
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
|
||||
test:(value) => {
|
||||
if(item && item.uid) {
|
||||
return true;
|
||||
}
|
||||
return value && !(value.trim().toLowerCase().includes('environments'))
|
||||
}
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
|
||||
.then(() => onClose())
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,14 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
requestMethod: 'GET'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
requestName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
requestName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'requestName',
|
||||
message: 'The request name "index" is reserved in bruno',
|
||||
test: value => value && !(value.trim().toLowerCase().includes('index')),
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (isEphermal) {
|
||||
@@ -49,7 +56,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
);
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
} else {
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
@@ -62,7 +69,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -95,7 +102,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
Http
|
||||
HTTP
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -111,7 +118,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
Graphql
|
||||
GraphQL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +145,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
Url
|
||||
URL
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2 ">
|
||||
|
||||
@@ -2,11 +2,13 @@ import MenuBar from './MenuBar';
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import StyledWrapper, { BottomWrapper, VersionNumber } from './StyledWrapper';
|
||||
import GitHubButton from 'react-github-btn'
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconChevronsRight } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging, toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 222;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
@@ -17,6 +19,10 @@ const Sidebar = () => {
|
||||
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
@@ -70,13 +76,16 @@ const Sidebar = () => {
|
||||
setAsideWidth(leftSidebarWidth);
|
||||
}, [leftSidebarWidth]);
|
||||
|
||||
const leftMenuBarWidth = leftMenuBarOpen ? 48 : 0;
|
||||
const collectionsWidth = asideWidth - leftMenuBarWidth;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex relative">
|
||||
<aside style={{ width: `${asideWidth}px`, minWidth: `${asideWidth}px` }}>
|
||||
<aside>
|
||||
<div className="flex flex-row h-full w-full">
|
||||
{leftMenuBarOpen && <MenuBar />}
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col w-full" style={{width: collectionsWidth}}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<TitleBar />
|
||||
<Collections />
|
||||
@@ -86,19 +95,29 @@ const Sidebar = () => {
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
{!leftMenuBarOpen && <IconChevronsRight size={24} strokeWidth={1.5} className="mr-2 hover:text-gray-700" onClick={() => dispatch(toggleLeftMenuBar())} />}
|
||||
{/* <IconLayoutGrid size={20} strokeWidth={1.5} className="mr-2"/> */}
|
||||
{/* Need to ut github stars link here */}
|
||||
</div>
|
||||
<div className="pl-1">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=usebruno&repo=bruno&type=star&count=true"
|
||||
frameBorder="0"
|
||||
scrolling="0"
|
||||
width="100"
|
||||
height="20"
|
||||
title="GitHub"
|
||||
></iframe>
|
||||
<div className="pl-1" style={{position: 'relative', top: '3px'}}>
|
||||
{storedTheme === 'dark' ? (
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
data-color-scheme="no-preference: dark; light: dark; dark: light;"
|
||||
data-show-count="true"
|
||||
aria-label="Star usebruno/bruno on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
) : (
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
data-color-scheme="no-preference: light; light: light; dark: light;"
|
||||
data-show-count="true"
|
||||
aria-label="Star usebruno/bruno on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.7.1</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.10.2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,29 @@ const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
height: 30px;
|
||||
height: 34px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
overflow: hidden;
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
@@ -28,6 +45,8 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-line {
|
||||
color: ${(props) => props.theme.text};
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -14,6 +14,10 @@ if (!SERVER_RENDERED) {
|
||||
class SingleLineEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Keep a cached version of the value, this cache will be updated when the
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
}
|
||||
@@ -22,9 +26,10 @@ class SingleLineEditor extends Component {
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
mode: "brunovariables",
|
||||
brunoVarInfo: {
|
||||
variables: getEnvironmentVariables(this.props.collection),
|
||||
variables: getAllVariables(this.props.collection),
|
||||
},
|
||||
extraKeys: {
|
||||
"Enter": () => {
|
||||
@@ -67,19 +72,39 @@ class SingleLineEditor extends Component {
|
||||
'Tab': () => {}
|
||||
},
|
||||
});
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.on('change', (cm) => {
|
||||
this.props.onChange(cm.getValue());
|
||||
});
|
||||
this.editor.setValue(this.props.value || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
// Ensure the changes caused by this update are not interpretted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value || '');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -87,7 +112,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, "text/plain");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
const Tooltip = ({ text, tooltipId }) => {
|
||||
return (
|
||||
<>
|
||||
<svg tabindex="-1" id={tooltipId} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="inline-block ml-2 cursor-pointer" viewBox="0 0 16 16" style={{marginTop: 1}}>
|
||||
<svg tabIndex="-1" id={tooltipId} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="inline-block ml-2 cursor-pointer" viewBox="0 0 16 16" style={{marginTop: 1}}>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
|
||||
</svg>
|
||||
|
||||
@@ -6,12 +6,12 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.variable-name{
|
||||
width:100px;
|
||||
min-width:180px;
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
max-width: 500px;
|
||||
inline-size: 500px;
|
||||
max-width: 600px;
|
||||
inline-size: 600px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VariablesTable = ({ variables }) => {
|
||||
const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
const collectionVars = [];
|
||||
|
||||
forOwn(cloneDeep(collectionVariables), (value, key) => {
|
||||
collectionVars.push({
|
||||
uid: uuid(),
|
||||
name: key,
|
||||
value: value
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className='mb-2 font-medium'>Environment Variables</div>
|
||||
{(variables && variables.length) ? variables.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
@@ -13,6 +27,16 @@ const VariablesTable = ({ variables }) => {
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
|
||||
<div className='mt-2 font-medium'>Collection Variables</div>
|
||||
{(collectionVars && collectionVars.length) ? collectionVars.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className='variable-name text-yellow-600 text-right pr-2'>{variable.name}</div>
|
||||
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import filter from 'lodash/filter';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import VariablesTable from './VariablesTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import PopOver from './PopOver';
|
||||
import PopOver from './Popover';
|
||||
import { IconEye } from '@tabler/icons';
|
||||
|
||||
const VariablesView = ({collection}) => {
|
||||
@@ -34,7 +34,7 @@ const VariablesView = ({collection}) => {
|
||||
handleClose={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
{(enabledVariables && enabledVariables.length) ? <VariablesTable variables={enabledVariables} /> : 'No variables found'}
|
||||
{(enabledVariables && enabledVariables.length) ? <VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables}/> : 'No variables found'}
|
||||
</div>
|
||||
</PopOver>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone } from '@tabler/icons';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
@@ -60,9 +60,9 @@ const Welcome = () => {
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={handleOpenCollection}>
|
||||
<span className="label ml-2">
|
||||
Open Collection
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,15 +74,18 @@ const Welcome = () => {
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
{/* <div className="flex items-center mt-2">
|
||||
<IconBook size={18} strokeWidth={2}/><span className="label ml-2">Docs</span>
|
||||
</div> */}
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
|
||||
@@ -43,7 +43,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
.btn-close {
|
||||
color: ${(props) => props.theme.button.close.color};
|
||||
background: ${(props) => props.theme.button.close.bg};
|
||||
border: solid 1px ${(props) => props.theme.button.close.border};;
|
||||
border: solid 1px ${(props) => props.theme.button.close.border};
|
||||
|
||||
&.btn-border {
|
||||
border: solid 1px #696969;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
13
packages/bruno-app/src/hooks/usePrevious/index.js
Normal file
13
packages/bruno-app/src/hooks/usePrevious/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value; //assign the value of ref to the argument
|
||||
},[value]); //this code will run when the value of 'value' changes
|
||||
|
||||
return ref.current; //in the end, return the current ref value.
|
||||
}
|
||||
|
||||
export default usePrevious;
|
||||
@@ -8,11 +8,13 @@ import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import 'codemirror/theme/material.css';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import 'codemirror/addon/scroll/simplescrollbars.css';
|
||||
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
if (!SERVER_RENDERED) {
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
@@ -52,7 +54,7 @@ export default function Main() {
|
||||
<div>
|
||||
<StyledWrapper className={className}>
|
||||
<Sidebar />
|
||||
<section className="flex flex-grow flex-col">
|
||||
<section className="flex flex-grow flex-col overflow-auto">
|
||||
{showHomePage ? (
|
||||
<Welcome />
|
||||
) : (
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
requestSentEvent,
|
||||
requestQueuedEvent,
|
||||
testResultsEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
assertionResultsEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
collectionRenamedEvent,
|
||||
runFolderEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -31,6 +34,10 @@ const useCollectionTreeSync = () => {
|
||||
};
|
||||
|
||||
const _collectionTreeUpdated = (type, val) => {
|
||||
if(window.__IS_DEV__) {
|
||||
console.log(type);
|
||||
console.log(val);
|
||||
}
|
||||
if (type === 'addDir') {
|
||||
dispatch(
|
||||
collectionAddDirectoryEvent({
|
||||
@@ -80,8 +87,13 @@ const useCollectionTreeSync = () => {
|
||||
toast.success('Collection is already opened');
|
||||
};
|
||||
|
||||
const _displayError = (message) => {
|
||||
toast.error(message || 'Something went wrong!');
|
||||
const _displayError = (error) => {
|
||||
if(typeof error === "string") {
|
||||
return toast.error(error || 'Something went wrong!');
|
||||
}
|
||||
if(typeof message === "object") {
|
||||
return toast.error(error.message || 'Something went wrong!');
|
||||
}
|
||||
};
|
||||
|
||||
const _httpRequestSent = (val) => {
|
||||
@@ -100,6 +112,18 @@ const useCollectionTreeSync = () => {
|
||||
dispatch(testResultsEvent(val));
|
||||
};
|
||||
|
||||
const _assertionResults = (val) => {
|
||||
dispatch(assertionResultsEvent(val));
|
||||
};
|
||||
|
||||
const _collectionRenamed = (val) => {
|
||||
dispatch(collectionRenamedEvent(val));
|
||||
};
|
||||
|
||||
const _runFolderEvent = (val) => {
|
||||
dispatch(runFolderEvent(val));
|
||||
};
|
||||
|
||||
ipcRenderer.invoke('renderer:ready');
|
||||
|
||||
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||
@@ -110,6 +134,9 @@ const useCollectionTreeSync = () => {
|
||||
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
|
||||
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
||||
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
|
||||
const removeListener9 = ipcRenderer.on('main:assertion-results', _assertionResults);
|
||||
const removeListener10 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
||||
const removeListener11 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
|
||||
|
||||
return () => {
|
||||
removeListener1();
|
||||
@@ -120,6 +147,9 @@ const useCollectionTreeSync = () => {
|
||||
removeListener6();
|
||||
removeListener7();
|
||||
removeListener8();
|
||||
removeListener9();
|
||||
removeListener10();
|
||||
removeListener11();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import AuthApi from 'api/auth';
|
||||
import reducer from './reducer';
|
||||
|
||||
const AuthContext = React.createContext();
|
||||
|
||||
const initialState = {
|
||||
isLoading: true,
|
||||
lastStateTransition: null,
|
||||
currentUser: null
|
||||
};
|
||||
|
||||
export const AuthProvider = (props) => {
|
||||
const router = useRouter();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
AuthApi.whoami()
|
||||
.then((response) => {
|
||||
let data = response.data;
|
||||
dispatch({
|
||||
type: 'WHOAMI_SUCCESS',
|
||||
user: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
username: data.username
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({
|
||||
type: 'WHOAMI_ERROR',
|
||||
error: error
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.lastStateTransition === 'LOGIN_SUCCESS') {
|
||||
router.push('/');
|
||||
}
|
||||
if (state.lastStateTransition === 'WHOAMI_ERROR') {
|
||||
// Todo: decide action
|
||||
// router.push('/login');
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return <AuthContext.Provider value={[state, dispatch]} {...props} />;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = React.useContext(AuthContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(`useAuth must be used within a AuthProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
@@ -1,43 +0,0 @@
|
||||
import produce from 'immer';
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'WHOAMI_SUCCESS': {
|
||||
return produce(state, (draft) => {
|
||||
draft.isLoading = false;
|
||||
draft.currentUser = action.user;
|
||||
draft.lastStateTransition = 'WHOAMI_SUCCESS';
|
||||
});
|
||||
}
|
||||
|
||||
case 'WHOAMI_ERROR': {
|
||||
return produce(state, (draft) => {
|
||||
draft.isLoading = false;
|
||||
draft.currentUser = null;
|
||||
draft.lastStateTransition = 'WHOAMI_ERROR';
|
||||
});
|
||||
}
|
||||
|
||||
case 'LOGIN_SUCCESS': {
|
||||
return produce(state, (draft) => {
|
||||
draft.isLoading = false;
|
||||
draft.currentUser = action.user;
|
||||
draft.lastStateTransition = 'LOGIN_SUCCESS';
|
||||
});
|
||||
}
|
||||
|
||||
case 'LOGOUT_SUCCESS': {
|
||||
return produce(state, (draft) => {
|
||||
draft.isLoading = false;
|
||||
draft.currentUser = null;
|
||||
draft.lastStateTransition = 'LOGOUT_SUCCESS';
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
|
||||
@@ -144,6 +145,23 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [setShowNewRequestModal]);
|
||||
|
||||
// close tab hotkey
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [activeTabUid]
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+w', 'ctrl+w']);
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
||||
|
||||
@@ -18,7 +18,11 @@ export const appSlice = createSlice({
|
||||
},
|
||||
toggleLeftMenuBar: (state) => {
|
||||
state.leftMenuBarOpen = !state.leftMenuBarOpen;
|
||||
state.leftSidebarWidth = state.leftMenuBarOpen ? 270 : 222;
|
||||
if(state.leftMenuBarOpen) {
|
||||
state.leftSidebarWidth += 48;
|
||||
} else {
|
||||
state.leftSidebarWidth -= 48;
|
||||
}
|
||||
},
|
||||
refreshScreenWidth: (state) => {
|
||||
state.screenWidth = window.innerWidth;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import path from 'path';
|
||||
import toast from 'react-hot-toast';
|
||||
import trim from 'lodash/trim';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
@@ -10,20 +13,21 @@ import {
|
||||
moveCollectionItemToRootOfCollection,
|
||||
findCollectionByUid,
|
||||
recursivelyGetAllItemUids,
|
||||
transformCollectionToSaveToIdb,
|
||||
transformRequestToSaveToFilesystem,
|
||||
findParentItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
isItemARequest,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
refreshUidsInItem
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
updateLastAction,
|
||||
resetRunResults,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
newItem as _newItem,
|
||||
@@ -47,26 +51,16 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
collectionCopy.name = newName;
|
||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy, {
|
||||
ignoreDraft: true
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
collectionSchema
|
||||
.validate(collectionToSave)
|
||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
_renameCollection({
|
||||
newName: newName,
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
ipcRenderer
|
||||
.invoke('renderer:rename-collection', newName, collection.pathname)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
@@ -109,7 +103,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
sendNetworkRequest(itemCopy, collection, environment)
|
||||
sendNetworkRequest(itemCopy, collection, environment, collectionCopy.collectionVariables)
|
||||
.then((response) => {
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
@@ -148,6 +142,38 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const folder = findItemInCollection(collectionCopy, folderUid);
|
||||
|
||||
if (folderUid && !folder) {
|
||||
return reject(new Error('Folder not found'));
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
dispatch(resetRunResults({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:run-collection-folder', folder, collectionCopy, environment, collectionCopy.collectionVariables, recursive)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@@ -168,7 +194,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('folder with same name already exists'));
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
} else {
|
||||
const currentItem = findItemInCollection(collection, itemUid);
|
||||
@@ -183,7 +209,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('folder with same name already exists'));
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
} else {
|
||||
return reject(new Error('unable to find parent folder'));
|
||||
@@ -207,14 +233,14 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
return reject(new Error('Unable to locate item'));
|
||||
}
|
||||
|
||||
const dirname = path.dirname(item.pathname);
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
|
||||
let newPathname = '';
|
||||
if (item.type === 'folder') {
|
||||
newPathname = `${dirname}${PATH_SEPARATOR}${trim(newName)}`;
|
||||
newPathname = path.join(dirname, trim(newName));
|
||||
} else {
|
||||
const filename = resolveRequestFilename(newName);
|
||||
newPathname = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||
newPathname = path.join(dirname, filename);
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
@@ -252,6 +278,8 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? (requestItems.length + 1) : 1;
|
||||
|
||||
itemSchema
|
||||
.validate(itemToSave)
|
||||
@@ -259,14 +287,16 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in collection`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
} else {
|
||||
const reqWithSameNameExists = find(parentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||
if (!reqWithSameNameExists) {
|
||||
const dirname = path.dirname(item.pathname);
|
||||
const fullName = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
const fullName = path.join(dirname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? (requestItems.length + 1) : 1;
|
||||
|
||||
itemSchema
|
||||
.validate(itemToSave)
|
||||
@@ -274,7 +304,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in the folder`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -360,8 +390,6 @@ export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispa
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
|
||||
console.log('itemsToResequence', itemsToResequence);
|
||||
console.log('itemsToResequence2', itemsToResequence2);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, targetItemParent.pathname)
|
||||
@@ -455,15 +483,23 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
|
||||
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
if(isItemAFolder(draggedItem)) {
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -501,8 +537,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const filename = resolveRequestFilename(requestName);
|
||||
if (!itemUid) {
|
||||
const reqWithSameNameExists = find(collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||
item.seq = collectionCopy.items ? collectionCopy.items.length : 1;
|
||||
item.seq = item.seq + 1;
|
||||
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
@@ -510,21 +546,21 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in collection`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
} else {
|
||||
const currentItem = findItemInCollection(collection, itemUid);
|
||||
if (currentItem) {
|
||||
const reqWithSameNameExists = find(currentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||
item.seq = currentItem.items ? currentItem.items.length : 1;
|
||||
item.seq = item.seq + 1;
|
||||
const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in the folder`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,6 +577,13 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name)
|
||||
.then(dispatch(updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
}
|
||||
})))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@@ -682,7 +725,9 @@ export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState)
|
||||
uid: uid,
|
||||
name: name,
|
||||
pathname: pathname,
|
||||
items: []
|
||||
items: [],
|
||||
showRunner: false,
|
||||
collectionVariables: {}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -8,9 +8,10 @@ import filter from 'lodash/filter';
|
||||
import each from 'lodash/each';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import splitOnFirst from 'split-on-first';
|
||||
import { splitOnFirst } from 'utils/url';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
findCollectionByPathname,
|
||||
findItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
areItemsTheSameExceptSeqUpdate
|
||||
} from 'utils/collections';
|
||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platform';
|
||||
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
@@ -37,6 +38,14 @@ export const collectionsSlice = createSlice({
|
||||
createCollection: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
// this is used in scenarios where we want to know the last action performed on the collection
|
||||
// and take some extra action based on that
|
||||
// for example, when a env is created, we want to auto select it the env modal
|
||||
collection.lastAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
@@ -53,13 +62,12 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
addEnvironment: (state, action) => {
|
||||
const { environment, collectionUid } = action.payload;
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.environments = collection.environments || [];
|
||||
collection.environments.push(environment);
|
||||
collection.lastAction = lastAction;
|
||||
}
|
||||
},
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
@@ -175,7 +183,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
scriptEnvironmentUpdateEvent: (state, action) => {
|
||||
const { collectionUid, environment } = action.payload;
|
||||
const { collectionUid, envVariables, collectionVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
@@ -183,7 +191,7 @@ export const collectionsSlice = createSlice({
|
||||
const activeEnvironment = findEnvironmentInCollection(collection, activeEnvironmentUid);
|
||||
|
||||
if (activeEnvironment) {
|
||||
forOwn(environment, (value, key) => {
|
||||
forOwn(envVariables, (value, key) => {
|
||||
const variable = find(activeEnvironment.variables, (v) => v.name === key);
|
||||
|
||||
if (variable) {
|
||||
@@ -191,6 +199,8 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
collection.collectionVariables = collectionVariables;
|
||||
}
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
@@ -622,6 +632,22 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestGraphqlVariables: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.body.mode = 'graphql';
|
||||
item.draft.request.body.graphql = item.draft.request.body.graphql || {};
|
||||
item.draft.request.body.graphql.variables = action.payload.variables;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -632,7 +658,23 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.script = action.payload.script;
|
||||
item.draft.request.script = item.draft.request.script || {};
|
||||
item.draft.request.script.req = action.payload.script;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateResponseScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.script = item.draft.request.script || {};
|
||||
item.draft.request.script.res = action.payload.script;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -664,12 +706,159 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
addAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.assertions = item.draft.request.assertions || [];
|
||||
item.draft.request.assertions.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
updateAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const assertion = item.draft.request.assertions.find((a) => a.uid === action.payload.assertion.uid);
|
||||
if (assertion) {
|
||||
assertion.name = action.payload.assertion.name;
|
||||
assertion.value = action.payload.assertion.value;
|
||||
assertion.enabled = action.payload.assertion.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.assertions = item.draft.request.assertions.filter((a) => a.uid !== action.payload.assertUid);
|
||||
}
|
||||
}
|
||||
},
|
||||
addVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
item.draft.request.vars.req.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
item.draft.request.vars.res.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
|
||||
const reqVar = find(item.draft.request.vars.req, (v) => v.uid === action.payload.var.uid);
|
||||
if (reqVar) {
|
||||
reqVar.name = action.payload.var.name;
|
||||
reqVar.value = action.payload.var.value;
|
||||
reqVar.description = action.payload.var.description;
|
||||
reqVar.enabled = action.payload.var.enabled;
|
||||
}
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
const resVar = find(item.draft.request.vars.res, (v) => v.uid === action.payload.var.uid);
|
||||
if (resVar) {
|
||||
resVar.name = action.payload.var.name;
|
||||
resVar.value = action.payload.var.value;
|
||||
resVar.description = action.payload.var.description;
|
||||
resVar.enabled = action.payload.var.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
item.draft.request.vars.req = item.draft.request.vars.req.filter((v) => v.uid !== action.payload.varUid);
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
item.draft.request.vars.res = item.draft.request.vars.res.filter((v) => v.uid !== action.payload.varUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const dirname = path.dirname(file.meta.pathname);
|
||||
const dirname = getDirectoryName(file.meta.pathname);
|
||||
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||
let currentPath = collection.pathname;
|
||||
let currentSubItems = collection.items;
|
||||
@@ -810,6 +999,14 @@ export const collectionsSlice = createSlice({
|
||||
existingEnv.variables = environment.variables;
|
||||
} else {
|
||||
collection.environments.push(environment);
|
||||
|
||||
const lastAction = collection.lastAction;
|
||||
if(lastAction && lastAction.type === 'ADD_ENVIRONMENT') {
|
||||
collection.lastAction = null;
|
||||
if(lastAction.payload === environment.name) {
|
||||
collection.activeEnvironmentUid = environment.uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -824,6 +1021,131 @@ export const collectionsSlice = createSlice({
|
||||
item.testResults = results;
|
||||
}
|
||||
}
|
||||
},
|
||||
assertionResultsEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, results } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
console.log(results);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (item) {
|
||||
item.assertionResults = results;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionRenamedEvent: (state, action) => {
|
||||
const { collectionPathname, newName } = action.payload;
|
||||
const collection = findCollectionByPathname(state.collections, collectionPathname);
|
||||
|
||||
if (collection) {
|
||||
collection.name = newName;
|
||||
}
|
||||
},
|
||||
toggleRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = !collection.showRunner;
|
||||
}
|
||||
},
|
||||
showRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = true;
|
||||
}
|
||||
},
|
||||
hideRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = false;
|
||||
}
|
||||
},
|
||||
resetRunResults: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
}
|
||||
},
|
||||
runFolderEvent: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, type, isRecursive, error } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
const request = findItemInCollection(collection, itemUid);
|
||||
|
||||
collection.runnerResult = collection.runnerResult || {info: {}, items: []};
|
||||
|
||||
// todo
|
||||
// get startedAt and endedAt from the runner and display it in the UI
|
||||
if(type === 'testrun-started') {
|
||||
const info = collection.runnerResult.info;
|
||||
info.collectionUid = collectionUid;
|
||||
info.folderUid = folderUid;
|
||||
info.isRecursive = isRecursive;
|
||||
info.status = 'started';
|
||||
}
|
||||
|
||||
if(type === 'testrun-ended') {
|
||||
const info = collection.runnerResult.info;
|
||||
info.status = 'ended';
|
||||
}
|
||||
|
||||
|
||||
if(type === 'request-queued') {
|
||||
collection.runnerResult.items.push({
|
||||
uid: request.uid,
|
||||
status: 'queued'
|
||||
});
|
||||
}
|
||||
|
||||
if(type === 'request-sent') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
item.status = 'running';
|
||||
item.requestSent = action.payload.requestSent;
|
||||
}
|
||||
|
||||
if(type === 'response-received') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
item.status = 'completed';
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
}
|
||||
|
||||
if(type === 'test-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
item.testResults = action.payload.testResults;
|
||||
}
|
||||
|
||||
if(type === 'assertion-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
item.assertionResults = action.payload.assertionResults;
|
||||
}
|
||||
|
||||
if(type === 'error') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
item.error = action.payload.error;
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
item.status = "error";
|
||||
}
|
||||
}
|
||||
},
|
||||
closeCollectionRunner: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.showRunner = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -832,7 +1154,7 @@ export const {
|
||||
createCollection,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
addEnvironment,
|
||||
updateLastAction,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
@@ -865,16 +1187,32 @@ export const {
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
updateRequestGraphqlVariables,
|
||||
updateRequestScript,
|
||||
updateResponseScript,
|
||||
updateRequestTests,
|
||||
updateRequestMethod,
|
||||
addAssertion,
|
||||
updateAssertion,
|
||||
deleteAssertion,
|
||||
addVar,
|
||||
updateVar,
|
||||
deleteVar,
|
||||
collectionAddFileEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionChangeFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
testResultsEvent
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
collectionRenamedEvent,
|
||||
toggleRunnerView,
|
||||
showRunnerView,
|
||||
hideRunnerView,
|
||||
resetRunResults,
|
||||
runFolderEvent,
|
||||
closeCollectionRunner
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -32,7 +32,7 @@ const darkTheme = {
|
||||
color: '#ccc',
|
||||
muted: '#9d9d9d',
|
||||
bg: '#252526',
|
||||
dragbar: '#8a8a8a',
|
||||
dragbar: '#666666',
|
||||
|
||||
badge: {
|
||||
bg: '#3D3D3D'
|
||||
@@ -193,9 +193,9 @@ const darkTheme = {
|
||||
|
||||
codemirror: {
|
||||
bg: '#1e1e1e',
|
||||
border: 'transparent',
|
||||
border: '#373737',
|
||||
gutter: {
|
||||
bg: '#1e1e1e'
|
||||
bg: '#262626'
|
||||
},
|
||||
variable: {
|
||||
valid: 'rgb(11 178 126)',
|
||||
@@ -215,7 +215,7 @@ const darkTheme = {
|
||||
},
|
||||
striped: '#2A2D2F',
|
||||
input: {
|
||||
color: '#555555'
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ const deleteUidsInItems = (items) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
each(get(item, 'request.headers'), (header) => delete header.uid);
|
||||
each(get(item, 'request.params'), (param) => delete param.uid);
|
||||
each(get(item, 'request.vars.req'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.res'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
|
||||
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
|
||||
}
|
||||
@@ -19,6 +22,31 @@ const deleteUidsInItems = (items) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Some of the models in the app are not consistent with the Collection Json format
|
||||
* This function is used to transform the models to the Collection Json format
|
||||
*/
|
||||
const transformItem = (items = []) => {
|
||||
each(items, (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
item.request.query = item.request.params;
|
||||
delete item.request.params;
|
||||
|
||||
if(item.type === 'graphql-request') {
|
||||
item.type = 'graphql';
|
||||
}
|
||||
|
||||
if(item.type === 'http-request') {
|
||||
item.type = 'http';
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
transformItem(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUidsInEnvs = (envs) => {
|
||||
each(envs, (env) => {
|
||||
delete env.uid;
|
||||
@@ -31,6 +59,8 @@ const exportCollection = (collection) => {
|
||||
delete collection.uid;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
transformItem(collection.items);
|
||||
|
||||
|
||||
const fileName = `${collection.name}.json`;
|
||||
const fileBlob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });
|
||||
|
||||
@@ -164,7 +164,11 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) =>
|
||||
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
collection.items.push(draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
if(draggedItem.type == 'folder') {
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
|
||||
} else {
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
}
|
||||
};
|
||||
|
||||
export const getItemsToResequence = (parent, collection) => {
|
||||
@@ -278,6 +282,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||
},
|
||||
script: si.draft.request.script,
|
||||
vars: si.draft.request.vars,
|
||||
assertions: si.draft.request.assertions,
|
||||
tests: si.draft.request.tests
|
||||
};
|
||||
}
|
||||
@@ -298,6 +304,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
},
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
tests: si.request.tests
|
||||
};
|
||||
}
|
||||
@@ -345,6 +353,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
headers: [],
|
||||
body: _item.request.body,
|
||||
script: _item.request.script,
|
||||
vars: _item.request.vars,
|
||||
assertions: _item.request.assertions,
|
||||
tests: _item.request.tests
|
||||
}
|
||||
};
|
||||
@@ -423,7 +433,7 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
label = 'Form Url Encoded';
|
||||
label = 'Form URL Encoded';
|
||||
break;
|
||||
}
|
||||
case 'multipartForm': {
|
||||
@@ -513,3 +523,25 @@ export const getEnvironmentVariables = (collection) => {
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
export const getTotalRequestCountInCollection = (collection) => {
|
||||
let count = 0;
|
||||
each(collection.items, (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
count++;
|
||||
} else if (isItemAFolder(item)) {
|
||||
count += getTotalRequestCountInCollection(item);
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collection.collectionVariables
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,11 +36,14 @@ export const safeParseJSON = (str) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const safeStringifyJSON = (obj) => {
|
||||
export const safeStringifyJSON = (obj, indent=false) => {
|
||||
if(!obj) {
|
||||
return obj;
|
||||
}
|
||||
try {
|
||||
if(indent) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return obj;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@@ -18,9 +19,17 @@ export const resolveRequestFilename = (name) => {
|
||||
};
|
||||
|
||||
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
if (!path.isAbsolute(pathname)) {
|
||||
throw new Error('Invalid path!');
|
||||
}
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
rootPath = slash(rootPath);
|
||||
|
||||
const relativePath = path.relative(rootPath, pathname);
|
||||
return relativePath ? relativePath.split(path.sep) : [];
|
||||
};
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
|
||||
return path.dirname(pathname);
|
||||
}
|
||||
|
||||
20
packages/bruno-app/src/utils/common/slash.js
Normal file
20
packages/bruno-app/src/utils/common/slash.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export default slash;
|
||||
@@ -1,6 +1,6 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { validateSchema, transformItemsInCollection, updateUidsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -30,6 +30,7 @@ const importCollection = () => {
|
||||
.then(parseJsonCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(updateUidsInCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
|
||||
@@ -30,7 +30,11 @@ export const updateUidsInCollection = (_collection) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.query'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
|
||||
@@ -48,7 +52,33 @@ export const updateUidsInCollection = (_collection) => {
|
||||
});
|
||||
};
|
||||
updateEnvUids(collection.environments);
|
||||
updateEnvUids(collection.environments);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
// todo
|
||||
// need to eventually get rid of supporting old collection app models
|
||||
// 1. start with making request type a constant fetched from a single place
|
||||
// 2. move references of param and replace it with query inside the app
|
||||
export const transformItemsInCollection = (collection) => {
|
||||
const transformItems = (items = []) => {
|
||||
each(items, (item) => {
|
||||
if (['http', 'graphql'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
if(item.request.query) {
|
||||
item.request.params = item.request.query;
|
||||
}
|
||||
|
||||
delete item.request.query;
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
transformItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
transformItems(collection.items);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
186
packages/bruno-app/src/utils/importers/insomnia-collection.js
Normal file
186
packages/bruno-app/src/utils/importers/insomnia-collection.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
try {
|
||||
const graphql = JSON.parse(text);
|
||||
|
||||
return {
|
||||
query: graphql.query,
|
||||
variables: JSON.stringify(graphql.variables, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const transformInsomniaRequestItem = (request) => {
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: request.name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(request.headers, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.parameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '');
|
||||
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = request.body.text;
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = request.body.text;
|
||||
} else if (mimeType === 'text/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = request.body.text;
|
||||
} else if (mimeType === 'application/graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQL(request.body.text);
|
||||
}
|
||||
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const parseInsomniaCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: "1",
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const insomniaExport = JSON.parse(data);
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find(resource => resource._type === 'workspace' && resource.scope === 'collection');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||
}
|
||||
|
||||
brunoCollection.name = insomniaCollection.name;
|
||||
|
||||
const requestsAndFolders = insomniaResources.filter(
|
||||
(resource) => resource._type === 'request' || resource._type === 'request_group'
|
||||
) || [];
|
||||
|
||||
function createFolderStructure(resources, parentId = null) {
|
||||
const requestGroups = resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder) => {
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === folder._id);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: folder.name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem)),
|
||||
}
|
||||
});
|
||||
|
||||
return folders.concat(requests.map(transformInsomniaRequestItem));
|
||||
}
|
||||
|
||||
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id),
|
||||
|
||||
resolve(brunoCollection);
|
||||
} catch (err) {
|
||||
reject(new BrunoError('An error occurred while parsing the Insomnia collection'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseInsomniaCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
@@ -3,7 +3,7 @@ import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, hydrateSeqInCollection } from './common';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -178,6 +178,7 @@ const importCollection = () => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve(collection))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const sendNetworkRequest = async (item, collection, environment) => {
|
||||
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const timeStart = Date.now();
|
||||
sendHttpRequest(item, collection, environment)
|
||||
sendHttpRequest(item, collection, environment, collectionVariables)
|
||||
.then((response) => {
|
||||
const timeEnd = Date.now();
|
||||
resolve({
|
||||
@@ -20,12 +20,23 @@ export const sendNetworkRequest = async (item, collection, environment) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment) => {
|
||||
const sendHttpRequest = async (item, collection, environment, collectionVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('send-http-request', item, collection.uid, collection.pathname, environment)
|
||||
.invoke('send-http-request', item, collection.uid, collection.pathname, environment, collectionVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchGqlSchema = async (endpoint, environment) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('fetch-gql-schema', endpoint, environment)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import trim from 'lodash/trim';
|
||||
import each from 'lodash/each';
|
||||
import splitOnFirst from 'split-on-first';
|
||||
import filter from 'lodash/filter';
|
||||
|
||||
const hasLength = (str) => {
|
||||
if(!str || !str.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = str.trim();
|
||||
|
||||
return str.length > 0;
|
||||
};
|
||||
|
||||
export const parseQueryParams = (query) => {
|
||||
if (!query || !query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let params = query.split('&');
|
||||
let result = [];
|
||||
let params = query.split('&').map(param => {
|
||||
let [name, value = ''] = param.split('=');
|
||||
return { name, value };
|
||||
});
|
||||
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
let pair = splitOnFirst(params[i], '=');
|
||||
result.push({
|
||||
name: pair[0],
|
||||
value: pair[1]
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
return filter(params, (p) => hasLength(p.name));
|
||||
};
|
||||
|
||||
export const stringifyQueryParams = (params) => {
|
||||
@@ -36,3 +40,16 @@ export const stringifyQueryParams = (params) => {
|
||||
|
||||
return queryString.join('&');
|
||||
};
|
||||
|
||||
export const splitOnFirst = (str, char) => {
|
||||
if(!str || !str.length) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
let index = str.indexOf(char);
|
||||
if (index === -1) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
};
|
||||
|
||||
70
packages/bruno-app/src/utils/url/index.spec.js
Normal file
70
packages/bruno-app/src/utils/url/index.spec.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { parseQueryParams, splitOnFirst } from './index';
|
||||
|
||||
describe('Url Utils - parseQueryParams', () => {
|
||||
it('should parse query - case 1', () => {
|
||||
const params = parseQueryParams("");
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse query - case 2', () => {
|
||||
const params = parseQueryParams("a");
|
||||
expect(params).toEqual([{name: 'a', value: ''}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 3', () => {
|
||||
const params = parseQueryParams("a=");
|
||||
expect(params).toEqual([{name: 'a', value: ''}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 4', () => {
|
||||
const params = parseQueryParams("a=1");
|
||||
expect(params).toEqual([{name: 'a', value: '1'}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 5', () => {
|
||||
const params = parseQueryParams("a=1&");
|
||||
expect(params).toEqual([{name: 'a', value: '1'}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 6', () => {
|
||||
const params = parseQueryParams("a=1&b");
|
||||
expect(params).toEqual([{name: 'a', value: '1'}, {name: 'b', value: ''}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 7', () => {
|
||||
const params = parseQueryParams("a=1&b=");
|
||||
expect(params).toEqual([{name: 'a', value: '1'}, {name: 'b', value: ''}]);
|
||||
});
|
||||
|
||||
it('should parse query - case 8', () => {
|
||||
const params = parseQueryParams("a=1&b=2");
|
||||
expect(params).toEqual([{name: 'a', value: '1'}, {name: 'b', value: '2'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - splitOnFirst', () => {
|
||||
it('should split on first - case 1', () => {
|
||||
const params = splitOnFirst("a", "=");
|
||||
expect(params).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should split on first - case 2', () => {
|
||||
const params = splitOnFirst("a=", "=");
|
||||
expect(params).toEqual(['a', '']);
|
||||
});
|
||||
|
||||
it('should split on first - case 3', () => {
|
||||
const params = splitOnFirst("a=1", "=");
|
||||
expect(params).toEqual(['a', '1']);
|
||||
});
|
||||
|
||||
it('should split on first - case 4', () => {
|
||||
const params = splitOnFirst("a=1&b=2", "=");
|
||||
expect(params).toEqual(['a', '1&b=2']);
|
||||
});
|
||||
|
||||
it('should split on first - case 5', () => {
|
||||
const params = splitOnFirst("a=1&b=2", "&");
|
||||
expect(params).toEqual(['a=1', 'b=2']);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 953 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,52 +0,0 @@
|
||||
let currentTab = {
|
||||
id: null,
|
||||
url: null,
|
||||
};
|
||||
|
||||
const getExtensionId = () => {
|
||||
const matches = chrome.runtime.getURL('x').match(/.*\/\/(.*)\/x$/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
return chrome.runtime.id;
|
||||
};
|
||||
|
||||
// Create a new tab for the extension
|
||||
function createNewTab() {
|
||||
chrome.tabs.create({ url: 'index.html' }, function (tab) {
|
||||
currentTab = {
|
||||
id: tab.id,
|
||||
url: tab.url
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Focus on the open extension tab
|
||||
function focusTab(tabId) {
|
||||
var updateProperties = { "active": true };
|
||||
chrome.tabs.update(tabId, updateProperties, function (tab) { });
|
||||
}
|
||||
|
||||
// Open the extension tab when the extension icon is clicked
|
||||
chrome.action.onClicked.addListener(function (tab) {
|
||||
if (!currentTab || !currentTab.id) {
|
||||
createNewTab();
|
||||
} else {
|
||||
chrome.tabs.get(currentTab.id, function (tab) {
|
||||
console.log(chrome.runtime.id, tab.url);
|
||||
if (tab && tab.url && tab.url.includes(getExtensionId())) {
|
||||
focusTab(currentTab.id);
|
||||
} else {
|
||||
createNewTab();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// When a tab is closed, check if it is the extension tab that was closed, and unset currentTabId
|
||||
chrome.tabs.onRemoved.addListener(function (tabId) {
|
||||
if (tabId === currentTab.id) {
|
||||
currentTab = {};
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user