-
+
{environments &&
environments.length &&
environments.map((env) => (
@@ -65,6 +71,11 @@ const EnvironmentList = ({ collection }) => {
setOpenCreateModal(true)}>
+ Create
+
+
setOpenImportModal(true)}>
+
+ Import
+
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js
new file mode 100644
index 000000000..5caba79b2
--- /dev/null
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js
@@ -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 (
+
+
+
+
+ Postman Environment
+
+
+
+
+ );
+};
+
+export default ImportEnvironment;
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
index 855e0fb31..6daccc374 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
@@ -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 &&
setOpenCreateModal(false)} />}
-
+ {openImportModal &&
setOpenImportModal(false)} />}
+
No environments found!
+
+
Or
+
+
diff --git a/packages/bruno-app/src/components/Preferences/Font/index.js b/packages/bruno-app/src/components/Preferences/Font/index.js
index bae23e723..2f27fea8b 100644
--- a/packages/bruno-app/src/components/Preferences/Font/index.js
+++ b/packages/bruno-app/src/components/Preferences/Font/index.js
@@ -30,18 +30,16 @@ const Font = ({ close }) => {
return (
-
-
-
+
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 80c823454..38e3c30ed 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -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({
diff --git a/packages/bruno-app/src/utils/importers/postman-environment.js b/packages/bruno-app/src/utils/importers/postman-environment.js
new file mode 100644
index 000000000..61c62311c
--- /dev/null
+++ b/packages/bruno-app/src/utils/importers/postman-environment.js
@@ -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;
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index ffe266f3e..70a74326b 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -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",
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index afddb8696..e3f896763 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -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);
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index a63aba0dc..9562f2cdf 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -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
diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js
index ae462f646..3606086b3 100644
--- a/packages/bruno-electron/src/store/preferences.js
+++ b/packages/bruno-electron/src/store/preferences.js
@@ -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: () => {
diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js
index 099d35111..afbf97873 100644
--- a/packages/bruno-js/src/bruno-request.js
+++ b/packages/bruno-js/src/bruno-request.js
@@ -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;