mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-23 15:19:01 +01:00
Merge branch 'main' into bugfix/fixes-251
This commit is contained in:
commit
b47b0565e5
4
.github/workflows/unit-tests.yml
vendored
4
.github/workflows/unit-tests.yml
vendored
@ -13,6 +13,8 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Check package-lock.json
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
@ -27,5 +29,7 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- name: Test Package bruno-cli
|
||||
run: npm run test --workspace=packages/bruno-cli
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,7 +4,6 @@
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
**English** | [Русский](/contributing_ru.md)
|
||||
|
||||
## Lets make bruno better, together !!
|
||||
|
||||
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
|
||||
@ -19,7 +21,7 @@ Libraries we use
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
|
||||
### Lets start coding
|
||||
|
||||
|
37
contributing_ru.md
Normal file
37
contributing_ru.md
Normal file
@ -0,0 +1,37 @@
|
||||
[English](/contributing.md) | **Русский**
|
||||
|
||||
## Давайте вместе сделаем Бруно лучше!!!
|
||||
|
||||
Я рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере.
|
||||
|
||||
### Стек
|
||||
|
||||
Bruno построен с использованием NextJs и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )
|
||||
|
||||
Библиотеки, которые мы используем
|
||||
|
||||
- CSS - Tailwind
|
||||
- Редакторы кода - Codemirror
|
||||
- Управление состоянием - Redux
|
||||
- Иконки - Tabler Icons
|
||||
- Формы - formik
|
||||
- Валидация схем - Yup
|
||||
- Запросы клиента - axios
|
||||
- Наблюдатель за файловой системой - chokidar
|
||||
|
||||
### Зависимости
|
||||
|
||||
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
|
||||
|
||||
### Приступим к коду
|
||||
|
||||
Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.
|
||||
|
||||
### Создание Pull Request
|
||||
|
||||
- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи
|
||||
- Пожалуйста, соблюдайте формат создания веток
|
||||
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
|
||||
- Пример: feature/dark-mode
|
||||
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
|
||||
- Пример bugfix/bug-1
|
@ -1,3 +1,5 @@
|
||||
**English** | [Русский](/docs/development_ru.md)
|
||||
|
||||
## Development
|
||||
|
||||
Bruno is being 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.
|
||||
|
55
docs/development_ru.md
Normal file
55
docs/development_ru.md
Normal file
@ -0,0 +1,55 @@
|
||||
[English](/docs/development.md) | **Русский**
|
||||
|
||||
## Разработка
|
||||
|
||||
Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение nextjs в одном терминале, а затем запустить приложение electron в другом терминале.
|
||||
|
||||
### Зависимости
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Локальная разработка
|
||||
|
||||
```bash
|
||||
# используйте nodejs 18 версии
|
||||
nvm use
|
||||
|
||||
# установите зависимости
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# билд документации по graphql
|
||||
npm run build:graphql-docs
|
||||
|
||||
# билд bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# запустить next приложение ( терминал 1 )
|
||||
npm run dev:web
|
||||
|
||||
# запустить приложение electron ( терминал 2 )
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Устранение неисправностей
|
||||
|
||||
При запуске `npm install` может возникнуть ошибка `Unsupported platform`. Чтобы исправить это, необходимо удалить `node_modules` и `package-lock.json` и запустить `npm install`. В результате будут установлены все пакеты, необходимые для работы приложения.
|
||||
|
||||
```shell
|
||||
# Удаление node_modules в подкаталогах
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Удаление package-lock в подкаталогах
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
29702
package-lock.json
generated
Normal file
29702
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,6 +38,5 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
|
@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full"
|
||||
className="h-full w-full"
|
||||
aria-label="Code Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
|
@ -0,0 +1,75 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { copyEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const CopyEnvironment = ({ collection, environment, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: environment.name + ' - Copy'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(copyEnvironment(values.name, environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment created in collection');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while created the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
New Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyEnvironment;
|
@ -17,7 +17,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconEdit, IconTrash, IconDatabase } from '@tabler/icons';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
@ -20,6 +22,9 @@ const EnvironmentDetails = ({ environment, collection }) => {
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
@ -27,6 +32,7 @@ const EnvironmentDetails = ({ environment, collection }) => {
|
||||
</div>
|
||||
<div className="flex gap-x-4 pl-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&.modal--animate-out {
|
||||
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
||||
@ -20,6 +22,7 @@ const Wrapper = styled.div`
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bruno-modal-card {
|
||||
|
@ -61,19 +61,20 @@ const Modal = ({
|
||||
children,
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter
|
||||
hideFooter,
|
||||
closeModalFadeTimeout = 500
|
||||
}) => {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const escFunction = (event) => {
|
||||
const escKeyCode = 27;
|
||||
if (event.keyCode === escKeyCode) {
|
||||
closeModal();
|
||||
closeModal({ type: 'esc' });
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
const closeModal = (args) => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => handleCancel(), 500);
|
||||
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -94,12 +95,12 @@ const Modal = ({
|
||||
return (
|
||||
<StyledWrapper className={classes}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
<ModalHeader title={title} handleCancel={() => closeModal()} />
|
||||
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
|
||||
<ModalContent>{children}</ModalContent>
|
||||
<ModalFooter
|
||||
confirmText={confirmText}
|
||||
cancelText={cancelText}
|
||||
handleCancel={() => closeModal()}
|
||||
handleCancel={() => closeModal({ type: 'button' })}
|
||||
handleSubmit={handleConfirm}
|
||||
confirmDisabled={confirmDisabled}
|
||||
hideCancel={hideCancel}
|
||||
@ -108,7 +109,12 @@ const Modal = ({
|
||||
</div>
|
||||
|
||||
{/* Clicking on backdrop closes the modal */}
|
||||
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
|
||||
<div
|
||||
className="bruno-modal-backdrop"
|
||||
onClick={() => {
|
||||
closeModal({ type: 'backdrop' });
|
||||
}}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -197,10 +197,11 @@ const AssertionRow = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assertion.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.auth-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,70 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default AuthMode;
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicAuth;
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = item.draft
|
||||
? get(item, 'draft.request.auth.bearer.token')
|
||||
: get(item, 'request.auth.bearer.token');
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'bearer',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
token: token
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BearerAuth;
|
@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
export default Wrapper;
|
31
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
31
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import AuthMode from './AuthMode';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Auth;
|
@ -116,10 +116,11 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ 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 Auth from 'components/RequestPane/Auth';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
@ -32,7 +33,14 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let { schema, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment);
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url, environment, request, collection);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if (!isSchemaLoading) {
|
||||
@ -90,6 +98,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@ -135,6 +146,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -25,7 +25,7 @@ const useGraphqlSchema = (endpoint, environment) => {
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
fetchGqlSchema(endpoint, environment, request, collection)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
|
@ -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 Auth from 'components/RequestPane/Auth';
|
||||
import AuthMode from 'components/RequestPane/Auth/AuthMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
@ -95,15 +103,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
|
||||
<section
|
||||
className={`flex w-full ${['script', 'vars', 'auth'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}
|
||||
>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
|
@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParam(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -32,6 +32,50 @@ const Wrapper = styled.div`
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltiptext::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
@ -5,8 +5,9 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import { IconDeviceFloppy, IconArrowRight } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
@ -14,6 +15,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
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 isMac = isMacOS();
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
@ -22,7 +25,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onSave = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@ -65,7 +71,25 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
|
||||
<div
|
||||
className="tooltip mr-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="tooltiptext text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -123,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveHeader(header)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -128,10 +128,11 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
const _handleCancel = ({ type }) => {
|
||||
if (type === 'button') {
|
||||
return onCloseWithoutSave();
|
||||
}
|
||||
|
||||
return onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
handleConfirm={onSaveAndClose}
|
||||
handleCancel={_handleCancel}
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
>
|
||||
<div className="font-normal">You have unsaved changes in you request.</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmRequestClose;
|
@ -1,14 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
|
||||
const handleCloseClick = (event) => {
|
||||
event.stopPropagation();
|
||||
@ -21,35 +29,38 @@ const RequestTab = ({ tab, collection }) => {
|
||||
};
|
||||
|
||||
const getMethodColor = (method = '') => {
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
let color = '';
|
||||
method = method.toLocaleLowerCase();
|
||||
|
||||
switch (method) {
|
||||
case 'get': {
|
||||
color = 'var(--color-method-get)';
|
||||
color = theme.request.methods.get;
|
||||
break;
|
||||
}
|
||||
case 'post': {
|
||||
color = 'var(--color-method-post)';
|
||||
color = theme.request.methods.post;
|
||||
break;
|
||||
}
|
||||
case 'put': {
|
||||
color = 'var(--color-method-put)';
|
||||
color = theme.request.methods.put;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
color = 'var(--color-method-delete)';
|
||||
color = theme.request.methods.delete;
|
||||
break;
|
||||
}
|
||||
case 'patch': {
|
||||
color = 'var(--color-method-patch)';
|
||||
color = theme.request.methods.patch;
|
||||
break;
|
||||
}
|
||||
case 'options': {
|
||||
color = 'var(--color-method-options)';
|
||||
color = theme.request.methods.options;
|
||||
break;
|
||||
}
|
||||
case 'head': {
|
||||
color = 'var(--color-method-head)';
|
||||
color = theme.request.methods.head;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -79,6 +90,39 @@ const RequestTab = ({ tab, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
onCancel={() => setShowConfirmClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(
|
||||
deleteRequestDraft({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
})
|
||||
);
|
||||
setShowConfirmClose(false);
|
||||
}}
|
||||
onSaveAndClose={() => {
|
||||
dispatch(saveRequest(item.uid, collection.uid))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
})
|
||||
);
|
||||
setShowConfirmClose(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-baseline tab-label pl-2">
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
||||
{method}
|
||||
@ -87,7 +131,14 @@ const RequestTab = ({ tab, collection }) => {
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
onClick={(e) => {
|
||||
if (!item.draft) return handleCloseClick(e);
|
||||
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
>
|
||||
{!item.draft ? (
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
|
@ -1,6 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
height: 100vh;
|
||||
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};
|
||||
|
||||
div.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -14,6 +19,11 @@ const StyledWrapper = styled.div`
|
||||
padding-top: 20%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
.loading-icon {
|
||||
transform: scaleY(-1);
|
||||
animation: rotateCounterClockwise 1s linear infinite;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -13,17 +13,17 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 px-3 w-full">
|
||||
<StyledWrapper className="px-3 w-full">
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 24, marginLeft: 5, marginRight: 5 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
<StopWatch />
|
||||
</div>
|
||||
</div>
|
||||
<IconRefresh size={24} className="animate-spin" />
|
||||
<IconRefresh size={24} className="loading-icon" />
|
||||
<button
|
||||
onClick={handleCancelRequest}
|
||||
className="mt-4 uppercase btn-md rounded btn-secondary ease-linear transition-all duration-150"
|
||||
className="mt-4 uppercase btn-sm rounded btn-secondary ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
>
|
||||
Cancel Request
|
||||
|
@ -13,13 +13,11 @@ const Placeholder = () => {
|
||||
<div className="px-1 py-2">Send Request</div>
|
||||
<div className="px-1 py-2">New Request</div>
|
||||
<div className="px-1 py-2">Edit Environments</div>
|
||||
<div className="px-1 py-2">Help</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col px-1">
|
||||
<div className="px-1 py-2">Cmd + Enter</div>
|
||||
<div className="px-1 py-2">Cmd + B</div>
|
||||
<div className="px-1 py-2">Cmd + E</div>
|
||||
<div className="px-1 py-2">Cmd + H</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -1,9 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: 1.25rem calc(100% - 1.25rem);
|
||||
|
||||
/* This is a hack to force Codemirror to use all available space */
|
||||
> div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div[role='tablist'] {
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -3,12 +3,57 @@ import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import classnames from 'classnames';
|
||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
|
||||
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const [tab, setTab] = useState('preview');
|
||||
const dispatch = useDispatch();
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType);
|
||||
|
||||
const formatResponse = (data, mode) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (mode.includes('json')) {
|
||||
return safeStringifyJSON(data, true);
|
||||
}
|
||||
|
||||
if (mode.includes('xml')) {
|
||||
let parsed = safeParseXML(data, { collapseContent: true });
|
||||
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(parsed, true);
|
||||
}
|
||||
|
||||
if (['text', 'html'].includes(mode)) {
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data);
|
||||
}
|
||||
|
||||
// final fallback
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data);
|
||||
};
|
||||
|
||||
const value = formatResponse(data, mode);
|
||||
|
||||
const onRun = () => {
|
||||
if (disableRunEventListener) {
|
||||
@ -17,18 +62,52 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
onRun={onRun}
|
||||
value={value || ''}
|
||||
mode={mode}
|
||||
readOnly
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`select-none ${tabName}`, {
|
||||
active: tabName === tab,
|
||||
'cursor-pointer': tabName !== tab
|
||||
});
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
if (!mode.includes('html')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
|
||||
Raw
|
||||
</div>
|
||||
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
||||
Preview
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const activeResult = useMemo(() => {
|
||||
if (tab === 'preview' && mode.includes('html') && item.requestSent && item.requestSent.url) {
|
||||
// Add the Base tag to the head so content loads properly. This also needs the correct CSP settings
|
||||
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent.url}">`);
|
||||
return (
|
||||
<webview
|
||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||
webpreferences="disableDialogs=true, javascript=yes"
|
||||
className="h-full bg-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value} mode={mode} readOnly />;
|
||||
}, [tab, collection, storedTheme, onRun, value, mode]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full h-full" style={{ maxWidth: width }}>
|
||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||
{getTabs()}
|
||||
</div>
|
||||
{activeResult}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ const ResponseSize = ({ size }) => {
|
||||
if (size > 1024) {
|
||||
// size is greater than 1kb
|
||||
let kb = Math.floor(size / 1024);
|
||||
let decimal = ((size % 1024) / 1024).toFixed(2) * 100;
|
||||
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
|
||||
sizeToDisplay = kb + '.' + decimal + 'KB';
|
||||
} else {
|
||||
sizeToDisplay = size + 'B';
|
||||
|
@ -16,7 +16,7 @@ const TestResults = ({ results, assertionResults }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col px-3">
|
||||
<div className="py-2 font-medium test-summary">
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getContentType, formatResponse } from 'utils/common';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
import Overlay from './Overlay';
|
||||
@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? formatResponse(response) : ''}
|
||||
mode={getContentType(response.headers)}
|
||||
data={response.data}
|
||||
headers={response.headers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -62,15 +61,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<Overlay item={item} collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (response.state !== 'success') {
|
||||
if (!item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Placeholder />
|
||||
@ -116,7 +115,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(focusedTab.responsePaneTab)}</section>
|
||||
<section className={`flex flex-grow ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{getTabPanel(focusedTab.responsePaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
disableRunEventListener={true}
|
||||
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||
data={responseReceived.data}
|
||||
headers={responseReceived.headers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -0,0 +1,21 @@
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { HTTPSnippet } from 'httpsnippet';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
|
||||
const CodeView = ({ language, item }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const { target, client, language: lang } = language;
|
||||
let snippet = '';
|
||||
|
||||
try {
|
||||
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snippet = 'Error generating code snippet';
|
||||
}
|
||||
|
||||
return <CodeEditor readOnly value={snippet} theme={storedTheme} mode={lang} />;
|
||||
};
|
||||
|
||||
export default CodeView;
|
@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.generate-code-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.generate-code-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,149 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useState } from 'react';
|
||||
import CodeView from './CodeView';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url/index';
|
||||
import get from 'lodash/get';
|
||||
import handlebars from 'handlebars';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
|
||||
const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
|
||||
if (!url || !url.length || typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = handlebars.compile(url, { noEscape: true });
|
||||
|
||||
return template({
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const languages = [
|
||||
{
|
||||
name: 'HTTP',
|
||||
target: 'http',
|
||||
client: 'http1.1'
|
||||
},
|
||||
{
|
||||
name: 'JavaScript-Fetch',
|
||||
target: 'javascript',
|
||||
client: 'fetch'
|
||||
},
|
||||
{
|
||||
name: 'Javascript-jQuery',
|
||||
target: 'javascript',
|
||||
client: 'jquery'
|
||||
},
|
||||
{
|
||||
name: 'Javascript-axios',
|
||||
target: 'javascript',
|
||||
client: 'axios'
|
||||
},
|
||||
{
|
||||
name: 'Python-Python3',
|
||||
target: 'python',
|
||||
client: 'python3'
|
||||
},
|
||||
{
|
||||
name: 'Python-Requests',
|
||||
target: 'python',
|
||||
client: 'requests'
|
||||
},
|
||||
{
|
||||
name: 'PHP',
|
||||
target: 'php',
|
||||
client: 'curl'
|
||||
},
|
||||
{
|
||||
name: 'Shell-curl',
|
||||
target: 'shell',
|
||||
client: 'curl'
|
||||
},
|
||||
{
|
||||
name: 'Shell-httpie',
|
||||
target: 'shell',
|
||||
client: 'httpie'
|
||||
}
|
||||
];
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const url = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
let envVars = {};
|
||||
if (environment) {
|
||||
const vars = get(environment, 'variables', []);
|
||||
envVars = vars.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url,
|
||||
envVars,
|
||||
collectionVariables: collection.collectionVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
});
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
<StyledWrapper>
|
||||
<div className="flex w-full">
|
||||
<div>
|
||||
<div className="generate-code-sidebar">
|
||||
{languages &&
|
||||
languages.length &&
|
||||
languages.map((language) => (
|
||||
<div
|
||||
key={language.name}
|
||||
className={
|
||||
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
|
||||
}
|
||||
onClick={() => setSelectedLanguage(language)}
|
||||
>
|
||||
<span className="capitalize">{language.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow p-4">
|
||||
{isValidUrl(interpolatedUrl) ? (
|
||||
<CodeView
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
request:
|
||||
item.request.url !== ''
|
||||
? {
|
||||
...item.request,
|
||||
url: interpolatedUrl
|
||||
}
|
||||
: {
|
||||
...item.draft.request,
|
||||
url: interpolatedUrl
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center w-full">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">Invalid URL: {interpolatedUrl}</h1>
|
||||
<p className="text-gray-500">Please check the URL and try again</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateCodeItem;
|
@ -17,7 +17,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -25,13 +25,13 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.request.methods.delete};
|
||||
}
|
||||
.method-patch {
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
color: ${(props) => props.theme.request.methods.patch};
|
||||
}
|
||||
.method-options {
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
color: ${(props) => props.theme.request.methods.options};
|
||||
}
|
||||
.method-head {
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
color: ${(props) => props.theme.request.methods.head};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -15,7 +15,8 @@ const RequestMethod = ({ item }) => {
|
||||
'method-put': method === 'put',
|
||||
'method-delete': method === 'delete',
|
||||
'method-patch': method === 'patch',
|
||||
'method-head': method === 'head'
|
||||
'method-head': method === 'head',
|
||||
'method-options': method == 'options'
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -16,11 +16,12 @@ import RenameCollectionItem from './RenameCollectionItem';
|
||||
import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import GenerateCodeItem from './GenerateCodeItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
@ -113,6 +115,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
setRenameItemModalOpen(true);
|
||||
};
|
||||
|
||||
let indents = range(item.depth);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const isFolder = isItemAFolder(item);
|
||||
@ -142,7 +148,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
@ -166,6 +180,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{runCollectionModalOpen && (
|
||||
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||
)}
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
@ -173,6 +190,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{
|
||||
@ -188,6 +206,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
: null}
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
@ -264,6 +283,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
Clone
|
||||
</div>
|
||||
)}
|
||||
{!isFolder && item.type === 'http-request' && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
Generate Code
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
|
@ -16,7 +16,7 @@ const RenameCollection = ({ collection, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import CollectionProperties from './CollectionProperties';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
|
||||
import RenameCollection from './RenameCollection';
|
||||
@ -69,7 +69,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
};
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
|
@ -1,21 +1,61 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
IconSearch,
|
||||
IconFolders,
|
||||
IconArrowsSort,
|
||||
IconSortAscendingLetters,
|
||||
IconSortDescendingLetters
|
||||
} from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// todo: move this to a separate folder
|
||||
// the coding convention is to keep all the components in a folder named after the component
|
||||
const CollectionsBadge = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||
const sortCollectionOrder = () => {
|
||||
let order;
|
||||
switch (collectionSortOrder) {
|
||||
case 'default':
|
||||
order = 'alphabetical';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
order = 'reverseAlphabetical';
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
order = 'default';
|
||||
break;
|
||||
}
|
||||
dispatch(sortCollections({ order }));
|
||||
};
|
||||
return (
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
<div className="collections-badge flex items-center justify-between px-2">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
{collections.length >= 1 && (
|
||||
<button onClick={() => sortCollectionOrder()}>
|
||||
{collectionSortOrder == 'default' ? (
|
||||
<IconArrowsSort size={18} strokeWidth={1.5} />
|
||||
) : collectionSortOrder == 'alphabetical' ? (
|
||||
<IconSortAscendingLetters size={18} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconSortDescendingLetters size={18} strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -21,17 +21,15 @@ const CreateCollection = ({ onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('collection name is required'),
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string()
|
||||
.min(1, 'location is required')
|
||||
.required('location is required'),
|
||||
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
@ -116,7 +114,10 @@ const CreateCollection = ({ onClose }) => {
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip text="This folder will be created under the selected location" tooltipId="collection-folder-name-tooltip" />
|
||||
<Tooltip
|
||||
text="This folder will be created under the selected location"
|
||||
tooltipId="collection-folder-name-tooltip"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
|
@ -16,7 +16,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionLocation: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(500, 'must be 500 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
@ -16,7 +16,7 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
folderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'folderName',
|
||||
|
@ -25,7 +25,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
requestName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'requestName',
|
||||
|
@ -18,6 +18,7 @@ const TitleBar = () => {
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const handleImportCollection = (collection) => {
|
||||
setImportedCollection(collection);
|
||||
@ -50,6 +51,10 @@ const TitleBar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const openDevTools = () => {
|
||||
ipcRenderer.invoke('renderer:open-devtools');
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 py-2">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
@ -104,6 +109,15 @@ const TitleBar = () => {
|
||||
>
|
||||
Import Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
openDevTools();
|
||||
}}
|
||||
>
|
||||
Devtools
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -96,27 +96,16 @@ const Sidebar = () => {
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
data-color-scheme={storedTheme}
|
||||
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.16.5</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.21.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,6 +66,7 @@ class SingleLineEditor extends Component {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
@ -104,7 +105,9 @@ class SingleLineEditor extends Component {
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
Tab: () => {}
|
||||
// Tabbing disabled to make tabindex work
|
||||
Tab: false,
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
if (this.props.autocomplete) {
|
||||
|
@ -129,6 +129,24 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateClockwise {
|
||||
0% {
|
||||
transform: scaleY(-1) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(-1) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateCounterClockwise {
|
||||
0% {
|
||||
transform: scaleY(-1) rotate(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(-1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
// codemirror
|
||||
.CodeMirror {
|
||||
.cm-variable-valid {
|
||||
|
@ -15,6 +15,7 @@ import 'tailwindcss/dist/tailwind.min.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import '@usebruno/graphql-docs/dist/esm/index.css';
|
||||
|
||||
function SafeHydrate({ children }) {
|
||||
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import useCollectionTreeSync from './useCollectionTreeSync';
|
||||
import useCollectionNextAction from './useCollectionNextAction';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -10,6 +11,7 @@ export const AppContext = React.createContext();
|
||||
export const AppProvider = (props) => {
|
||||
useTelemetry();
|
||||
useCollectionTreeSync();
|
||||
useCollectionNextAction();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { getDefaultRequestPaneTab, findItemInCollectionByPathname } from 'utils/collections/index';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { updateNextAction } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
const useCollectionNextAction = () => {
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
each(collections, (collection) => {
|
||||
if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
|
||||
const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
|
||||
|
||||
if (item) {
|
||||
dispatch(updateNextAction(collection.uid, null));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item.type)
|
||||
})
|
||||
);
|
||||
dispatch(hideHomePage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [collections, each, dispatch, updateNextAction, hideHomePage, addTab]);
|
||||
};
|
||||
|
||||
export default useCollectionNextAction;
|
@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
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';
|
||||
@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
|
||||
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
|
||||
|
||||
const getCurrentCollectionItems = () => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
||||
|
||||
// help (ctrl/cmd + h)
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+h', 'ctrl+h'], (e) => {
|
||||
setShowBrunoSupportModal(true);
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+h', 'ctrl+h']);
|
||||
};
|
||||
}, [setShowNewRequestModal]);
|
||||
|
||||
// close tab hotkey
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||
@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
||||
{showSaveRequestModal && (
|
||||
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
||||
)}
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
getItemsToResequence,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
findCollectionByUid,
|
||||
recursivelyGetAllItemUids,
|
||||
transformRequestToSaveToFilesystem,
|
||||
findParentItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
@ -22,11 +21,12 @@ import {
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
updateLastAction,
|
||||
updateNextAction,
|
||||
resetRunResults,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
@ -39,6 +39,7 @@ import {
|
||||
createCollection as _createCollection,
|
||||
renameCollection as _renameCollection,
|
||||
removeCollection as _removeCollection,
|
||||
sortCollections as _sortCollections,
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||
} from './index';
|
||||
|
||||
@ -81,8 +82,12 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
itemSchema
|
||||
.validate(itemToSave)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
|
||||
.then(() => toast.success('Request saved successfully'))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save request!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -145,6 +150,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
// todo: this can be directly put inside the collections/index.js file
|
||||
// the coding convention is to put only actions that need ipc in this file
|
||||
export const sortCollections = (order) => (dispatch) => {
|
||||
dispatch(_sortCollections(order));
|
||||
};
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@ -262,7 +272,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
||||
ipcRenderer
|
||||
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/251
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@ -346,7 +368,16 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/265
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_deleteItem({ itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
return;
|
||||
@ -569,6 +600,19 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
// the useCollectionNextAction() will track this and open the new request in a new tab
|
||||
// once the request is created
|
||||
dispatch(
|
||||
updateNextAction({
|
||||
nextAction: {
|
||||
type: 'OPEN_REQUEST',
|
||||
payload: {
|
||||
pathname: fullName
|
||||
}
|
||||
},
|
||||
collectionUid
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
@ -586,6 +630,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
|
||||
// the useCollectionNextAction() will track this and open the new request in a new tab
|
||||
// once the request is created
|
||||
dispatch(
|
||||
updateNextAction({
|
||||
nextAction: {
|
||||
type: 'OPEN_REQUEST',
|
||||
payload: {
|
||||
pathname: fullName
|
||||
}
|
||||
},
|
||||
collectionUid
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
@ -620,6 +678,37 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const baseEnv = findEnvironmentInCollection(collection, baseEnvUid);
|
||||
if (!collection) {
|
||||
return reject(new Error('Environmnent not found'));
|
||||
}
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:copy-environment', collection.pathname, name, baseEnv.variables)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const renameEnvironment = (newName, environmentUid, collectionUid) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
|
@ -28,7 +28,8 @@ import os from 'os';
|
||||
const PATH_SEPARATOR = /Windows/i.test(os.release()) ? '\\' : '/';
|
||||
|
||||
const initialState = {
|
||||
collections: []
|
||||
collections: [],
|
||||
collectionSortOrder: 'default'
|
||||
};
|
||||
|
||||
export const collectionsSlice = createSlice({
|
||||
@ -39,13 +40,19 @@ export const collectionsSlice = createSlice({
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
// 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.importedAt = new Date().getTime();
|
||||
collection.lastAction = null;
|
||||
|
||||
// an improvement over the above approach.
|
||||
// this defines an action that need to be performed next and is executed vy the useCollectionNextAction()
|
||||
collection.nextAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
@ -70,6 +77,20 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
sortCollections: (state, action) => {
|
||||
state.collectionSortOrder = action.payload.order;
|
||||
switch (action.payload.order) {
|
||||
case 'default':
|
||||
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
|
||||
break;
|
||||
case 'alphabetical':
|
||||
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
|
||||
break;
|
||||
}
|
||||
},
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@ -78,6 +99,14 @@ export const collectionsSlice = createSlice({
|
||||
collection.lastAction = lastAction;
|
||||
}
|
||||
},
|
||||
updateNextAction: (state, action) => {
|
||||
const { collectionUid, nextAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.nextAction = nextAction;
|
||||
}
|
||||
},
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
const { data: environment, meta } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, meta.collectionUid);
|
||||
@ -229,6 +258,17 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteRequestDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && item.draft) {
|
||||
item.draft = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
newEphemeralHttpRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -307,6 +347,31 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateAuth: (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.auth = item.draft.request.auth || {};
|
||||
switch (action.payload.mode) {
|
||||
case 'bearer':
|
||||
item.draft.request.auth.mode = 'bearer';
|
||||
item.draft.request.auth.bearer = action.payload.content;
|
||||
break;
|
||||
case 'basic':
|
||||
item.draft.request.auth.mode = 'basic';
|
||||
item.draft.request.auth.basic = action.payload.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -563,6 +628,20 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection && collection.items && collection.items.length) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.auth.mode = action.payload.mode;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestBodyMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1035,7 +1114,6 @@ export const collectionsSlice = createSlice({
|
||||
const { cancelTokenUid } = action.payload;
|
||||
item.requestUid = requestUid;
|
||||
item.requestState = 'queued';
|
||||
item.response = null;
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
|
||||
@ -1141,7 +1219,9 @@ export const {
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
updateNextAction,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
@ -1154,10 +1234,12 @@ export const {
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
saveRequest,
|
||||
deleteRequestDraft,
|
||||
newEphemeralHttpRequest,
|
||||
collectionClicked,
|
||||
collectionFolderClicked,
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
@ -1170,6 +1252,7 @@ export const {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam,
|
||||
updateRequestAuthMode,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
|
@ -53,3 +53,9 @@ body::-webkit-scrollbar-thumb,
|
||||
background-color: #cdcdcd;
|
||||
border-radius: 5rem;
|
||||
}
|
||||
|
||||
/* making all the checkboxes and radios bigger */
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
@ -9,13 +9,20 @@ const darkTheme = {
|
||||
green: 'rgb(11 178 126)',
|
||||
danger: '#f06f57',
|
||||
muted: '#9d9d9d',
|
||||
purple: '#cd56d6'
|
||||
purple: '#cd56d6',
|
||||
yellow: '#f59e0b'
|
||||
},
|
||||
bg: {
|
||||
danger: '#d03544'
|
||||
}
|
||||
},
|
||||
|
||||
input: {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
},
|
||||
|
||||
variables: {
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
|
||||
@ -79,7 +86,11 @@ const darkTheme = {
|
||||
get: '#8cd656',
|
||||
post: '#cd56d6',
|
||||
put: '#d69956',
|
||||
delete: '#f06f57'
|
||||
delete: '#f06f57',
|
||||
// customize these colors if needed
|
||||
patch: '#d69956',
|
||||
options: '#d69956',
|
||||
head: '#d69956'
|
||||
}
|
||||
},
|
||||
|
||||
@ -98,7 +109,8 @@ const darkTheme = {
|
||||
responseSendIcon: '#555',
|
||||
responseStatus: '#ccc',
|
||||
responseOk: '#8cd656',
|
||||
responseError: '#f06f57'
|
||||
responseError: '#f06f57',
|
||||
responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
|
||||
},
|
||||
|
||||
collection: {
|
||||
|
@ -9,13 +9,20 @@ const lightTheme = {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563',
|
||||
purple: '#8e44ad'
|
||||
purple: '#8e44ad',
|
||||
yellow: '#d97706'
|
||||
},
|
||||
bg: {
|
||||
danger: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
input: {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
},
|
||||
|
||||
menubar: {
|
||||
bg: 'rgb(44, 44, 44)'
|
||||
},
|
||||
@ -79,7 +86,11 @@ const lightTheme = {
|
||||
get: 'rgb(5, 150, 105)',
|
||||
post: '#8e44ad',
|
||||
put: '#ca7811',
|
||||
delete: 'rgb(185, 28, 28)'
|
||||
delete: 'rgb(185, 28, 28)',
|
||||
// customize these colors if needed
|
||||
patch: '#ca7811',
|
||||
options: '#ca7811',
|
||||
head: '#ca7811'
|
||||
}
|
||||
},
|
||||
|
||||
@ -98,7 +109,8 @@ const lightTheme = {
|
||||
responseSendIcon: 'rgb(209, 213, 219)',
|
||||
responseStatus: 'rgb(117 117 117)',
|
||||
responseOk: '#047857',
|
||||
responseError: 'rgb(185, 28, 28)'
|
||||
responseError: 'rgb(185, 28, 28)',
|
||||
responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
|
||||
},
|
||||
|
||||
collection: {
|
||||
|
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
@ -0,0 +1,71 @@
|
||||
const createContentType = (mode) => {
|
||||
switch (mode) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
case 'formUrlEncoded':
|
||||
return 'application/x-www-form-urlencoded';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
return 'application/json';
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers, mode) => {
|
||||
const contentType = createContentType(mode);
|
||||
const headersArray = headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value
|
||||
};
|
||||
});
|
||||
const headerNames = headersArray.map((header) => header.name);
|
||||
if (!headerNames.includes('Content-Type')) {
|
||||
return [...headersArray, { name: 'Content-Type', value: contentType }];
|
||||
}
|
||||
return headersArray;
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
return queryParams.map((param) => {
|
||||
return {
|
||||
name: param.name,
|
||||
value: param.value
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createPostData = (body) => {
|
||||
const contentType = createContentType(body.mode);
|
||||
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({ name: param.name, value: param.value }))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: body[body.mode]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const buildHarRequest = (request) => {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(request.headers, request.body.mode),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
bodySize: 0
|
||||
};
|
||||
};
|
@ -129,24 +129,28 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
if (targetItem.type === 'folder') {
|
||||
targetItem.items = targetItem.items || [];
|
||||
targetItem.items = sortBy(targetItem.items || [], (item) => item.seq);
|
||||
targetItem.items.push(draggedItem);
|
||||
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
|
||||
} else {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
@ -162,7 +166,9 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) =>
|
||||
return;
|
||||
}
|
||||
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items.push(draggedItem);
|
||||
if (draggedItem.type == 'folder') {
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
|
||||
@ -203,7 +209,7 @@ export const getItemsToResequence = (parent, collection) => {
|
||||
return itemsToResequence;
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
return map(headers, (header) => {
|
||||
return {
|
||||
@ -281,6 +287,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||
},
|
||||
auth: {
|
||||
mode: get(si.draft.request, 'auth.mode', 'none'),
|
||||
basic: {
|
||||
username: get(si.draft.request, 'auth.basic.username', ''),
|
||||
password: get(si.draft.request, 'auth.basic.password', '')
|
||||
},
|
||||
bearer: {
|
||||
token: get(si.draft.request, 'auth.bearer.token', '')
|
||||
}
|
||||
},
|
||||
script: si.draft.request.script,
|
||||
vars: si.draft.request.vars,
|
||||
assertions: si.draft.request.assertions,
|
||||
@ -303,6 +319,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
},
|
||||
auth: {
|
||||
mode: get(si.request, 'auth.mode', 'none'),
|
||||
basic: {
|
||||
username: get(si.request, 'auth.basic.username', ''),
|
||||
password: get(si.request, 'auth.basic.password', '')
|
||||
},
|
||||
bearer: {
|
||||
token: get(si.request, 'auth.bearer.token', '')
|
||||
}
|
||||
},
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
@ -351,6 +377,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
url: _item.request.url,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: _item.request.auth,
|
||||
body: _item.request.body,
|
||||
script: _item.request.script,
|
||||
vars: _item.request.vars,
|
||||
@ -445,6 +472,22 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
return label;
|
||||
};
|
||||
|
||||
export const humanizeRequestAuthMode = (mode) => {
|
||||
let label = 'No Auth';
|
||||
switch (mode) {
|
||||
case 'basic': {
|
||||
label = 'Basic Auth';
|
||||
break;
|
||||
}
|
||||
case 'bearer': {
|
||||
label = 'Bearer Token';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
export const refreshUidsInItem = (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
|
@ -42,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
});
|
||||
};
|
||||
|
||||
export const getCodeMirrorModeBasedOnContentType = (contentType) => {
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return 'application/text';
|
||||
}
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
return 'application/ld+json';
|
||||
} else if (contentType.includes('xml')) {
|
||||
return 'application/xml';
|
||||
} else if (contentType.includes('html')) {
|
||||
return 'application/html';
|
||||
} else if (contentType.includes('text')) {
|
||||
return 'application/text';
|
||||
} else if (contentType.includes('application/edn')) {
|
||||
return 'application/xml';
|
||||
} else if (contentType.includes('yaml')) {
|
||||
return 'application/yaml';
|
||||
} else {
|
||||
return 'application/text';
|
||||
}
|
||||
};
|
||||
|
@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const safeParseXML = (str, options) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
try {
|
||||
return xmlFormat(str, options);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
||||
export const normalizeFileName = (name) => {
|
||||
if (!name) {
|
||||
@ -80,16 +91,6 @@ export const getContentType = (headers) => {
|
||||
return contentType[0];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const formatResponse = (response) => {
|
||||
let type = getContentType(response.headers);
|
||||
if (type.includes('json')) {
|
||||
return safeStringifyJSON(response.data, true);
|
||||
}
|
||||
if (type.includes('xml')) {
|
||||
return xmlFormat(response.data, { collapseContent: true });
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
import platform from 'platform';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@ -33,3 +34,17 @@ export const getDirectoryName = (pathname) => {
|
||||
|
||||
return path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('windows');
|
||||
};
|
||||
|
||||
export const isMacOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('os x');
|
||||
};
|
||||
|
@ -30,14 +30,42 @@ const parseGraphQL = (text) => {
|
||||
}
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request) => {
|
||||
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||
// Check if the request name already exist and if so add a number suffix
|
||||
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||
if (otherItem.name === item.name && otherIndex < index) {
|
||||
nameSuffix++;
|
||||
}
|
||||
return nameSuffix;
|
||||
}, 0);
|
||||
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||
};
|
||||
|
||||
const regexVariable = new RegExp('{{.*?}}', 'g');
|
||||
|
||||
const normalizeVariables = (value) => {
|
||||
const variables = value.match(regexVariable) || [];
|
||||
each(variables, (variable) => {
|
||||
value = value.replace(variable, variable.replace('_.', '').replaceAll(' ', ''));
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: request.name,
|
||||
name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
@ -71,7 +99,22 @@ const transformInsomniaRequestItem = (request) => {
|
||||
});
|
||||
});
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '');
|
||||
const authType = get(request, 'authentication.type', '');
|
||||
|
||||
if (authType === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: normalizeVariables(get(request, 'authentication.username', '')),
|
||||
password: normalizeVariables(get(request, 'authentication.password', ''))
|
||||
};
|
||||
} else if (authType === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: normalizeVariables(get(request, 'authentication.token', ''))
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '').split(';')[0];
|
||||
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
@ -143,14 +186,15 @@ const parseInsomniaCollection = (data) => {
|
||||
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 folders = requestGroups.map((folder, index, allFolder) => {
|
||||
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||
const requests = resources.filter(
|
||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||
);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: folder.name,
|
||||
name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||
};
|
||||
|
@ -1,10 +1,8 @@
|
||||
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, collectionVariables)
|
||||
.then((response) => {
|
||||
const timeEnd = Date.now();
|
||||
resolve({
|
||||
state: 'success',
|
||||
data: response.data,
|
||||
@ -12,7 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
|
||||
size: response.headers['content-length'] || 0,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: timeEnd - timeStart
|
||||
duration: response.duration
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
@ -31,11 +29,11 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchGqlSchema = async (endpoint, environment) => {
|
||||
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('fetch-gql-schema', endpoint, environment).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('fetch-gql-schema', endpoint, environment, request, collection).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => {
|
||||
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
};
|
||||
|
||||
export const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.0
|
||||
|
||||
- feat(#306) Module whitelisting and filesystem access support
|
||||
|
||||
## 0.12.0
|
||||
|
||||
- show response time in milliseconds per request and total
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- fix(#119) Support for Basic and Bearer Auth
|
||||
|
||||
## 0.10.1
|
||||
|
||||
- fix(#233) Fixed Issue related to content header parsing
|
||||
|
@ -1,8 +1,11 @@
|
||||
{
|
||||
"summary": {
|
||||
"totalRequests": 10,
|
||||
"passedRequests": 10,
|
||||
"failedRequests": 0,
|
||||
"totalAssertions": 4,
|
||||
"passedAssertions": 4,
|
||||
"failedAssertions": 0,
|
||||
"passedAssertions": 0,
|
||||
"failedAssertions": 4,
|
||||
"totalTests": 0,
|
||||
"passedTests": 0,
|
||||
"failedTests": 0
|
||||
@ -11,53 +14,33 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v4",
|
||||
"url": "http://localhost:3000/test/v4",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-08gGpUcq2NTnMCVT5AuXxQ0DzGE\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "146",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v4",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /test/v4</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "mTrKBl5YU6jiAVG-phKT4",
|
||||
"uid": "oidgfXLiyD8Jv0NBAHUHF",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
"status": "fail",
|
||||
"error": "expected 404 to equal 200"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
@ -65,53 +48,33 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v2",
|
||||
"url": "http://localhost:3000/test/v2",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-lMqxZgVOJiQXjF5yk3AFEU8O9Ro\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "146",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v2",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /test/v2</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "XsjjGx9cjt5t8tE_t69ZB",
|
||||
"uid": "IgliYuHd9wKp6JNyqyHFK",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
"status": "fail",
|
||||
"error": "expected 404 to equal 200"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
@ -119,53 +82,33 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v3",
|
||||
"url": "http://localhost:3000/test/v3",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-tSiYu0/vWz3r+NYRCaed0aW1waw\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "146",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v3",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /test/v3</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "i_8MmDMtJA9YfvB_FrW15",
|
||||
"uid": "u-3sRebrCyuUbZOkwS0z8",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
"status": "fail",
|
||||
"error": "expected 404 to equal 200"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
@ -173,7 +116,7 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:8080/test/v1",
|
||||
"url": "http://localhost:3000/test/v1",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
@ -181,57 +124,201 @@
|
||||
"test": "hello"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "147",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /test/v1</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "PpKLK6I38I5_ibw4lZqLb",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "eq 200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "fail",
|
||||
"error": "expected 404 to equal 200"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:3000/test",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "144",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /test</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "HEAD",
|
||||
"url": "http://localhost:3000/",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "623",
|
||||
"etag": "W/\"26f-ku5QGz4p9f02u79vJIve7JH3QYM\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "12",
|
||||
"etag": "W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\"",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": ""
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:3000",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "140",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:3000/",
|
||||
"headers": {
|
||||
"content-type": "multipart/form-data; boundary=--------------------------897965859410704836065858"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v1",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"content-length": "16",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "POST",
|
||||
"body": "{\"test\":\"hello\"}",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {},
|
||||
"json": {
|
||||
"test": "hello"
|
||||
}
|
||||
"_overheadLength": 103,
|
||||
"_valueLength": 3,
|
||||
"_valuesToMeasure": [],
|
||||
"writable": false,
|
||||
"readable": true,
|
||||
"dataSize": 0,
|
||||
"maxDataSize": 2097152,
|
||||
"pauseStreams": true,
|
||||
"_released": true,
|
||||
"_streams": [],
|
||||
"_currentStream": null,
|
||||
"_insideLoop": false,
|
||||
"_pendingNext": false,
|
||||
"_boundary": "--------------------------897965859410704836065858",
|
||||
"_events": {},
|
||||
"_eventsCount": 3
|
||||
}
|
||||
},
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "hNBSF_GBdSTFHNiyCcOn9",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "140",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:3000/",
|
||||
"headers": {
|
||||
"content-type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
"data": "a=b&c=d"
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "140",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:3000/test",
|
||||
"headers": {
|
||||
"content-type": "text/xml"
|
||||
},
|
||||
"data": "<xml>\n <test>1</test>\n</xml>"
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"statusText": "Not Found",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-length": "144",
|
||||
"date": "Fri, 29 Sep 2023 00:37:50 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /test</pre>\n</body>\n</html>\n"
|
||||
},
|
||||
"error": null,
|
||||
"assertionResults": [],
|
||||
"testResults": []
|
||||
}
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "0.10.1",
|
||||
"version": "0.13.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
@ -13,6 +13,9 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"bin",
|
||||
@ -21,14 +24,17 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.6.0",
|
||||
"@usebruno/lang": "0.4.0",
|
||||
"@usebruno/js": "0.8.0",
|
||||
"@usebruno/lang": "0.5.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
"decomment": "^0.9.5",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"inquirer": "^9.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
|
@ -12,17 +12,56 @@ const { dotenvToJson } = require('@usebruno/lang');
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
const printRunSummary = (assertionResults, testResults) => {
|
||||
// display assertion results and test results summary
|
||||
const totalAssertions = assertionResults.length;
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass').length;
|
||||
const failedAssertions = totalAssertions - passedAssertions;
|
||||
const printRunSummary = (results) => {
|
||||
let totalRequests = 0;
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let totalAssertions = 0;
|
||||
let passedAssertions = 0;
|
||||
let failedAssertions = 0;
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
|
||||
for (const result of results) {
|
||||
totalRequests += 1;
|
||||
totalTests += result.testResults.length;
|
||||
totalAssertions += result.assertionResults.length;
|
||||
let anyFailed = false;
|
||||
let hasAnyTestsOrAssertions = false;
|
||||
for (const testResult of result.testResults) {
|
||||
hasAnyTestsOrAssertions = true;
|
||||
if (testResult.status === 'pass') {
|
||||
passedTests += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedTests += 1;
|
||||
}
|
||||
}
|
||||
for (const assertionResult of result.assertionResults) {
|
||||
hasAnyTestsOrAssertions = true;
|
||||
if (assertionResult.status === 'pass') {
|
||||
passedAssertions += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
if (!hasAnyTestsOrAssertions && result.error) {
|
||||
failedRequests += 1;
|
||||
} else {
|
||||
passedRequests += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter((result) => result.status === 'pass').length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
const maxLength = 12;
|
||||
|
||||
let requestSummary = `${rpad('Requests:', maxLength)} ${chalk.green(`${passedRequests} passed`)}`;
|
||||
if (failedRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
|
||||
}
|
||||
requestSummary += `, ${totalRequests} total`;
|
||||
|
||||
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
|
||||
if (failedTests > 0) {
|
||||
assertSummary += `, ${chalk.red(`${failedTests} failed`)}`;
|
||||
@ -35,10 +74,14 @@ const printRunSummary = (assertionResults, testResults) => {
|
||||
}
|
||||
testSummary += `, ${totalAssertions} total`;
|
||||
|
||||
console.log('\n' + chalk.bold(assertSummary));
|
||||
console.log('\n' + chalk.bold(requestSummary));
|
||||
console.log(chalk.bold(assertSummary));
|
||||
console.log(chalk.bold(testSummary));
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
@ -255,9 +298,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
|
||||
const _isFile = await isFile(filename);
|
||||
let assertionResults = [];
|
||||
let testResults = [];
|
||||
let testrunResults = [];
|
||||
let results = [];
|
||||
|
||||
let bruJsons = [];
|
||||
|
||||
@ -311,17 +352,12 @@ const handler = async function (argv) {
|
||||
brunoConfig
|
||||
);
|
||||
|
||||
if (result) {
|
||||
testrunResults.push(result);
|
||||
const { assertionResults: _assertionResults, testResults: _testResults } = result;
|
||||
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
const summary = printRunSummary(results);
|
||||
const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0);
|
||||
console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`)));
|
||||
|
||||
if (outputPath && outputPath.length) {
|
||||
const outputDir = path.dirname(outputPath);
|
||||
@ -333,14 +369,14 @@ const handler = async function (argv) {
|
||||
|
||||
const outputJson = {
|
||||
summary,
|
||||
results: testrunResults
|
||||
results
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
|
||||
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
|
||||
}
|
||||
|
||||
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -354,5 +390,6 @@ module.exports = {
|
||||
command,
|
||||
desc,
|
||||
builder,
|
||||
handler
|
||||
handler,
|
||||
printRunSummary
|
||||
};
|
||||
|
55
packages/bruno-cli/src/runner/interpolate-string.js
Normal file
55
packages/bruno-cli/src/runner/interpolate-string.js
Normal file
@ -0,0 +1,55 @@
|
||||
const Handlebars = require('handlebars');
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
|
||||
const interpolateEnvVars = (str, processEnvVars) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
return template({
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
processEnvVars = processEnvVars || {};
|
||||
collectionVariables = collectionVariables || {};
|
||||
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = envVars ? cloneDeep(envVars) : {};
|
||||
|
||||
// envVars can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVars first with processEnvVars
|
||||
forOwn(envVars, (value, key) => {
|
||||
envVars[key] = interpolateEnvVars(value, processEnvVars);
|
||||
});
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
// collectionVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return template(combinedVars);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interpolateString
|
||||
};
|
@ -109,6 +109,18 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
}
|
||||
}
|
||||
|
||||
// todo: we have things happening in two places w.r.t basic auth
|
||||
// need to refactor this in the future
|
||||
// the request.auth (basic auth) object gets set inside the prepare-request.js file
|
||||
if (request.auth) {
|
||||
const username = interpolate(request.auth.username) || '';
|
||||
const password = interpolate(request.auth.password) || '';
|
||||
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
|
||||
const prepareRequest = (request) => {
|
||||
const headers = {};
|
||||
@ -18,6 +19,20 @@ const prepareRequest = (request) => {
|
||||
headers: headers
|
||||
};
|
||||
|
||||
// Authentication
|
||||
if (request.auth) {
|
||||
if (request.auth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
}
|
||||
}
|
||||
|
||||
request.body = request.body || {};
|
||||
|
||||
if (request.body.mode === 'json') {
|
||||
@ -25,7 +40,7 @@ const prepareRequest = (request) => {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
}
|
||||
try {
|
||||
axiosRequest.data = JSON.parse(request.body.json);
|
||||
axiosRequest.data = JSON.parse(decomment(request.body.json));
|
||||
} catch (ex) {
|
||||
axiosRequest.data = request.body.json;
|
||||
}
|
||||
@ -64,7 +79,7 @@ const prepareRequest = (request) => {
|
||||
if (request.body.mode === 'graphql') {
|
||||
const graphqlQuery = {
|
||||
query: get(request, 'body.graphql.query'),
|
||||
variables: JSON.parse(get(request, 'body.graphql.variables') || '{}')
|
||||
variables: JSON.parse(decomment(get(request, 'body.graphql.variables') || '{}'))
|
||||
};
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
|
@ -1,15 +1,19 @@
|
||||
const qs = require('qs');
|
||||
const chalk = require('chalk');
|
||||
const decomment = require('decomment');
|
||||
const fs = require('fs');
|
||||
const { forOwn, each, extend, get } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const axios = require('axios');
|
||||
const https = require('https');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const https = require('https');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { makeAxiosInstance } = require('../utils/axios-instance');
|
||||
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
@ -20,11 +24,13 @@ const runSingleRequest = async function (
|
||||
processEnvVars,
|
||||
brunoConfig
|
||||
) {
|
||||
let request;
|
||||
|
||||
try {
|
||||
let request;
|
||||
|
||||
request = prepareRequest(bruJson.request);
|
||||
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
@ -55,41 +61,17 @@ const runSingleRequest = async function (
|
||||
if (requestScriptFile && requestScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runRequestScript(
|
||||
requestScriptFile,
|
||||
decomment(requestScriptFile),
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
}
|
||||
|
||||
// set proxy if enabled
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
|
||||
const proxyHostname = get(brunoConfig, 'proxy.hostname');
|
||||
const proxyPort = get(brunoConfig, 'proxy.port');
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
|
||||
const proxyConfig = {
|
||||
protocol: proxyProtocol,
|
||||
hostname: proxyHostname,
|
||||
port: proxyPort
|
||||
};
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
|
||||
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
|
||||
proxyConfig.auth = {
|
||||
username: proxyAuthUsername,
|
||||
password: proxyAuthPassword
|
||||
};
|
||||
}
|
||||
|
||||
request.proxy = proxyConfig;
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
|
||||
|
||||
@ -111,7 +93,39 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
// set proxy if enabled
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
let proxy;
|
||||
const interpolationOptions = {
|
||||
envVars: envVariables,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
|
||||
interpolateString;
|
||||
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
|
||||
proxy = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
} else {
|
||||
proxy = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
}
|
||||
|
||||
request.httpsAgent = new HttpsProxyAgent(
|
||||
proxy,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
|
||||
request.httpAgent = new HttpProxyAgent(proxy);
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
@ -122,10 +136,51 @@ const runSingleRequest = async function (
|
||||
request.data = qs.stringify(request.data);
|
||||
}
|
||||
|
||||
// run request
|
||||
const response = await axios(request);
|
||||
let response, responseTime;
|
||||
try {
|
||||
// run request
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`));
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} catch (err) {
|
||||
if (err && err.response) {
|
||||
response = err.response;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: null,
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
@ -147,21 +202,22 @@ const runSingleRequest = async function (
|
||||
if (responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(
|
||||
responseScriptFile,
|
||||
decomment(responseScriptFile),
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -185,17 +241,18 @@ const runSingleRequest = async function (
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if (testFile && testFile.length) {
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = await testRuntime.runTests(
|
||||
testFile,
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
@ -221,102 +278,32 @@ const runSingleRequest = async function (
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
data: response.data,
|
||||
responseTime
|
||||
},
|
||||
error: null,
|
||||
assertionResults,
|
||||
testResults
|
||||
};
|
||||
} catch (err) {
|
||||
if (err && err.response) {
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if (responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(
|
||||
responseScriptFile,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if (assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if (testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = await testRuntime.runTests(
|
||||
testFile,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
if (testResults && testResults.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
}
|
||||
return {
|
||||
request: {
|
||||
method: null,
|
||||
url: null,
|
||||
headers: null,
|
||||
data: null
|
||||
},
|
||||
response: {
|
||||
status: null,
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
42
packages/bruno-cli/src/utils/axios-instance.js
Normal file
42
packages/bruno-cli/src/utils/axios-instance.js
Normal file
@ -0,0 +1,42 @@
|
||||
const axios = require('axios');
|
||||
|
||||
/**
|
||||
* Function that configures axios with timing interceptors
|
||||
* Important to note here that the timings are not completely accurate.
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {import('axios').AxiosStatic}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
/** @type {import('axios').AxiosStatic} */
|
||||
const instance = axios.create();
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers['request-start-time'] = Date.now();
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const end = Date.now();
|
||||
const start = response.config.headers['request-start-time'];
|
||||
response.headers['request-duration'] = end - start;
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
if (error.response) {
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeAxiosInstance
|
||||
};
|
@ -38,6 +38,7 @@ const bruToJson = (bru) => {
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
body: _.get(json, 'body', {}),
|
||||
@ -49,6 +50,7 @@ const bruToJson = (bru) => {
|
||||
};
|
||||
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
|
||||
return transformedJson;
|
||||
} catch (err) {
|
||||
|
67
packages/bruno-cli/tests/commands/run.spec.js
Normal file
67
packages/bruno-cli/tests/commands/run.spec.js
Normal file
@ -0,0 +1,67 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
const { printRunSummary } = require('../../src/commands/run');
|
||||
|
||||
describe('printRunSummary', () => {
|
||||
// Suppress console.log output
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
it('should produce the correct summary for a successful run', () => {
|
||||
const results = [
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'pass' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }],
|
||||
error: null
|
||||
}
|
||||
];
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
|
||||
expect(summary.totalRequests).toBe(2);
|
||||
expect(summary.passedRequests).toBe(2);
|
||||
expect(summary.failedRequests).toBe(0);
|
||||
expect(summary.totalAssertions).toBe(5);
|
||||
expect(summary.passedAssertions).toBe(5);
|
||||
expect(summary.failedAssertions).toBe(0);
|
||||
expect(summary.totalTests).toBe(5);
|
||||
expect(summary.passedTests).toBe(5);
|
||||
expect(summary.failedTests).toBe(0);
|
||||
});
|
||||
|
||||
it('should produce the correct summary for a failed run', () => {
|
||||
const results = [
|
||||
{
|
||||
testResults: [{ status: 'fail' }, { status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'fail' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'fail' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'fail' }, { status: 'fail' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [],
|
||||
assertionResults: [],
|
||||
error: new Error('Request failed')
|
||||
}
|
||||
];
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
|
||||
expect(summary.totalRequests).toBe(3);
|
||||
expect(summary.passedRequests).toBe(2);
|
||||
expect(summary.failedRequests).toBe(1);
|
||||
expect(summary.totalAssertions).toBe(5);
|
||||
expect(summary.passedAssertions).toBe(2);
|
||||
expect(summary.failedAssertions).toBe(3);
|
||||
expect(summary.totalTests).toBe(5);
|
||||
expect(summary.passedTests).toBe(3);
|
||||
expect(summary.failedTests).toBe(2);
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v0.16.5",
|
||||
"version": "v0.21.1",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@ -14,13 +14,14 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.6.0",
|
||||
"@usebruno/lang": "0.4.0",
|
||||
"@usebruno/js": "0.8.0",
|
||||
"@usebruno/lang": "0.5.0",
|
||||
"@usebruno/schema": "0.5.0",
|
||||
"about-window": "^1.15.2",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"decomment": "^0.9.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
@ -30,6 +31,8 @@
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
|
@ -24,7 +24,10 @@ const template = [
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' }
|
||||
{ role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -2,12 +2,10 @@ const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
|
||||
const { hasBruExtension } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
|
||||
const { itemSchema } = require('@usebruno/schema');
|
||||
const { uuid } = require('../utils/common');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { decryptString } = require('../utils/encryption');
|
||||
@ -17,13 +15,6 @@ const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return dirname === collectionPath && basename === 'environments.json';
|
||||
};
|
||||
|
||||
const isDotEnvFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
@ -87,11 +78,6 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
|
||||
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
// migrate old env json to bru file
|
||||
if (isLegacyEnvFile(bruContent)) {
|
||||
bruContent = await migrateLegacyEnvFile(bruContent, pathname);
|
||||
}
|
||||
|
||||
file.data = bruToEnvJson(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
@ -103,7 +89,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
|
||||
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable) {
|
||||
if (variable && secret.value) {
|
||||
variable.value = decryptString(secret.value);
|
||||
}
|
||||
});
|
||||
@ -137,7 +123,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
|
||||
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable) {
|
||||
if (variable && secret.value) {
|
||||
variable.value = decryptString(secret.value);
|
||||
}
|
||||
});
|
||||
@ -205,57 +191,10 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isJsonEnvironmentConfig(pathname, collectionPath)) {
|
||||
try {
|
||||
const dirname = path.dirname(pathname);
|
||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
const jsonData = JSON.parse(bruContent);
|
||||
|
||||
const envDirectory = path.join(dirname, 'environments');
|
||||
if (!fs.existsSync(envDirectory)) {
|
||||
fs.mkdirSync(envDirectory);
|
||||
}
|
||||
|
||||
for (const env of jsonData) {
|
||||
const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
|
||||
const bruContent = envJsonToBru(env);
|
||||
await writeFile(bruEnvFilename, bruContent);
|
||||
}
|
||||
|
||||
await fs.unlinkSync(pathname);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
|
||||
}
|
||||
|
||||
// migrate old json files to bru
|
||||
if (hasJsonExtension(pathname)) {
|
||||
try {
|
||||
const json = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = JSON.parse(json);
|
||||
|
||||
await itemSchema.validate(jsonData);
|
||||
|
||||
const content = jsonToBru(jsonData);
|
||||
|
||||
const re = /(.*)\.json$/;
|
||||
const subst = `$1.bru`;
|
||||
const bruFilename = pathname.replace(re, subst);
|
||||
|
||||
await writeFile(bruFilename, content);
|
||||
await fs.unlinkSync(pathname);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
@ -268,11 +207,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
// migrate old bru format to new bru format
|
||||
if (isLegacyBruFile(bruContent)) {
|
||||
bruContent = await migrateLegacyBruFile(bruContent, pathname);
|
||||
}
|
||||
|
||||
file.data = bruToJson(bruContent);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
@ -404,11 +338,6 @@ class Watcher {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
|
||||
// todo
|
||||
// enable this in a future release
|
||||
// once we can confirm all older json based files have been auto migrated to .bru format
|
||||
// watchPath = path.join(watchPath, '**/*.bru');
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
|
@ -1,6 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const { bruToJsonV2, jsonToBruV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
|
||||
const { each } = require('lodash');
|
||||
|
||||
const bruToEnvJson = (bru) => {
|
||||
try {
|
||||
@ -10,7 +9,7 @@ const bruToEnvJson = (bru) => {
|
||||
// this need to be evaluated and safely removed
|
||||
// i don't see it being used in schema validation
|
||||
if (json && json.variables && json.variables.length) {
|
||||
each(json.variables, (v) => (v.type = 'text'));
|
||||
_.each(json.variables, (v) => (v.type = 'text'));
|
||||
}
|
||||
|
||||
return json;
|
||||
@ -61,6 +60,7 @@ const bruToJson = (bru) => {
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
@ -69,6 +69,7 @@ const bruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
@ -104,10 +105,12 @@ const jsonToBru = (json) => {
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
query: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
|
@ -1,99 +0,0 @@
|
||||
const {
|
||||
bruToEnvJson: bruToEnvJsonV1,
|
||||
bruToJson: bruToJsonV1,
|
||||
|
||||
jsonToBruV2,
|
||||
envJsonToBruV2
|
||||
} = require('@usebruno/lang');
|
||||
const _ = require('lodash');
|
||||
|
||||
const { writeFile } = require('../utils/filesystem');
|
||||
|
||||
const isLegacyEnvFile = (bruContent = '') => {
|
||||
bruContent = bruContent.trim();
|
||||
let regex = /^vars[\s\S]*\/vars$/;
|
||||
|
||||
return regex.test(bruContent);
|
||||
};
|
||||
|
||||
const migrateLegacyEnvFile = async (bruContent, pathname) => {
|
||||
const envJson = bruToEnvJsonV1(bruContent);
|
||||
const newBruContent = envJsonToBruV2(envJson);
|
||||
|
||||
await writeFile(pathname, newBruContent);
|
||||
|
||||
return newBruContent;
|
||||
};
|
||||
|
||||
const isLegacyBruFile = (bruContent = '') => {
|
||||
bruContent = bruContent.trim();
|
||||
let lines = bruContent.split(/\r?\n/);
|
||||
let hasName = false;
|
||||
let hasMethod = false;
|
||||
let hasUrl = false;
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (line.startsWith('name')) {
|
||||
hasName = true;
|
||||
} else if (line.startsWith('method')) {
|
||||
hasMethod = true;
|
||||
} else if (line.startsWith('url')) {
|
||||
hasUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasName && hasMethod && hasUrl;
|
||||
};
|
||||
|
||||
const migrateLegacyBruFile = async (bruContent, pathname) => {
|
||||
const json = bruToJsonV1(bruContent);
|
||||
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
let script = {};
|
||||
let legacyScript = _.get(json, 'request.script');
|
||||
if (legacyScript && legacyScript.trim().length > 0) {
|
||||
script = {
|
||||
res: legacyScript
|
||||
};
|
||||
}
|
||||
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: _.get(json, 'seq')
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
query: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: script,
|
||||
tests: _.get(json, 'request.tests', '')
|
||||
};
|
||||
|
||||
const newBruContent = jsonToBruV2(bruJson);
|
||||
|
||||
await writeFile(pathname, newBruContent);
|
||||
|
||||
return newBruContent;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isLegacyEnvFile,
|
||||
migrateLegacyEnvFile,
|
||||
isLegacyBruFile,
|
||||
migrateLegacyBruFile
|
||||
};
|
@ -9,6 +9,7 @@ const LastOpenedCollections = require('./store/last-opened-collections');
|
||||
const registerNetworkIpc = require('./ipc/network');
|
||||
const registerCollectionsIpc = require('./ipc/collection');
|
||||
const Watcher = require('./app/watcher');
|
||||
const { loadWindowState, saveWindowState } = require('./utils/window');
|
||||
|
||||
const lastOpenedCollections = new LastOpenedCollections();
|
||||
|
||||
@ -16,9 +17,7 @@ setContentSecurityPolicy(`
|
||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src * 'unsafe-inline';
|
||||
base-uri 'none';
|
||||
form-action 'none';
|
||||
img-src 'self' data:image/svg+xml;
|
||||
`);
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
@ -29,14 +28,24 @@ let watcher;
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
const { x, y, width, height } = loadWindowState();
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 768,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
webviewTag: true
|
||||
},
|
||||
title: 'Bruno',
|
||||
icon: path.join(__dirname, 'about/256x256.png')
|
||||
// we will bring this back
|
||||
// see https://github.com/usebruno/bruno/issues/440
|
||||
// autoHideMenuBar: true
|
||||
});
|
||||
|
||||
const url = isDev
|
||||
@ -50,6 +59,9 @@ app.on('ready', async () => {
|
||||
mainWindow.loadURL(url);
|
||||
watcher = new Watcher();
|
||||
|
||||
mainWindow.on('resize', () => saveWindowState(mainWindow));
|
||||
mainWindow.on('move', () => saveWindowState(mainWindow));
|
||||
|
||||
mainWindow.webContents.on('new-window', function (e, url) {
|
||||
e.preventDefault();
|
||||
require('electron').shell.openExternal(url);
|
||||
|
@ -151,6 +151,28 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
// copy environment
|
||||
ipcMain.handle('renderer:copy-environment', async (event, collectionPathname, name, baseVariables) => {
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
if (!fs.existsSync(envDirPath)) {
|
||||
await createDirectory(envDirPath);
|
||||
}
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${name}.bru`);
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} already exists`);
|
||||
}
|
||||
|
||||
const content = envJsonToBru({
|
||||
variables: baseVariables
|
||||
});
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// save environment
|
||||
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
|
||||
try {
|
||||
@ -464,6 +486,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:open-devtools', async () => {
|
||||
mainWindow.webContents.openDevTools();
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user