mirror of
https://github.com/usebruno/bruno.git
synced 2025-02-23 05:01:43 +01:00
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 { storedTheme } = useTheme();
|
||||||
|
|
||||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||||
console.log('saved auth', awsv4Auth);
|
|
||||||
|
|
||||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
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 { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import ProxySettings from './ProxySettings';
|
import ProxySettings from './ProxySettings';
|
||||||
|
import ClientCertSettings from './ClientCertSettings';
|
||||||
import Headers from './Headers';
|
import Headers from './Headers';
|
||||||
import Auth from './Auth';
|
import Auth from './Auth';
|
||||||
import Script from './Script';
|
import Script from './Script';
|
||||||
@ -28,6 +29,8 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
|
|
||||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||||
|
|
||||||
|
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||||
|
|
||||||
const onProxySettingsUpdate = (config) => {
|
const onProxySettingsUpdate = (config) => {
|
||||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||||
brunoConfig.proxy = config;
|
brunoConfig.proxy = config;
|
||||||
@ -38,6 +41,33 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
.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) => {
|
const getTabPanel = (tab) => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'headers': {
|
case 'headers': {
|
||||||
@ -55,6 +85,15 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||||
}
|
}
|
||||||
|
case 'clientCert': {
|
||||||
|
return (
|
||||||
|
<ClientCertSettings
|
||||||
|
clientCertConfig={clientCertConfig}
|
||||||
|
onUpdate={onClientCertSettingsUpdate}
|
||||||
|
onRemove={onClientCertSettingsRemove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'docs': {
|
case 'docs': {
|
||||||
return <Docs collection={collection} />;
|
return <Docs collection={collection} />;
|
||||||
}
|
}
|
||||||
@ -85,11 +124,16 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
Proxy
|
Proxy
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||||
|
Client Certificates
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||||
Docs
|
Docs
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import Portal from 'components/Portal/index';
|
import Portal from 'components/Portal';
|
||||||
import Modal from 'components/Modal/index';
|
import Modal from 'components/Modal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
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};
|
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.environment-item {
|
.environment-item {
|
||||||
@ -35,7 +36,8 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-create-environment {
|
.btn-create-environment,
|
||||||
|
.btn-import-environment {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -47,6 +49,10 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-import-environment {
|
||||||
|
color: ${(props) => props.theme.colors.text.muted};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default StyledWrapper;
|
export default StyledWrapper;
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||||
import { findEnvironmentInCollection } from 'utils/collections';
|
import { findEnvironmentInCollection } from 'utils/collections';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { toastError } from 'utils/common/error';
|
||||||
import usePrevious from 'hooks/usePrevious';
|
import usePrevious from 'hooks/usePrevious';
|
||||||
import EnvironmentDetails from './EnvironmentDetails';
|
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';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const EnvironmentList = ({ collection }) => {
|
const EnvironmentList = ({ collection }) => {
|
||||||
const { environments } = collection;
|
const { environments } = collection;
|
||||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||||
|
const [openImportModal, setOpenImportModal] = useState(false);
|
||||||
|
|
||||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||||
const prevEnvUids = usePrevious(envUids);
|
const prevEnvUids = usePrevious(envUids);
|
||||||
@ -48,9 +53,10 @@ const EnvironmentList = ({ collection }) => {
|
|||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||||
|
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<div className="environments-sidebar">
|
<div className="environments-sidebar flex flex-col">
|
||||||
{environments &&
|
{environments &&
|
||||||
environments.length &&
|
environments.length &&
|
||||||
environments.map((env) => (
|
environments.map((env) => (
|
||||||
@ -65,6 +71,11 @@ const EnvironmentList = ({ collection }) => {
|
|||||||
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
|
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
|
||||||
+ <span>Create</span>
|
+ <span>Create</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
|
<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 CreateEnvironment from './CreateEnvironment';
|
||||||
import EnvironmentList from './EnvironmentList';
|
import EnvironmentList from './EnvironmentList';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import ImportEnvironment from './ImportEnvironment';
|
||||||
|
|
||||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||||
const { environments } = collection;
|
const { environments } = collection;
|
||||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||||
|
const [openImportModal, setOpenImportModal] = useState(false);
|
||||||
|
|
||||||
if (!environments || !environments.length) {
|
if (!environments || !environments.length) {
|
||||||
return (
|
return (
|
||||||
@ -20,13 +22,23 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
|||||||
hideCancel={true}
|
hideCancel={true}
|
||||||
>
|
>
|
||||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
{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>
|
<p>No environments found!</p>
|
||||||
<button
|
<button
|
||||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||||
onClick={() => setOpenCreateModal(true)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -30,18 +30,16 @@ const Font = ({ close }) => {
|
|||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<label className="block font-medium">Code Editor Font</label>
|
<label className="block font-medium">Code Editor Font</label>
|
||||||
<div className="input-container">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
className="block textbox mt-2 w-full"
|
||||||
className="block textbox mt-2 w-full"
|
autoComplete="off"
|
||||||
autoComplete="off"
|
autoCorrect="off"
|
||||||
autoCorrect="off"
|
autoCapitalize="off"
|
||||||
autoCapitalize="off"
|
spellCheck="false"
|
||||||
spellCheck="false"
|
onChange={handleInputChange}
|
||||||
onChange={handleInputChange}
|
defaultValue={codeFont}
|
||||||
defaultValue={codeFont}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||||
|
@ -8,13 +8,15 @@ const General = ({ close }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
||||||
|
const [timeout, setTimeout] = useState(preferences.request.timeout);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
savePreferences({
|
savePreferences({
|
||||||
...preferences,
|
...preferences,
|
||||||
request: {
|
request: {
|
||||||
sslVerification
|
sslVerification,
|
||||||
|
timeout
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -22,19 +24,37 @@ const General = ({ close }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTimeoutChange = (value) => {
|
||||||
|
const validTimeout = isNaN(Number(value)) ? timeout : Number(value);
|
||||||
|
setTimeout(validTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="flex items-center mt-2">
|
<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
|
<input
|
||||||
id="ssl-verification"
|
id="ssl-cert-verification"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={sslVerification}
|
checked={sslVerification}
|
||||||
onChange={() => setSslVerification(!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>
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
@ -13,7 +13,6 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
|||||||
const { storedTheme } = useTheme();
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
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 handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
const handleSave = () => dispatch(saveRequest(item.uid, 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};
|
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.generate-code-item {
|
.generate-code-item {
|
||||||
|
@ -105,7 +105,7 @@ const Sidebar = () => {
|
|||||||
Star
|
Star
|
||||||
</GitHubButton>
|
</GitHubButton>
|
||||||
</div>
|
</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>
|
</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) => {
|
export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
@ -736,7 +762,7 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer
|
ipcRenderer
|
||||||
.invoke('renderer:copy-environment', collection.pathname, name, baseEnv.variables)
|
.invoke('renderer:create-environment', collection.pathname, name, baseEnv.variables)
|
||||||
.then(
|
.then(
|
||||||
dispatch(
|
dispatch(
|
||||||
updateLastAction({
|
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",
|
"name": "bruno",
|
||||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||||
"homepage": "https://www.usebruno.com",
|
"homepage": "https://www.usebruno.com",
|
||||||
|
@ -135,7 +135,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
});
|
});
|
||||||
|
|
||||||
// create environment
|
// create environment
|
||||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
|
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
|
||||||
try {
|
try {
|
||||||
const envDirPath = path.join(collectionPathname, 'environments');
|
const envDirPath = path.join(collectionPathname, 'environments');
|
||||||
if (!fs.existsSync(envDirPath)) {
|
if (!fs.existsSync(envDirPath)) {
|
||||||
@ -147,31 +147,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
throw new Error(`environment: ${envFilePath} already exists`);
|
throw new Error(`environment: ${envFilePath} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = envJsonToBru({
|
const environment = {
|
||||||
variables: []
|
name: name,
|
||||||
});
|
variables: variables || []
|
||||||
await writeFile(envFilePath, content);
|
};
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// copy environment
|
if (envHasSecrets(environment)) {
|
||||||
ipcMain.handle('renderer:copy-environment', async (event, collectionPathname, name, baseVariables) => {
|
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||||
try {
|
|
||||||
const envDirPath = path.join(collectionPathname, 'environments');
|
|
||||||
if (!fs.existsSync(envDirPath)) {
|
|
||||||
await createDirectory(envDirPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFilePath = path.join(envDirPath, `${name}.bru`);
|
const content = envJsonToBru(environment);
|
||||||
if (fs.existsSync(envFilePath)) {
|
|
||||||
throw new Error(`environment: ${envFilePath} already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = envJsonToBru({
|
|
||||||
variables: baseVariables
|
|
||||||
});
|
|
||||||
await writeFile(envFilePath, content);
|
await writeFile(envFilePath, content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const fs = require('fs');
|
||||||
const qs = require('qs');
|
const qs = require('qs');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fs = require('fs');
|
|
||||||
const decomment = require('decomment');
|
const decomment = require('decomment');
|
||||||
const Mustache = require('mustache');
|
const Mustache = require('mustache');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
@ -100,8 +100,37 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxy configuration
|
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
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 proxyConfig = get(brunoConfig, 'proxy', {});
|
||||||
let proxyEnabled = get(proxyConfig, 'enabled', 'disabled');
|
let proxyEnabled = get(proxyConfig, 'enabled', 'disabled');
|
||||||
if (proxyEnabled === 'global') {
|
if (proxyEnabled === 'global') {
|
||||||
@ -157,6 +186,10 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
|
|||||||
delete request.awsv4config;
|
delete request.awsv4config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preferences = getPreferences();
|
||||||
|
const timeout = get(preferences, 'request.timeout', 0);
|
||||||
|
request.timeout = timeout;
|
||||||
|
|
||||||
return axiosInstance;
|
return axiosInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -516,6 +549,11 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
|
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()) {
|
if (!preferences.isTlsVerification()) {
|
||||||
request.httpsAgent = new https.Agent({
|
request.httpsAgent = new https.Agent({
|
||||||
rejectUnauthorized: false
|
rejectUnauthorized: false
|
||||||
|
@ -11,7 +11,7 @@ const { get } = require('lodash');
|
|||||||
const defaultPreferences = {
|
const defaultPreferences = {
|
||||||
request: {
|
request: {
|
||||||
sslVerification: true,
|
sslVerification: true,
|
||||||
caCert: ''
|
timeout: 0
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
codeFont: 'default'
|
codeFont: 'default'
|
||||||
@ -32,7 +32,8 @@ const defaultPreferences = {
|
|||||||
|
|
||||||
const preferencesSchema = Yup.object().shape({
|
const preferencesSchema = Yup.object().shape({
|
||||||
request: Yup.object().shape({
|
request: Yup.object().shape({
|
||||||
sslVerification: Yup.boolean()
|
sslVerification: Yup.boolean(),
|
||||||
|
timeout: Yup.number()
|
||||||
}),
|
}),
|
||||||
font: Yup.object().shape({
|
font: Yup.object().shape({
|
||||||
codeFont: Yup.string().nullable()
|
codeFont: Yup.string().nullable()
|
||||||
@ -104,8 +105,8 @@ const preferences = {
|
|||||||
return get(getPreferences(), 'request.sslVerification', true);
|
return get(getPreferences(), 'request.sslVerification', true);
|
||||||
},
|
},
|
||||||
|
|
||||||
getCaCert: () => {
|
getTimeout: () => {
|
||||||
return get(getPreferences(), 'request.cacert');
|
return get(getPreferences(), 'request.timeout');
|
||||||
},
|
},
|
||||||
|
|
||||||
getProxyConfig: () => {
|
getProxyConfig: () => {
|
||||||
|
@ -5,6 +5,7 @@ class BrunoRequest {
|
|||||||
this.method = req.method;
|
this.method = req.method;
|
||||||
this.headers = req.headers;
|
this.headers = req.headers;
|
||||||
this.body = req.data;
|
this.body = req.data;
|
||||||
|
this.timeout = req.timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl() {
|
getUrl() {
|
||||||
@ -50,6 +51,14 @@ class BrunoRequest {
|
|||||||
setMaxRedirects(maxRedirects) {
|
setMaxRedirects(maxRedirects) {
|
||||||
this.req.maxRedirects = maxRedirects;
|
this.req.maxRedirects = maxRedirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeout() {
|
||||||
|
return this.req.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(timeout) {
|
||||||
|
this.req.timeout = timeout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = BrunoRequest;
|
module.exports = BrunoRequest;
|
||||||
|
Loading…
Reference in New Issue
Block a user