forked from extern/bruno
Merge branch 'main' into feature/proxy-global-and-collection
# Conflicts: # packages/bruno-app/src/components/Preferences/General/index.js # packages/bruno-electron/src/ipc/network/index.js # packages/bruno-electron/src/store/preferences.js
This commit is contained in:
commit
658a47e03e
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# Description
|
||||
|
||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||
|
||||
# Contribution Checklist:
|
||||
|
||||
- [ ] **The pull request does not introduce any breaking changes**
|
||||
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
||||
- [ ] **Create an issue and link to the pull request.**
|
@ -12,7 +12,6 @@ const AwsV4Auth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
console.log('saved auth', awsv4Auth);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.certificate-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.available-certificates {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
|
||||
button.remove-certificate {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
|
||||
import { useFormik } from 'formik';
|
||||
import { uuid } from 'utils/common';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
domain: '',
|
||||
certFilePath: '',
|
||||
keyFilePath: '',
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string().required(),
|
||||
certFilePath: Yup.string().required(),
|
||||
keyFilePath: Yup.string().required(),
|
||||
passphrase: Yup.string()
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
}
|
||||
});
|
||||
|
||||
const getFile = (e) => {
|
||||
formik.values[e.name] = e.files[0].path;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center font-semibold mt-4 mb-2">
|
||||
<IconCertificate className="mr-1 certificate-icon" size={24} strokeWidth={1.5} /> Client Certificates
|
||||
</div>
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'None'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certicate</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="certFilePath">
|
||||
Cert file
|
||||
</label>
|
||||
<input
|
||||
id="certFilePath"
|
||||
type="file"
|
||||
name="certFilePath"
|
||||
className="block"
|
||||
onChange={(e) => getFile(e.target)}
|
||||
/>
|
||||
{formik.touched.certFilePath && formik.errors.certFilePath ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.certFilePath}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="keyFilePath">
|
||||
Key file
|
||||
</label>
|
||||
<input
|
||||
id="keyFilePath"
|
||||
type="file"
|
||||
name="keyFilePath"
|
||||
className="block"
|
||||
onChange={(e) => getFile(e.target)}
|
||||
/>
|
||||
{formik.touched.keyFilePath && formik.errors.keyFilePath ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.keyFilePath}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="passphrase">
|
||||
Passphrase
|
||||
</label>
|
||||
<input
|
||||
id="passphrase"
|
||||
type="text"
|
||||
name="passphrase"
|
||||
className="block textbox"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.passphrase || ''}
|
||||
/>
|
||||
{formik.touched.passphrase && formik.errors.passphrase ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientCertSettings;
|
@ -7,6 +7,7 @@ import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actio
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ProxySettings from './ProxySettings';
|
||||
import ClientCertSettings from './ClientCertSettings';
|
||||
import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
@ -28,6 +29,8 @@ const CollectionSettings = ({ collection }) => {
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.proxy = config;
|
||||
@ -38,6 +41,33 @@ const CollectionSettings = ({ collection }) => {
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.clientCertificates) {
|
||||
brunoConfig.clientCertificates = {
|
||||
enabled: true,
|
||||
certs: [config]
|
||||
};
|
||||
} else {
|
||||
brunoConfig.clientCertificates.certs.push(config);
|
||||
}
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsRemove = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.clientCertificates = brunoConfig.clientCertificates.filter((item) => item.domain != config.domain);
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'headers': {
|
||||
@ -55,6 +85,15 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'docs': {
|
||||
return <Docs collection={collection} />;
|
||||
}
|
||||
@ -85,11 +124,16 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs'].includes(tab) ? '' : 'mt-4'}`}>{getTabPanel(tab)}</section>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
@ -10,6 +10,7 @@ const StyledWrapper = styled.div`
|
||||
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;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.environment-item {
|
||||
@ -35,7 +36,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-create-environment {
|
||||
.btn-create-environment,
|
||||
.btn-import-environment {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
@ -47,6 +49,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-import-environment {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -1,14 +1,19 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconUpload } from '@tabler/icons';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentList = ({ collection }) => {
|
||||
const { environments } = collection;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
@ -48,9 +53,10 @@ const EnvironmentList = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
<div className="flex">
|
||||
<div>
|
||||
<div className="environments-sidebar">
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
@ -65,6 +71,11 @@ const EnvironmentList = ({ collection }) => {
|
||||
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center btn-import-environment" onClick={() => setOpenImportModal(true)}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
|
||||
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportEnvironment = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}>
|
||||
Postman Environment
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironment;
|
@ -3,10 +3,12 @@ import React, { useState } from 'react';
|
||||
import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironment from './ImportEnvironment';
|
||||
|
||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
@ -20,13 +22,23 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
hideCancel={true}
|
||||
>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
<div className="text-center">
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
<div className="text-center flex flex-col">
|
||||
<p>No environments found!</p>
|
||||
<button
|
||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={() => setOpenCreateModal(true)}
|
||||
>
|
||||
+ <span>Create Environment</span>
|
||||
<span>Create Environment</span>
|
||||
</button>
|
||||
|
||||
<span>Or</span>
|
||||
|
||||
<button
|
||||
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
|
||||
onClick={() => setOpenImportModal(true)}
|
||||
>
|
||||
<span>Import Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -30,18 +30,16 @@ const Font = ({ close }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium">Code Editor Font</label>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleInputChange}
|
||||
defaultValue={codeFont}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleInputChange}
|
||||
defaultValue={codeFont}
|
||||
/>
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
@ -8,13 +8,15 @@ const General = ({ close }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
||||
const [timeout, setTimeout] = useState(preferences.request.timeout);
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
request: {
|
||||
sslVerification
|
||||
sslVerification,
|
||||
timeout
|
||||
}
|
||||
})
|
||||
).then(() => {
|
||||
@ -22,19 +24,37 @@ const General = ({ close }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeoutChange = (value) => {
|
||||
const validTimeout = isNaN(Number(value)) ? timeout : Number(value);
|
||||
setTimeout(validTimeout);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center mt-2">
|
||||
<label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification">
|
||||
TLS Certificate Verification
|
||||
</label>
|
||||
<input
|
||||
id="ssl-verification"
|
||||
id="ssl-cert-verification"
|
||||
type="checkbox"
|
||||
checked={sslVerification}
|
||||
onChange={() => setSslVerification(!sslVerification)}
|
||||
className="mr-3 mousetrap"
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="block font-medium select-none">Request Timeout (in ms)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-1/4"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={(e) => handleTimeoutChange(e.target.value)}
|
||||
defaultValue={timeout === 0 ? '' : timeout}
|
||||
/>
|
||||
<label htmlFor="ssl-verification" className="select-none">
|
||||
TLS Certificate Verification
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
|
@ -13,7 +13,6 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
console.log('saved auth', awsv4Auth);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
@ -9,6 +9,7 @@ const StyledWrapper = styled.div`
|
||||
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;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.generate-code-item {
|
||||
|
@ -105,7 +105,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.24.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.25.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -722,6 +722,32 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const importEnvironment = (name, variables, 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'));
|
||||
}
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name, variables)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
@ -736,7 +762,7 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
|
||||
}
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:copy-environment', collection.pathname, name, baseEnv.variables)
|
||||
.invoke('renderer:create-environment', collection.pathname, name, baseEnv.variables)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
|
@ -0,0 +1,71 @@
|
||||
import each from 'lodash/each';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const isSecret = (type) => {
|
||||
return type === 'secret';
|
||||
};
|
||||
|
||||
const importPostmanEnvironmentVariables = (brunoEnvironment, values) => {
|
||||
brunoEnvironment.variables = brunoEnvironment.variables || [];
|
||||
|
||||
each(values, (i) => {
|
||||
const brunoEnvironmentVariable = {
|
||||
name: i.key,
|
||||
value: i.value,
|
||||
enabled: i.enabled,
|
||||
secret: isSecret(i.type)
|
||||
};
|
||||
|
||||
brunoEnvironment.variables.push(brunoEnvironmentVariable);
|
||||
});
|
||||
};
|
||||
|
||||
const importPostmanEnvironment = (environment) => {
|
||||
const brunoEnvironment = {
|
||||
name: environment.name,
|
||||
variables: []
|
||||
};
|
||||
|
||||
importPostmanEnvironmentVariables(brunoEnvironment, environment.values);
|
||||
return brunoEnvironment;
|
||||
};
|
||||
|
||||
const parsePostmanEnvironment = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let environment = JSON.parse(str);
|
||||
return resolve(importPostmanEnvironment(environment));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof BrunoError) {
|
||||
return reject(err);
|
||||
}
|
||||
return reject(new BrunoError('Unable to parse the postman environment json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importEnvironment = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanEnvironment)
|
||||
.then((environment) => resolve(environment))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import Environment failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importEnvironment;
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v0.24.0",
|
||||
"version": "v0.25.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
@ -135,7 +135,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
// create environment
|
||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
|
||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
if (!fs.existsSync(envDirPath)) {
|
||||
@ -147,31 +147,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`environment: ${envFilePath} already exists`);
|
||||
}
|
||||
|
||||
const content = envJsonToBru({
|
||||
variables: []
|
||||
});
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
const environment = {
|
||||
name: name,
|
||||
variables: variables || []
|
||||
};
|
||||
|
||||
// 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);
|
||||
if (envHasSecrets(environment)) {
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${name}.bru`);
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} already exists`);
|
||||
}
|
||||
const content = envJsonToBru(environment);
|
||||
|
||||
const content = envJsonToBru({
|
||||
variables: baseVariables
|
||||
});
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
@ -1,8 +1,8 @@
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const qs = require('qs');
|
||||
const https = require('https');
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const decomment = require('decomment');
|
||||
const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
@ -100,8 +100,37 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
|
||||
}
|
||||
}
|
||||
|
||||
// proxy configuration
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const interpolationOptions = {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||
|
||||
for (clientCert of clientCertConfig) {
|
||||
const domain = interpolateString(clientCert.domain, interpolationOptions);
|
||||
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
|
||||
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
|
||||
if (domain && certFilePath && keyFilePath) {
|
||||
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
|
||||
if (request.url.match(hostRegex)) {
|
||||
try {
|
||||
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
||||
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
||||
} catch (err) {
|
||||
console.log('Error reading cert/key file', err);
|
||||
}
|
||||
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proxy configuration
|
||||
let proxyConfig = get(brunoConfig, 'proxy', {});
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', 'disabled');
|
||||
if (proxyEnabled === 'global') {
|
||||
@ -157,6 +186,10 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
const preferences = getPreferences();
|
||||
const timeout = get(preferences, 'request.timeout', 0);
|
||||
request.timeout = timeout;
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
@ -516,6 +549,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
|
||||
|
||||
const preferences = getPreferences();
|
||||
const timeout = get(preferences, 'request.timeout', 0);
|
||||
request.timeout = timeout;
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
|
||||
if (!preferences.isTlsVerification()) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
|
@ -11,7 +11,7 @@ const { get } = require('lodash');
|
||||
const defaultPreferences = {
|
||||
request: {
|
||||
sslVerification: true,
|
||||
caCert: ''
|
||||
timeout: 0
|
||||
},
|
||||
font: {
|
||||
codeFont: 'default'
|
||||
@ -32,7 +32,8 @@ const defaultPreferences = {
|
||||
|
||||
const preferencesSchema = Yup.object().shape({
|
||||
request: Yup.object().shape({
|
||||
sslVerification: Yup.boolean()
|
||||
sslVerification: Yup.boolean(),
|
||||
timeout: Yup.number()
|
||||
}),
|
||||
font: Yup.object().shape({
|
||||
codeFont: Yup.string().nullable()
|
||||
@ -104,8 +105,8 @@ const preferences = {
|
||||
return get(getPreferences(), 'request.sslVerification', true);
|
||||
},
|
||||
|
||||
getCaCert: () => {
|
||||
return get(getPreferences(), 'request.cacert');
|
||||
getTimeout: () => {
|
||||
return get(getPreferences(), 'request.timeout');
|
||||
},
|
||||
|
||||
getProxyConfig: () => {
|
||||
|
@ -5,6 +5,7 @@ class BrunoRequest {
|
||||
this.method = req.method;
|
||||
this.headers = req.headers;
|
||||
this.body = req.data;
|
||||
this.timeout = req.timeout;
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
@ -50,6 +51,14 @@ class BrunoRequest {
|
||||
setMaxRedirects(maxRedirects) {
|
||||
this.req.maxRedirects = maxRedirects;
|
||||
}
|
||||
|
||||
getTimeout() {
|
||||
return this.req.timeout;
|
||||
}
|
||||
|
||||
setTimeout(timeout) {
|
||||
this.req.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrunoRequest;
|
||||
|
Loading…
Reference in New Issue
Block a user