Environment Name
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
index 7eec1394c..5f4e34d8f 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
@@ -39,6 +39,11 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
+ .tooltip-mod {
+ font-size: 11px !important;
+ width: 150px !important;
+ }
+
input[type='text'] {
width: 100%;
border: solid 1px transparent;
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
index 45a43a6a9..20ac3ee92 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -1,5 +1,6 @@
-import React from 'react';
-import { IconTrash } from '@tabler/icons';
+import React, { useRef, useEffect } from 'react';
+import cloneDeep from 'lodash/cloneDeep';
+import { IconTrash, IconAlertCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
@@ -9,12 +10,13 @@ import { useFormik } from 'formik';
import * as Yup from 'yup';
import { variableNameRegex } from 'utils/common/regex';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
-import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
+import { Tooltip } from 'react-tooltip';
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
+ const addButtonRef = useRef(null);
const formik = useFormik({
enableReinitialize: true,
@@ -58,14 +60,15 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
const ErrorMessage = ({ name }) => {
const meta = formik.getFieldMeta(name);
- if (!meta.error) {
+ const id = uuid();
+ if (!meta.error || !meta.touched) {
return null;
}
-
return (
-
- {meta.error}
-
+
+
+
+
);
};
@@ -85,6 +88,14 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
};
+ useEffect(() => {
+ if (formik.dirty) {
+ // Smooth scrolling to the changed parameter is temporarily disabled
+ // due to UX issues when editing the first row in a long list of environment variables.
+ // addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, [formik.values, formik.dirty]);
+
const handleReset = () => {
formik.resetForm({ originalEnvironmentVariables });
};
@@ -115,19 +126,21 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
/>
-
-
+
+
+
+
@@ -159,11 +172,15 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
))}
-
-
-
- + Add Variable
-
+
+
+ + Add Variable
+
+
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
index 84572db90..3ebcadca1 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
@@ -22,6 +22,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
.required('name is required')
}),
onSubmit: (values) => {
+ if (values.name === environment.name) {
+ return;
+ }
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
.then(() => {
toast.success('Environment renamed successfully');
@@ -50,7 +53,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
handleConfirm={onSubmit}
handleCancel={onClose}
>
-
diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
index c3414170e..17d79629e 100644
--- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
@@ -82,7 +82,6 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
Value
-
) : (
@@ -130,6 +129,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
)
}
collection={collection}
+ item={folder}
/>
diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js
index 966c36b36..9c7dc087d 100644
--- a/packages/bruno-app/src/components/FolderSettings/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/index.js
@@ -7,6 +7,15 @@ import Script from './Script';
import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
+import DotIcon from 'components/Icons/Dot';
+
+const ContentIndicator = () => {
+ return (
+
+
+
+ );
+};
const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -16,6 +25,17 @@ const FolderSettings = ({ collection, folder }) => {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
+ const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
+ const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
+ const hasTests = folderRoot?.request?.tests;
+
+ const headers = folderRoot?.request?.headers || [];
+ const activeHeadersCount = headers.filter((header) => header.enabled).length;
+
+ const requestVars = folderRoot?.request?.vars?.req || [];
+ const responseVars = folderRoot?.request?.vars?.res || [];
+ const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
+
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
@@ -55,15 +75,19 @@ const FolderSettings = ({ collection, folder }) => {
setTab('headers')}>
Headers
+ {activeHeadersCount > 0 && {activeHeadersCount} }
setTab('script')}>
Script
+ {hasScripts && }
setTab('test')}>
Test
+ {hasTests && }
setTab('vars')}>
Vars
+ {activeVarsCount > 0 && {activeVarsCount} }
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/StyledWrapper.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/StyledWrapper.js
new file mode 100644
index 000000000..7504d5c1e
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/StyledWrapper.js
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ .current-environment {
+ }
+ .environment-active {
+ padding: 0.3rem 0.4rem;
+ color: ${(props) => props.theme.colors.text.yellow};
+ border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
+ }
+ .environment-selector {
+ .active: {
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js
new file mode 100644
index 000000000..869978572
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js
@@ -0,0 +1,97 @@
+import React, { useRef, forwardRef, useState } from 'react';
+import find from 'lodash/find';
+import Dropdown from 'components/Dropdown';
+import { IconSettings, IconWorld, IconDatabase, IconDatabaseOff, IconCheck } from '@tabler/icons';
+import EnvironmentSettings from '../EnvironmentSettings';
+import toast from 'react-hot-toast';
+import { useDispatch, useSelector } from 'react-redux';
+import StyledWrapper from './StyledWrapper';
+import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import ToolHint from 'components/ToolHint/index';
+
+const EnvironmentSelector = () => {
+ const dispatch = useDispatch();
+ const dropdownTippyRef = useRef();
+ const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
+ const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
+ const [openSettingsModal, setOpenSettingsModal] = useState(false);
+ const activeEnvironment = activeGlobalEnvironmentUid ? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid) : null;
+
+ const Icon = forwardRef((props, ref) => {
+ return (
+
+
+
+ {
+ activeEnvironment ? {activeEnvironment?.name}
: null
+ }
+
+
+ );
+ });
+
+ const handleSettingsIconClick = () => {
+ setOpenSettingsModal(true);
+ };
+
+ const handleModalClose = () => {
+ setOpenSettingsModal(false);
+ };
+
+ const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
+
+ const onSelect = (environment) => {
+ dispatch(selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null }))
+ .then(() => {
+ if (environment) {
+ toast.success(`Environment changed to ${environment.name}`);
+ } else {
+ toast.success(`No Environments are active now`);
+ }
+ })
+ .catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
+ };
+
+ return (
+
+
+
} placement="bottom-end" transparent={true}>
+
Global Environments
+ {globalEnvironments && globalEnvironments.length
+ ? globalEnvironments.map((e) => (
+
{
+ onSelect(e);
+ dropdownTippyRef.current.hide();
+ }}
+ >
+ {e.name}
+
+ ))
+ : null}
+
{
+ dropdownTippyRef.current.hide();
+ onSelect(null);
+ }}
+ >
+
+ No Environment
+
+
+
+
+ {openSettingsModal && }
+
+ );
+};
+
+export default EnvironmentSelector;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CopyEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CopyEnvironment/index.js
new file mode 100644
index 000000000..ed9ea40b0
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CopyEnvironment/index.js
@@ -0,0 +1,78 @@
+import Modal from 'components/Modal/index';
+import Portal from 'components/Portal/index';
+import { useFormik } from 'formik';
+import { copyGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { useEffect, useRef } from 'react';
+import toast from 'react-hot-toast';
+import { useDispatch } from 'react-redux';
+import * as Yup from 'yup';
+
+const CopyEnvironment = ({ 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(copyGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))
+ .then(() => {
+ toast.success('Global environment created!');
+ onClose();
+ })
+ .catch((error) => {
+ toast.error('An error occurred while created the environment');
+ console.error(error);
+ });
+ }
+ });
+
+ useEffect(() => {
+ if (inputRef && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [inputRef]);
+
+ const onSubmit = () => {
+ formik.handleSubmit();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default CopyEnvironment;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js
new file mode 100644
index 000000000..3103f9b4e
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js
@@ -0,0 +1,83 @@
+import React, { useEffect, useRef } from 'react';
+import toast from 'react-hot-toast';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { useDispatch } from 'react-redux';
+import Portal from 'components/Portal';
+import Modal from 'components/Modal';
+import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+
+const CreateEnvironment = ({ onClose }) => {
+ const dispatch = useDispatch();
+ const inputRef = useRef();
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: {
+ name: ''
+ },
+ 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(addGlobalEnvironment({ name: values.name }))
+ .then(() => {
+ toast.success('Global environment created!');
+ onClose();
+ })
+ .catch(() => toast.error('An error occurred while creating the environment'));
+ }
+ });
+
+ useEffect(() => {
+ if (inputRef && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [inputRef]);
+
+ const onSubmit = () => {
+ formik.handleSubmit();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default CreateEnvironment;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/StyledWrapper.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/StyledWrapper.js
new file mode 100644
index 000000000..48b874214
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/StyledWrapper.js
@@ -0,0 +1,15 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ button.submit {
+ color: white;
+ background-color: var(--color-background-danger) !important;
+ border: inherit !important;
+
+ &:hover {
+ border: inherit !important;
+ }
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/index.js
new file mode 100644
index 000000000..2edab3be8
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/DeleteEnvironment/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import Portal from 'components/Portal/index';
+import toast from 'react-hot-toast';
+import Modal from 'components/Modal/index';
+import { useDispatch } from 'react-redux';
+import StyledWrapper from './StyledWrapper';
+import { deleteGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+
+const DeleteEnvironment = ({ onClose, environment }) => {
+ const dispatch = useDispatch();
+ const onConfirm = () => {
+ dispatch(deleteGlobalEnvironment({ environmentUid: environment.uid }))
+ .then(() => {
+ toast.success('Global Environment deleted successfully');
+ onClose();
+ })
+ .catch(() => toast.error('An error occurred while deleting the environment'));
+ };
+
+ return (
+
+
+
+ Are you sure you want to delete {environment.name} ?
+
+
+
+ );
+};
+
+export default DeleteEnvironment;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/ConfirmSwitchEnv.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/ConfirmSwitchEnv.js
new file mode 100644
index 000000000..715bf9e75
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/ConfirmSwitchEnv.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { IconAlertTriangle } from '@tabler/icons';
+import Modal from 'components/Modal';
+import { createPortal } from 'react-dom';
+
+const ConfirmSwitchEnv = ({ onCancel }) => {
+ return createPortal(
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ hideFooter={true}
+ >
+
+
+
Hold on..
+
+ You have unsaved changes in this environment.
+
+
+ ,
+ document.body
+ );
+};
+
+export default ConfirmSwitchEnv;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
new file mode 100644
index 000000000..5f4e34d8f
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
@@ -0,0 +1,66 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-weight: 600;
+ table-layout: fixed;
+
+ thead,
+ td {
+ border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
+ padding: 4px 10px;
+
+ &:nth-child(1),
+ &:nth-child(4) {
+ width: 70px;
+ }
+ &:nth-child(5) {
+ width: 40px;
+ }
+
+ &:nth-child(2) {
+ width: 25%;
+ }
+ }
+
+ thead {
+ color: ${(props) => props.theme.table.thead.color};
+ font-size: 0.8125rem;
+ user-select: none;
+ }
+ thead td {
+ padding: 6px 10px;
+ }
+ }
+
+ .btn-add-param {
+ font-size: 0.8125rem;
+ }
+
+ .tooltip-mod {
+ font-size: 11px !important;
+ width: 150px !important;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ border: solid 1px transparent;
+ outline: none !important;
+ background-color: transparent;
+
+ &:focus {
+ outline: none !important;
+ border: solid 1px transparent;
+ }
+ }
+
+ input[type='checkbox'] {
+ cursor: pointer;
+ position: relative;
+ top: 1px;
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
new file mode 100644
index 000000000..10ab9fba3
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -0,0 +1,199 @@
+import React, { useRef, useEffect } from 'react';
+import cloneDeep from 'lodash/cloneDeep';
+import { IconTrash, IconAlertCircle } from '@tabler/icons';
+import { useTheme } from 'providers/Theme';
+import { useDispatch } from 'react-redux';
+import SingleLineEditor from 'components/SingleLineEditor';
+import StyledWrapper from './StyledWrapper';
+import { uuid } from 'utils/common';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { variableNameRegex } from 'utils/common/regex';
+import toast from 'react-hot-toast';
+import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { Tooltip } from 'react-tooltip';
+
+const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables }) => {
+ const dispatch = useDispatch();
+ const { storedTheme } = useTheme();
+ const addButtonRef = useRef(null);
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: environment.variables || [],
+ validationSchema: Yup.array().of(
+ Yup.object({
+ enabled: Yup.boolean(),
+ name: Yup.string()
+ .required('Name cannot be empty')
+ .matches(
+ variableNameRegex,
+ 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
+ )
+ .trim(),
+ secret: Yup.boolean(),
+ type: Yup.string(),
+ uid: Yup.string(),
+ value: Yup.string().trim().nullable()
+ })
+ ),
+ onSubmit: (values) => {
+ if (!formik.dirty) {
+ toast.error('Nothing to save');
+ return;
+ }
+
+ dispatch(saveGlobalEnvironment({ environmentUid: environment.uid, variables: cloneDeep(values) }))
+ .then(() => {
+ toast.success('Changes saved successfully');
+ formik.resetForm({ values });
+ setIsModified(false);
+ })
+ .catch((error) => {
+ console.error(error);
+ toast.error('An error occurred while saving the changes')
+ });
+ }
+ });
+
+ // Effect to track modifications.
+ React.useEffect(() => {
+ setIsModified(formik.dirty);
+ }, [formik.dirty]);
+
+ const ErrorMessage = ({ name }) => {
+ const meta = formik.getFieldMeta(name);
+ const id = uuid();
+ if (!meta.error || !meta.touched) {
+ return null;
+ }
+ return (
+
+
+
+
+ );
+ };
+
+ const addVariable = () => {
+ const newVariable = {
+ uid: uuid(),
+ name: '',
+ value: '',
+ type: 'text',
+ secret: false,
+ enabled: true
+ };
+ formik.setFieldValue(formik.values.length, newVariable, false);
+ };
+
+ const handleRemoveVar = (id) => {
+ formik.setValues(formik.values.filter((variable) => variable.uid !== id));
+ };
+
+ useEffect(() => {
+ if (formik.dirty) {
+ // Smooth scrolling to the changed parameter is temporarily disabled
+ // due to UX issues when editing the first row in a long list of environment variables.
+ // addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, [formik.values, formik.dirty]);
+
+ const handleReset = () => {
+ formik.resetForm({ originalEnvironmentVariables });
+ };
+
+ return (
+
+
+
+
+
+ + Add Variable
+
+
+
+
+
+
+ Save
+
+
+ Reset
+
+
+
+ );
+};
+export default EnvironmentVariables;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js
new file mode 100644
index 000000000..e4dba0e9c
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js
@@ -0,0 +1,46 @@
+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, setIsModified }) => {
+ const [openEditModal, setOpenEditModal] = useState(false);
+ const [openDeleteModal, setOpenDeleteModal] = useState(false);
+ const [openCopyModal, setOpenCopyModal] = useState(false);
+
+ return (
+
+ {openEditModal && (
+
setOpenEditModal(false)} environment={environment} />
+ )}
+ {openDeleteModal && (
+ setOpenDeleteModal(false)}
+ environment={environment}
+ />
+ )}
+ {openCopyModal && (
+ setOpenCopyModal(false)} environment={environment} />
+ )}
+
+
+
+ {environment.name}
+
+
+ setOpenEditModal(true)} />
+ setOpenCopyModal(true)} />
+ setOpenDeleteModal(true)} />
+
+
+
+
+
+
+
+ );
+};
+
+export default EnvironmentDetails;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/StyledWrapper.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/StyledWrapper.js
new file mode 100644
index 000000000..330ae082c
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/StyledWrapper.js
@@ -0,0 +1,58 @@
+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};
+
+ .environments-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;
+ height: 100%;
+ max-height: 85vh;
+ overflow-y: auto;
+ }
+
+ .environment-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;
+ }
+ }
+
+ .btn-create-environment,
+ .btn-import-environment {
+ padding: 8px 10px;
+ cursor: pointer;
+ border-bottom: none;
+ color: ${(props) => props.theme.textLink};
+
+ span:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .btn-import-environment {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js
new file mode 100644
index 000000000..ebd953a0d
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js
@@ -0,0 +1,149 @@
+import React, { useEffect, useState } from 'react';
+import usePrevious from 'hooks/usePrevious';
+import EnvironmentDetails from './EnvironmentDetails';
+import CreateEnvironment from '../CreateEnvironment';
+import { IconDownload, IconShieldLock } from '@tabler/icons';
+import StyledWrapper from './StyledWrapper';
+import ConfirmSwitchEnv from './ConfirmSwitchEnv';
+import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
+import ImportEnvironment from '../ImportEnvironment';
+import { isEqual } from 'lodash';
+
+const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified }) => {
+ const [openCreateModal, setOpenCreateModal] = useState(false);
+ const [openImportModal, setOpenImportModal] = useState(false);
+ const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
+
+ const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);
+ const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);
+
+ const envUids = environments ? environments.map((env) => env.uid) : [];
+ const prevEnvUids = usePrevious(envUids);
+
+ useEffect(() => {
+ if (!environments?.length) {
+ setSelectedEnvironment(null);
+ setOriginalEnvironmentVariables([]);
+ return;
+ }
+
+ if (selectedEnvironment) {
+ const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
+ const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
+ if (hasSelectedEnvironmentChanged) {
+ setSelectedEnvironment(_selectedEnvironment);
+ }
+ setOriginalEnvironmentVariables(selectedEnvironment.variables);
+ return;
+ }
+
+ const environment = environments?.find(env => env.uid === activeEnvironmentUid) || environments?.[0];
+
+ setSelectedEnvironment(environment);
+ setOriginalEnvironmentVariables(environment?.variables || []);
+ }, [environments, activeEnvironmentUid]);
+
+
+ useEffect(() => {
+ if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
+ const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
+ if (newEnv) {
+ setSelectedEnvironment(newEnv);
+ }
+ }
+
+ if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
+ setSelectedEnvironment(environments && environments.length ? environments[0] : null);
+ }
+ }, [envUids, environments, prevEnvUids]);
+
+ const handleEnvironmentClick = (env) => {
+ if (!isModified) {
+ setSelectedEnvironment(env);
+ } else {
+ setSwitchEnvConfirmClose(true);
+ }
+ };
+
+ if (!selectedEnvironment) {
+ return null;
+ }
+
+ const handleCreateEnvClick = () => {
+ if (!isModified) {
+ setOpenCreateModal(true);
+ } else {
+ setSwitchEnvConfirmClose(true);
+ }
+ };
+
+ const handleImportClick = () => {
+ if (!isModified) {
+ setOpenImportModal(true);
+ } else {
+ setSwitchEnvConfirmClose(true);
+ }
+ };
+
+ const handleSecretsClick = () => {
+ setOpenManageSecretsModal(true);
+ };
+
+ const handleConfirmSwitch = (saveChanges) => {
+ if (!saveChanges) {
+ setSwitchEnvConfirmClose(false);
+ }
+ };
+
+ return (
+
+ {openCreateModal && setOpenCreateModal(false)} />}
+ {openImportModal && setOpenImportModal(false)} />}
+ {openManageSecretsModal && setOpenManageSecretsModal(false)} />}
+
+
+
+ {switchEnvConfirmClose && (
+
+ handleConfirmSwitch(false)} />
+
+ )}
+
+ {environments &&
+ environments.length &&
+ environments.map((env) => (
+
handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
+ >
+ {env.name}
+
+ ))}
+
handleCreateEnvClick()}>
+ + Create
+
+
+
+
handleImportClick()}>
+
+ Import
+
+
handleSecretsClick()}>
+
+ Managing Secrets
+
+
+
+
+
+
+
+ );
+};
+
+export default EnvironmentList;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js
new file mode 100644
index 000000000..99900f740
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import Portal from 'components/Portal';
+import Modal from 'components/Modal';
+import toast from 'react-hot-toast';
+import { useDispatch } from 'react-redux';
+import importPostmanEnvironment from 'utils/importers/postman-environment';
+import { toastError } from 'utils/common/error';
+import { IconDatabaseImport } from '@tabler/icons';
+import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { uuid } from 'utils/common/index';
+
+const ImportEnvironment = ({ onClose }) => {
+ const dispatch = useDispatch();
+
+ const handleImportPostmanEnvironment = () => {
+ importPostmanEnvironment()
+ .then((environments) => {
+ environments
+ .filter((env) =>
+ env.name && env.name !== 'undefined'
+ ? true
+ : () => {
+ toast.error('Failed to import environment: env has no name');
+ return false;
+ }
+ )
+ .map((environment) => {
+ let variables = environment?.variables?.map(v => ({
+ ...v,
+ uid: uuid(),
+ type: 'text'
+ }));
+ dispatch(addGlobalEnvironment({ name: environment.name, variables }))
+ .then(() => {
+ toast.success('Global Environment imported successfully');
+ })
+ .catch(() => toast.error('An error occurred while importing the environment'));
+ });
+ })
+ .then(() => {
+ onClose();
+ })
+ .catch((err) => toastError(err, 'Postman Import environment failed'));
+ };
+
+ return (
+
+
+
+
+ Import your Postman environments
+
+
+
+ );
+};
+
+export default ImportEnvironment;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js
new file mode 100644
index 000000000..ff1809383
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js
@@ -0,0 +1,88 @@
+import React, { useEffect, useRef } from 'react';
+import Portal from 'components/Portal/index';
+import Modal from 'components/Modal/index';
+import toast from 'react-hot-toast';
+import { useFormik } from 'formik';
+import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
+import * as Yup from 'yup';
+import { useDispatch } from 'react-redux';
+import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+
+const RenameEnvironment = ({ onClose, environment }) => {
+ const dispatch = useDispatch();
+ const inputRef = useRef();
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: {
+ name: environment.name
+ },
+ 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) => {
+ if (values.name === environment.name) {
+ return;
+ }
+ dispatch(renameGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))
+ .then(() => {
+ toast.success('Environment renamed successfully');
+ onClose();
+ })
+ .catch((error) => {
+ toast.error('An error occurred while renaming the environment');
+ console.error(error);
+ });
+ }
+ });
+
+ useEffect(() => {
+ if (inputRef && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [inputRef]);
+
+ const onSubmit = () => {
+ formik.handleSubmit();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default RenameEnvironment;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/StyledWrapper.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/StyledWrapper.js
new file mode 100644
index 000000000..2dfad0cfe
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/StyledWrapper.js
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ button.btn-create-environment {
+ &:hover {
+ span {
+ text-decoration: underline;
+ }
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js
new file mode 100644
index 000000000..551a4ec58
--- /dev/null
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js
@@ -0,0 +1,78 @@
+import Modal from 'components/Modal/index';
+import React, { useState } from 'react';
+import CreateEnvironment from './CreateEnvironment';
+import EnvironmentList from './EnvironmentList';
+import StyledWrapper from './StyledWrapper';
+import { IconFileAlert } from '@tabler/icons';
+import ImportEnvironment from './ImportEnvironment/index';
+
+export const SharedButton = ({ children, className, onClick }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DefaultTab = ({ setTab }) => {
+ return (
+
+
+
No Global Environments found
+
+ setTab('create')}>
+ Create Global Environment
+
+
+ Or
+
+ setTab('import')}>
+ Import Environment
+
+
+
+ );
+};
+
+const EnvironmentSettings = ({ globalEnvironments, activeGlobalEnvironmentUid, onClose }) => {
+ const [isModified, setIsModified] = useState(false);
+ const environments = globalEnvironments;
+ const [selectedEnvironment, setSelectedEnvironment] = useState(null);
+ const [tab, setTab] = useState('default');
+ if (!environments || !environments.length) {
+ return (
+
+
+ {tab === 'create' ? (
+ setTab('default')} />
+ ) : tab === 'import' ? (
+ setTab('default')} />
+ ) : (
+ <>>
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default EnvironmentSettings;
diff --git a/packages/bruno-app/src/components/MarkDown/index.jsx b/packages/bruno-app/src/components/MarkDown/index.jsx
index 3c778c5a6..e6582a8b6 100644
--- a/packages/bruno-app/src/components/MarkDown/index.jsx
+++ b/packages/bruno-app/src/components/MarkDown/index.jsx
@@ -2,6 +2,7 @@ import MarkdownIt from 'markdown-it';
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
import StyledWrapper from './StyledWrapper';
import React from 'react';
+import { isValidUrl } from 'utils/url/index';
const Markdown = ({ collectionPath, onDoubleClick, content }) => {
const markdownItOptions = {
@@ -15,7 +16,7 @@ const Markdown = ({ collectionPath, onDoubleClick, content }) => {
if (target.tagName === 'A') {
event.preventDefault();
const href = target.getAttribute('href');
- if (href) {
+ if (href && isValidUrl(href)) {
window.open(href, '_blank');
return;
}
diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js
index b83549aac..3ee08cbb9 100644
--- a/packages/bruno-app/src/components/Modal/index.js
+++ b/packages/bruno-app/src/components/Modal/index.js
@@ -1,5 +1,9 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import StyledWrapper from './StyledWrapper';
+import useFocusTrap from 'hooks/useFocusTrap';
+
+const ESC_KEY_CODE = 27;
+const ENTER_KEY_CODE = 13;
const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
@@ -69,25 +73,35 @@ const Modal = ({
onClick,
closeModalFadeTimeout = 500
}) => {
+ const modalRef = useRef(null);
const [isClosing, setIsClosing] = useState(false);
- const escFunction = (event) => {
- const escKeyCode = 27;
- if (event.keyCode === escKeyCode) {
- closeModal({ type: 'esc' });
+
+ const handleKeydown = (event) => {
+ const { keyCode, shiftKey, ctrlKey, altKey, metaKey } = event;
+ switch (keyCode) {
+ case ESC_KEY_CODE: {
+ if (disableEscapeKey) return;
+ return closeModal({ type: 'esc' });
+ }
+ case ENTER_KEY_CODE: {
+ if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm) {
+ return handleConfirm();
+ }
+ }
}
};
+ useFocusTrap(modalRef);
+
const closeModal = (args) => {
setIsClosing(true);
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
};
useEffect(() => {
- if (disableEscapeKey) return;
- document.addEventListener('keydown', escFunction, false);
-
+ document.addEventListener('keydown', handleKeydown, false);
return () => {
- document.removeEventListener('keydown', escFunction, false);
+ document.removeEventListener('keydown', handleKeydown);
};
}, [disableEscapeKey, document]);
@@ -100,7 +114,13 @@ const Modal = ({
}
return (
onClick(e) : null}>
-
+
{
- const [modalOpen, setModalOpen] = useState(false);
-
- const menuDropdownTippyRef = useRef();
- const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
- const MenuIcon = forwardRef((props, ref) => {
- return (
-
-
-
- );
- });
-
- return (
-
-
- Collections
- {/* */}
-
-
-
} placement="bottom-start">
-
{
- menuDropdownTippyRef.current.hide();
- setModalOpen(true);
- }}
- >
- Create Collection
-
-
{
- menuDropdownTippyRef.current.hide();
- }}
- >
- Import Collection
-
-
{
- menuDropdownTippyRef.current.hide();
- }}
- >
- Settings
-
-
-
-
- );
-};
-
-export default Navbar;
diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js
index 245538541..ba257bf48 100644
--- a/packages/bruno-app/src/components/Notifications/index.js
+++ b/packages/bruno-app/src/components/Notifications/index.js
@@ -93,10 +93,12 @@ const Notifications = () => {
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
+ aria-label="Check all Notifications"
>
-
+
0 ? 'bell' : ''}`}
/>
@@ -133,8 +135,9 @@ const Notifications = () => {
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
{notification?.title}
@@ -144,8 +147,9 @@ const Notifications = () => {
{'Prev'}
@@ -161,8 +165,9 @@ const Notifications = () => {
{'Next'}
diff --git a/packages/bruno-app/src/components/Preferences/Font/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Display/Font/StyledWrapper.js
similarity index 100%
rename from packages/bruno-app/src/components/Preferences/Font/StyledWrapper.js
rename to packages/bruno-app/src/components/Preferences/Display/Font/StyledWrapper.js
diff --git a/packages/bruno-app/src/components/Preferences/Font/index.js b/packages/bruno-app/src/components/Preferences/Display/Font/index.js
similarity index 93%
rename from packages/bruno-app/src/components/Preferences/Font/index.js
rename to packages/bruno-app/src/components/Preferences/Display/Font/index.js
index ef6ac9f2f..622ea0817 100644
--- a/packages/bruno-app/src/components/Preferences/Font/index.js
+++ b/packages/bruno-app/src/components/Preferences/Display/Font/index.js
@@ -39,7 +39,7 @@ const Font = ({ close }) => {
- Code Editor Font
+ Code Editor Font
{
/>
-
Font Size
+
Font Size
{
+ return (
+
+
+
+ Theme
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Display;
diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js
index 9855c2747..0d26c955d 100644
--- a/packages/bruno-app/src/components/Preferences/General/index.js
+++ b/packages/bruno-app/src/components/Preferences/General/index.js
@@ -100,7 +100,7 @@ const General = ({ close }) => {
return (
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index 51d3194be..e1f4b6fda 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -20,6 +20,8 @@ import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings';
+import { getGlobalEnvironmentVariables } from 'utils/collections/index';
+import { produce } from 'immer';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
@@ -32,11 +34,25 @@ const RequestTabPanel = () => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
- const collections = useSelector((state) => state.collections.collections);
- const screenWidth = useSelector((state) => state.app.screenWidth);
-
- let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
+ const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
+ const _collections = useSelector((state) => state.collections.collections);
+
+ // merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
+ let collections = produce(_collections, draft => {
+ let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);
+
+ if (collection) {
+ // add selected global env variables to the collection object
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ collection.globalEnvironmentVariables = globalEnvironmentVariables;
+ }
+ });
+
+ let collection = find(collections, (c) => c.uid === focusedTab?.collectionUid);
+
+ const screenWidth = useSelector((state) => state.app.screenWidth);
+ let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const [leftPaneWidth, setLeftPaneWidth] = useState(
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
); // 2.2 so that request pane is relatively smaller
@@ -117,7 +133,6 @@ const RequestTabPanel = () => {
return An error occurred!
;
}
- let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
if (!collection || !collection.uid) {
return Collection not found!
;
}
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
index 26ec31545..8ca76b15e 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
@@ -2,6 +2,7 @@ import React from 'react';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
+import GlobalEnvironmentSelector from 'components/GlobalEnvironments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import ToolHint from 'components/ToolHint';
@@ -48,7 +49,7 @@ const CollectionToolBar = ({ collection }) => {
{collection?.name}
-
+
@@ -67,6 +68,9 @@ const CollectionToolBar = ({ collection }) => {
+
+
+
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/CloseTabIcon.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/CloseTabIcon.js
new file mode 100644
index 000000000..de2aa845d
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/CloseTabIcon.js
@@ -0,0 +1,10 @@
+const CloseTabIcon = () => (
+
+
+
+);
+
+export default CloseTabIcon;
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/DraftTabIcon.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/DraftTabIcon.js
new file mode 100644
index 000000000..ab097c536
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/DraftTabIcon.js
@@ -0,0 +1,15 @@
+const DraftTabIcon = () => (
+
+
+
+);
+
+export default DraftTabIcon;
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
index 7bf45446e..220f12200 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { IconAlertTriangle } from '@tabler/icons';
+import CloseTabIcon from './CloseTabIcon';
const RequestTabNotFound = ({ handleCloseClick }) => {
const [showErrorMessage, setShowErrorMessage] = useState(false);
@@ -28,12 +29,7 @@ const RequestTabNotFound = ({ handleCloseClick }) => {
) : null}
handleCloseClick(e)}>
-
-
-
+
>
);
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
index 8745cf079..c5d09faa8 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
@@ -1,4 +1,5 @@
import React from 'react';
+import CloseTabIcon from './CloseTabIcon';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
@@ -51,12 +52,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
<>
{getTabInfo(type, tabName)}
handleCloseClick(e)}>
-
-
-
+
>
);
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
index 64d6eebb5..e73313c13 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
@@ -15,6 +15,9 @@ import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
+import CloseTabIcon from './CloseTabIcon';
+import DraftTabIcon from './DraftTabIcon';
+import { flattenItems } from 'utils/collections/index';
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
@@ -49,9 +52,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const handleMouseUp = (e) => {
if (e.button === 1) {
- e.stopPropagation();
e.preventDefault();
+ e.stopPropagation();
+ // Close the tab
dispatch(
closeTabs({
tabUids: [tab.uid]
@@ -68,7 +72,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
-
+
{tab.type === 'folder-settings' ? (
) : (
@@ -82,7 +89,17 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
if (!item) {
return (
-
+ {
+ if (e.button === 1) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ dispatch(closeTabs({ tabUids: [tab.uid] }));
+ }
+ }}
+ >
);
@@ -166,24 +183,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
}}
>
{!item.draft ? (
-
-
-
+
) : (
-
-
-
+
)}
@@ -245,8 +247,9 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
function handleCloseSavedTabs(event) {
event.stopPropagation();
- const savedTabs = collection.items.filter((item) => !item.draft);
- const savedTabIds = savedTabs.map((item) => item.uid) || [];
+ const items = flattenItems(collection?.items);
+ const savedTabs = items?.filter?.((item) => !item.draft);
+ const savedTabIds = savedTabs?.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
index 86d79c4bc..5683801ab 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
@@ -7,6 +7,8 @@ import { useState } from 'react';
import 'pdfjs-dist/build/pdf.worker';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
+import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';
+GlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
const QueryResultPreview = ({
previewTab,
@@ -29,7 +31,7 @@ const QueryResultPreview = ({
setNumPages(numPages);
}
// Fail safe, so we don't render anything with an invalid tab
- if (!allowedPreviewModes.includes(previewTab)) {
+ if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
return null;
}
@@ -40,7 +42,7 @@ const QueryResultPreview = ({
dispatch(sendRequest(item, collection.uid));
};
- switch (previewTab) {
+ switch (previewTab?.mode) {
case 'preview-web': {
const webViewSrc = data.replace('', ` `);
return (
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index ee956f1b1..e9acd1f5c 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -12,18 +12,35 @@ import { useState } from 'react';
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
+import { uuid } from 'utils/common/index';
const formatResponse = (data, mode, filter) => {
if (data === undefined) {
return '';
}
+ if (data === null) {
+ return data;
+ }
+
if (mode.includes('json')) {
+ let isValidJSON = false;
+
+ try {
+ isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object';
+ } catch (error) {
+ console.log('Error parsing JSON: ', error.message);
+ }
+
+ if (!isValidJSON && typeof data === 'string') {
+ return data;
+ }
+
if (filter) {
try {
data = JSONPath({ path: filter, json: data });
} catch (e) {
- console.warn('Could not filter with JSONPath.', e.message);
+ console.warn('Could not apply JSONPath filter:', e.message);
}
}
@@ -35,7 +52,6 @@ const formatResponse = (data, mode, filter) => {
if (typeof parsed === 'string') {
return parsed;
}
-
return safeStringifyJSON(parsed, true);
}
@@ -43,7 +59,7 @@ const formatResponse = (data, mode, filter) => {
return data;
}
- return safeStringifyJSON(data);
+ return safeStringifyJSON(data, true);
};
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
@@ -59,18 +75,18 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
const allowedPreviewModes = useMemo(() => {
// Always show raw
- const allowedPreviewModes = ['raw'];
+ const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
if (mode.includes('html') && typeof data === 'string') {
- allowedPreviewModes.unshift('preview-web');
+ allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
} else if (mode.includes('image')) {
- allowedPreviewModes.unshift('preview-image');
+ allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
} else if (contentType.includes('pdf')) {
- allowedPreviewModes.unshift('preview-pdf');
+ allowedPreviewModes.unshift({ mode: 'preview-pdf', name: 'PDF', uid: uuid() });
} else if (contentType.includes('audio')) {
- allowedPreviewModes.unshift('preview-audio');
+ allowedPreviewModes.unshift({ mode: 'preview-audio', name: 'Audio', uid: uuid() });
} else if (contentType.includes('video')) {
- allowedPreviewModes.unshift('preview-video');
+ allowedPreviewModes.unshift({ mode: 'preview-video', name: 'Video', uid: uuid() });
}
return allowedPreviewModes;
@@ -79,7 +95,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
// Ensure the active Tab is always allowed
useEffect(() => {
- if (!allowedPreviewModes.includes(previewTab)) {
+ if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
setPreviewTab(allowedPreviewModes[0]);
}
}, [previewTab, allowedPreviewModes]);
@@ -91,12 +107,15 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
return allowedPreviewModes.map((previewMode) => (
setPreviewTab(previewMode)}
- key={previewMode}
+ key={previewMode?.uid}
>
- {previewMode.replace(/-(.*)/, ' ')}
+ {previewMode?.name}
));
}, [allowedPreviewModes, previewTab]);
diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js
index 97745f060..925b4b77b 100644
--- a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js
@@ -17,6 +17,8 @@ const Timeline = ({ request, response }) => {
});
});
+ let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
+
return (
@@ -31,10 +33,10 @@ const Timeline = ({ request, response }) => {
);
})}
- {request.data ? (
+ {requestData ? (
{'>'} data{' '}
- {request.data}
+ {requestData}
) : null}
diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
index 007d398c0..8fd8de9d9 100644
--- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
@@ -15,7 +15,7 @@ import StyledWrapper from './StyledWrapper';
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const [selectedTab, setSelectedTab] = useState('response');
- const { requestSent, responseReceived, testResults, assertionResults } = item;
+ const { requestSent, responseReceived, testResults, assertionResults, error } = item;
const headers = get(item, 'responseReceived.headers', []);
const status = get(item, 'responseReceived.status', 0);
@@ -36,6 +36,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
data={responseReceived.data}
dataBuffer={responseReceived.dataBuffer}
headers={responseReceived.headers}
+ error={error}
key={item.filename}
/>
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
index 0b073fa77..41d3e5ff2 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
@@ -41,10 +41,10 @@ const CloneCollection = ({ onClose, collection }) => {
)
)
.then(() => {
- toast.success('Collection created');
+ toast.success('Collection created!');
onClose();
})
- .catch(() => toast.error('An error occurred while creating the collection'));
+ .catch((e) => toast.error('An error occurred while creating the collection - ' + e));
}
});
@@ -72,7 +72,7 @@ const CloneCollection = ({ onClose, collection }) => {
return (
-
+ e.preventDefault()}>
Name
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
index 0dd96e197..0bf17603d 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
@@ -25,6 +25,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
onSubmit: (values) => {
dispatch(cloneItem(values.name, item.uid, collection.uid))
.then(() => {
+ toast.success('Request cloned!');
onClose();
})
.catch((err) => {
@@ -49,7 +50,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
handleConfirm={onSubmit}
handleCancel={onClose}
>
-
+ e.preventDefault()}>
{isFolder ? 'Folder' : 'Request'} Name
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
index 78977cabb..3092df4ba 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
@@ -8,19 +8,27 @@ import { useSelector } from 'react-redux';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import toast from 'react-hot-toast';
import { IconCopy } from '@tabler/icons';
-import { findCollectionByItemUid } from '../../../../../../../utils/collections/index';
+import { findCollectionByItemUid, getGlobalEnvironmentVariables } from '../../../../../../../utils/collections/index';
import { getAuthHeaders } from '../../../../../../../utils/codegenerator/auth';
+import { cloneDeep } from 'lodash';
const CodeView = ({ language, item }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
+ const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const { target, client, language: lang } = language;
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
- const collection = findCollectionByItemUid(
+ let _collection = findCollectionByItemUid(
useSelector((state) => state.collections.collections),
item.uid
);
+ let collection = cloneDeep(_collection);
+
+ // add selected global env variables to the collection object
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ collection.globalEnvironmentVariables = globalEnvironmentVariables;
+
const collectionRootAuth = collection?.root?.request?.auth;
const requestAuth = item.draft ? get(item, 'draft.request.auth') : get(item, 'request.auth');
@@ -32,7 +40,10 @@ const CodeView = ({ language, item }) => {
let snippet = '';
try {
- snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
+ snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert(
+ target,
+ client
+ );
} catch (e) {
console.error(e);
snippet = 'Error generating code snippet';
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js
index 635c545e9..ca582a842 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js
@@ -8,8 +8,9 @@ const StyledWrapper = styled.div`
.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;
+ max-height: 80vh;
height: 100%;
+ overflow-y: auto;
}
.generate-code-item {
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
index 2b19d461b..aabed03dd 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
@@ -3,60 +3,20 @@ import { useState } from 'react';
import CodeView from './CodeView';
import StyledWrapper from './StyledWrapper';
import { isValidUrl } from 'utils/url';
-import { find, get } from 'lodash';
+import { get } from 'lodash';
import { findEnvironmentInCollection } from 'utils/collections';
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
-
-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'
- }
-];
+import { getLanguages } from 'utils/codegenerator/targets';
+import { useSelector } from 'react-redux';
+import { getGlobalEnvironmentVariables } from 'utils/collections/index';
const GenerateCodeItem = ({ collection, item, onClose }) => {
- const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
+ const languages = getLanguages();
+
+ const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+
+ const environment = findEnvironmentInCollection(collection, collection?.activeEnvironmentUid);
let envVars = {};
if (environment) {
const vars = get(environment, 'variables', []);
@@ -72,6 +32,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
// interpolate the url
const interpolatedUrl = interpolateUrl({
url: requestUrl,
+ globalEnvironmentVariables,
envVars,
runtimeVariables: collection.runtimeVariables,
processEnvVars: collection.processEnvVariables
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
index 74b25de47..6cf8cb21a 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
@@ -5,6 +5,7 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import toast from 'react-hot-toast';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -27,8 +28,17 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
if (!isFolder && item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
- dispatch(renameItem(values.name, item.uid, collection.uid));
- onClose();
+ if (item.name === values.name) {
+ return;
+ }
+ dispatch(renameItem(values.name, item.uid, collection.uid))
+ .then(() => {
+ toast.success('Request renamed');
+ onClose();
+ })
+ .catch((err) => {
+ toast.error(err ? err.message : 'An error occurred while renaming the request');
+ });
}
});
@@ -48,7 +58,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
handleConfirm={onSubmit}
handleCancel={onClose}
>
-
+ e.preventDefault()}>
{isFolder ? 'Folder' : 'Request'} Name
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
index 8c3da90a7..4a81f59af 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
@@ -23,43 +23,53 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
onClose();
};
- const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
- const items = flattenItems(item ? item.items : collection.items);
- const requestItems = items.filter((item) => item.type !== 'folder');
- const recursiveRunLength = requestItems.length;
+ const getRequestsCount = (items) => {
+ const requestTypes = ['http-request', 'graphql-request']
+ return items.filter(req => requestTypes.includes(req.type)).length;
+ }
+
+ const runLength = item ? getRequestsCount(item.items) : get(collection, 'items.length', 0);
+ const flattenedItems = flattenItems(item ? item.items : collection.items);
+ const recursiveRunLength = getRequestsCount(flattenedItems);
return (
-
- Run
- ({runLength} requests)
-
- This will only run the requests in this folder.
+ {!runLength && !recursiveRunLength ? (
+ No request found in this folder.
+ ) : (
+
+
+ Run
+ ({runLength} requests)
+
+
This will only run the requests in this folder.
-
- Recursive Run
- ({recursiveRunLength} requests)
-
-
This will run all the requests in this folder and all its subfolders.
+
+ Recursive Run
+ ({recursiveRunLength} requests)
+
+
This will run all the requests in this folder and all its subfolders.
-
-
-
- Cancel
-
-
-
- onSubmit(true)}>
- Recursive Run
-
-
-
- onSubmit(false)}>
- Run
-
-
-
+
+
+
+ Cancel
+
+
+
+ onSubmit(true)}>
+ Recursive Run
+
+
+
+ onSubmit(false)}>
+ Run
+
+
+
+
+ )}
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
index 6e5947e58..09b9552b5 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
@@ -349,7 +349,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
Run
)}
- {!isFolder && item.type === 'http-request' && (
+ {!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
{
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js
index 07a927415..73b1e172a 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js
@@ -21,9 +21,14 @@ const RenameCollection = ({ collection, onClose }) => {
.required('name is required')
}),
onSubmit: (values) => {
- dispatch(renameCollection(values.name, collection.uid));
- toast.success('Collection renamed!');
- onClose();
+ dispatch(renameCollection(values.name, collection.uid))
+ .then(() => {
+ toast.success('Collection renamed!');
+ onClose();
+ })
+ .catch((err) => {
+ toast.error(err ? err.message : 'An error occurred while renaming the collection');
+ });
}
});
@@ -37,7 +42,7 @@ const RenameCollection = ({ collection, onClose }) => {
return (
-
+ e.preventDefault()}>
Name
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index ea5a17397..6f05207d2 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -34,10 +34,10 @@ const CreateCollection = ({ onClose }) => {
onSubmit: (values) => {
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
.then(() => {
- toast.success('Collection created');
+ toast.success('Collection created!');
onClose();
})
- .catch(() => toast.error('An error occurred while creating the collection'));
+ .catch((e) => toast.error('An error occurred while creating the collection - ' + e));
}
});
@@ -65,7 +65,7 @@ const CreateCollection = ({ onClose }) => {
return (
-
+ e.preventDefault()}>
Name
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
index 7dd827298..a58dd5b52 100644
--- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
+++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
@@ -144,7 +144,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
return (
-
+ e.preventDefault()}>
Name
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
index 934a3bd29..ada38a1cb 100644
--- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
@@ -32,7 +32,10 @@ const NewFolder = ({ collection, item, onClose }) => {
}),
onSubmit: (values) => {
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
- .then(() => onClose())
+ .then(() => {
+ toast.success('New folder created!');
+ onClose()
+ })
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));
}
});
@@ -47,7 +50,7 @@ const NewFolder = ({ collection, item, onClose }) => {
return (
-
+ e.preventDefault()}>
Folder Name
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index 50e7be277..48b871af3 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -113,7 +113,10 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
auth: request.auth
})
)
- .then(() => onClose())
+ .then(() => {
+ toast.success('New request created!');
+ onClose()
+ })
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else {
dispatch(
@@ -126,7 +129,10 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
itemUid: item ? item.uid : null
})
)
- .then(() => onClose())
+ .then(() => {
+ toast.success('New request created!');
+ onClose()
+ })
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
}
}
@@ -162,16 +168,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
return (
- {
- if (e.key === 'Enter') {
- e.preventDefault();
- formik.handleSubmit();
- }
- }}
- >
+ e.preventDefault()}>
Type
diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js
index bec0c92a5..bbd88fbcf 100644
--- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js
+++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js
@@ -82,16 +82,12 @@ const TitleBar = () => {
) : null}
-
-
-
-
+
+
+
+
bruno
-
+
} placement="bottom-start">
{
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const preferencesOpen = useSelector((state) => state.app.showPreferences);
- const [goldenEditonOpen, setGoldenEditonOpen] = useState(false);
+ const [goldenEditionOpen, setGoldenEditionOpen] = useState(false);
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
const [cookiesOpen, setCookiesOpen] = useState(false);
@@ -83,10 +82,43 @@ const Sidebar = () => {
return (
- {goldenEditonOpen && setGoldenEditonOpen(false)} />}
+ {goldenEditionOpen && (
+ {
+ setGoldenEditionOpen(false);
+ document.querySelector('[data-trigger="golden-edition"]').focus();
+ }}
+ aria-modal="true"
+ role="dialog"
+ aria-labelledby="golden-edition-title"
+ aria-describedby="golden-edition-description"
+ />
+ )}
- {preferencesOpen &&
dispatch(showPreferences(false))} />}
- {cookiesOpen && setCookiesOpen(false)} />}
+ {preferencesOpen && (
+ {
+ dispatch(showPreferences(false));
+ document.querySelector('[data-trigger="preferences"]').focus();
+ }}
+ aria-modal="true"
+ role="dialog"
+ aria-labelledby="preferences-title"
+ aria-describedby="preferences-description"
+ />
+ )}
+ {cookiesOpen && (
+ {
+ setCookiesOpen(false);
+ document.querySelector('[data-trigger="cookies"]').focus();
+ }}
+ aria-modal="true"
+ role="dialog"
+ aria-labelledby="cookies-title"
+ aria-describedby="cookies-description"
+ />
+ )}
@@ -96,30 +128,50 @@ const Sidebar = () => {
{/* This will get moved to home page */}
@@ -129,18 +181,19 @@ const Sidebar = () => {
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
- Star
+ Star
*/}
-
v1.27.0
+
v1.34.2
+
-
+
);
};
diff --git a/packages/bruno-app/src/components/StopWatch/index.js b/packages/bruno-app/src/components/StopWatch/index.js
index e2b069532..debba9cd8 100644
--- a/packages/bruno-app/src/components/StopWatch/index.js
+++ b/packages/bruno-app/src/components/StopWatch/index.js
@@ -1,25 +1,23 @@
import React, { useState, useEffect } from 'react';
-const StopWatch = ({ requestTimestamp }) => {
+const StopWatch = () => {
const [milliseconds, setMilliseconds] = useState(0);
- const tickInterval = 200;
+ const tickInterval = 100;
const tick = () => {
- setMilliseconds(milliseconds + tickInterval);
+ setMilliseconds(_milliseconds => _milliseconds + tickInterval);
};
useEffect(() => {
- let timerID = setInterval(() => tick(), tickInterval);
+ let timerID = setInterval(() => {
+ tick()
+ }, tickInterval);
return () => {
- clearInterval(timerID);
+ clearTimeout(timerID);
};
- });
+ }, []);
- useEffect(() => {
- setMilliseconds(Date.now() - requestTimestamp);
- }, [requestTimestamp]);
-
- if (milliseconds < 1000) {
+ if (milliseconds < 250) {
return 'Loading...';
}
@@ -27,4 +25,4 @@ const StopWatch = ({ requestTimestamp }) => {
return {seconds.toFixed(1)}s ;
};
-export default StopWatch;
+export default React.memo(StopWatch);
diff --git a/packages/bruno-app/src/components/Table/StyledWrapper.js b/packages/bruno-app/src/components/Table/StyledWrapper.js
new file mode 100644
index 000000000..eeead4ed2
--- /dev/null
+++ b/packages/bruno-app/src/components/Table/StyledWrapper.js
@@ -0,0 +1,64 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ table {
+ width: 100%;
+ display: grid;
+ overflow-y: hidden;
+ overflow-x: auto;
+
+ // for icon hover
+ position: inherit;
+ left: -4px;
+ padding-left: 4px;
+ padding-right: 4px;
+
+ grid-template-columns: ${({ columns }) =>
+ columns?.[0]?.width
+ ? columns.map((col) => `${col?.width}`).join(' ')
+ : columns.map((col) => `${100 / columns.length}%`).join(' ')};
+ }
+
+ table thead,
+ table tbody,
+ table tr {
+ display: contents;
+ }
+
+ table th {
+ position: relative;
+ border-bottom: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ }
+
+ table tr td {
+ padding: 0.5rem;
+ text-align: left;
+ border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ }
+
+ tr {
+ transition: transform 0.2s ease-in-out;
+ }
+
+ tr.dragging {
+ opacity: 0.5;
+ }
+
+ tr.hovered {
+ transform: translateY(10px); /* Adjust the value as needed for the animation effect */
+ }
+
+ table tr th {
+ padding: 0.5rem;
+ text-align: left;
+ border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+
+ &:nth-child(1) {
+ border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Table/index.js b/packages/bruno-app/src/components/Table/index.js
new file mode 100644
index 000000000..80bfb19f3
--- /dev/null
+++ b/packages/bruno-app/src/components/Table/index.js
@@ -0,0 +1,110 @@
+import { useState, useRef, useEffect, useCallback } from 'react';
+import StyledWrapper from './StyledWrapper';
+
+const Table = ({ minColumnWidth = 1, headers = [], children }) => {
+ const [activeColumnIndex, setActiveColumnIndex] = useState(null);
+ const tableRef = useRef(null);
+
+ const columns = headers?.map((item) => ({
+ ...item,
+ ref: useRef()
+ }));
+
+ const updateDivHeights = () => {
+ if (tableRef.current) {
+ const height = tableRef.current.offsetHeight;
+ columns.forEach((col) => {
+ if (col.ref.current) {
+ col.ref.current.querySelector('.resizer').style.height = `${height}px`;
+ }
+ });
+ }
+ };
+
+ useEffect(() => {
+ updateDivHeights();
+ window.addEventListener('resize', updateDivHeights);
+
+ return () => {
+ window.removeEventListener('resize', updateDivHeights);
+ };
+ }, [columns]);
+
+ useEffect(() => {
+ if (tableRef.current) {
+ const observer = new MutationObserver(updateDivHeights);
+ observer.observe(tableRef.current, { childList: true, subtree: true });
+
+ return () => {
+ observer.disconnect();
+ };
+ }
+ }, [columns]);
+
+ const handleMouseDown = (index) => (e) => {
+ setActiveColumnIndex(index);
+ };
+
+ const handleMouseMove = useCallback(
+ (e) => {
+ const gridColumns = columns.map((col, i) => {
+ if (i === activeColumnIndex) {
+ const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
+
+ if (width >= minColumnWidth) {
+ return `${width}px`;
+ }
+ }
+ return `${col.ref.current.offsetWidth}px`;
+ });
+
+ tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
+ },
+ [activeColumnIndex, columns, minColumnWidth]
+ );
+
+ const handleMouseUp = useCallback(() => {
+ setActiveColumnIndex(null);
+ removeListeners();
+ }, [removeListeners]);
+
+ const removeListeners = useCallback(() => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', removeListeners);
+ }, [handleMouseMove]);
+
+ useEffect(() => {
+ if (activeColumnIndex !== null) {
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+ }
+ return () => {
+ removeListeners();
+ };
+ }, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
+
+ return (
+
+
+
+
+
+ {columns.map(({ ref, name }, i) => (
+
+ {name}
+
+
+ ))}
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Table;
diff --git a/packages/bruno-app/src/components/Toast/StyledWrapper.js b/packages/bruno-app/src/components/Toast/StyledWrapper.js
deleted file mode 100644
index e06a2a058..000000000
--- a/packages/bruno-app/src/components/Toast/StyledWrapper.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import styled from 'styled-components';
-
-const Wrapper = styled.div`
- &.bruno-toast {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- display: flex;
- justify-content: center;
- }
-
- .bruno-toast-card {
- -webkit-animation-duration: 0.85s;
- animation-duration: 0.85s;
- -webkit-animation-delay: 0.1s;
- animation-delay: 0.1s;
- border-radius: var(--border-radius);
- position: relative;
- max-width: calc(100% - var(--spacing-base-unit));
- margin: 3vh 10vw;
-
- animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
- }
-
- .notification-toast-content {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: 5px;
- border-radius: 4px;
- }
-
- .alert {
- position: relative;
- padding: 0.25rem 0.75rem;
- border: 1px solid transparent;
- border-radius: 0.25rem;
- display: flex;
- justify-content: space-between;
- }
-
- .alert-error {
- color: #721c24;
- background-color: #f8d7da;
- border-color: #f5c6cb;
- }
-
- .alert-info {
- color: #004085;
- background-color: #cce5ff;
- border-color: #b8daff;
- }
-
- .alert-warning {
- color: #856404;
- background-color: #fff3cd;
- border-color: #ffeeba;
- }
-
- .alert-success {
- color: #155724;
- background-color: #d4edda;
- border-color: #c3e6cb;
- }
-
- .closeToast {
- cursor: pointer;
- padding-left: 10px;
- }
-`;
-
-export default Wrapper;
diff --git a/packages/bruno-app/src/components/Toast/index.js b/packages/bruno-app/src/components/Toast/index.js
deleted file mode 100644
index 243b9a364..000000000
--- a/packages/bruno-app/src/components/Toast/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React, { useEffect } from 'react';
-import StyledWrapper from './StyledWrapper';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faTimes } from '@fortawesome/free-solid-svg-icons';
-
-const ToastContent = ({ type, text, handleClose }) => (
-
-);
-
-const Toast = ({ text, type, duration, handleClose }) => {
- let lifetime = duration ? duration : 3000;
-
- useEffect(() => {
- if (text) {
- setTimeout(handleClose, lifetime);
- }
- }, [text]);
-
- return (
-
-
-
-
-
- );
-};
-
-export default Toast;
diff --git a/packages/bruno-app/src/components/ToolHint/index.js b/packages/bruno-app/src/components/ToolHint/index.js
index 5ca7af233..b8799dd69 100644
--- a/packages/bruno-app/src/components/ToolHint/index.js
+++ b/packages/bruno-app/src/components/ToolHint/index.js
@@ -10,7 +10,8 @@ const ToolHint = ({
tooltipStyle = {},
place = 'top',
offset,
- theme = null
+ theme = null,
+ className = ''
}) => {
const { theme: contextTheme } = useTheme();
const appliedTheme = theme || contextTheme;
@@ -22,13 +23,14 @@ const ToolHint = ({
...tooltipStyle,
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
+ zIndex: 9999,
backgroundColor: toolhintBackgroundColor,
color: toolhintTextColor
};
return (
<>
- {children}
+ {children}
{
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const handleOpenCollection = () => {
- dispatch(openCollection()).catch(
- (err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))
- );
+ dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
};
const handleImportCollection = ({ collection, translationLog }) => {
@@ -64,7 +62,7 @@ const Welcome = () => {
/>
) : null}
-
+
bruno
@@ -72,40 +70,69 @@ const Welcome = () => {
{t('COMMON.COLLECTIONS')}
-
setCreateCollectionModalOpen(true)}>
-
+ setCreateCollectionModalOpen(true)}
+ aria-label={t('WELCOME.CREATE_COLLECTION')}
+ >
+
{t('WELCOME.CREATE_COLLECTION')}
-
-
-
+
+
+
+
{t('WELCOME.OPEN_COLLECTION')}
-
-
setImportCollectionModalOpen(true)}>
-
+
+
+ setImportCollectionModalOpen(true)}
+ aria-label={t('WELCOME.IMPORT_COLLECTION')}
+ >
+
{t('WELCOME.IMPORT_COLLECTION')}
-
+
+
{t('WELCOME.LINKS')}
diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js
index 7839a55ac..30f264ab1 100644
--- a/packages/bruno-app/src/globalStyles.js
+++ b/packages/bruno-app/src/globalStyles.js
@@ -231,6 +231,11 @@ const GlobalStyle = createGlobalStyle`
.CodeMirror-brunoVarInfo p {
margin: 1em 0;
}
+
+ .CodeMirror-hint-active {
+ background: #89f !important;
+ color: #fff !important;
+ }
`;
export default GlobalStyle;
diff --git a/packages/bruno-app/src/hooks/useFocusTrap/index.js b/packages/bruno-app/src/hooks/useFocusTrap/index.js
new file mode 100644
index 000000000..760603d21
--- /dev/null
+++ b/packages/bruno-app/src/hooks/useFocusTrap/index.js
@@ -0,0 +1,53 @@
+import { useEffect, useRef } from 'react';
+
+const useFocusTrap = (modalRef) => {
+
+ // refer to this implementation for modal focus: https://stackoverflow.com/a/38865836
+ const focusableSelector = 'a[href], area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex]:not([tabindex="-1"]), *[contenteditable]';
+
+ useEffect(() => {
+ const modalElement = modalRef.current;
+ if (!modalElement) return;
+
+ const focusableElements = Array.from(document.querySelectorAll(focusableSelector));
+ const modalFocusableElements = Array.from(modalElement.querySelectorAll(focusableSelector));
+ const elementsToHide = focusableElements.filter(el => !modalFocusableElements.includes(el));
+
+ // Hide elements outside the modal
+ elementsToHide.forEach(el => {
+ const originalTabIndex = el.getAttribute('tabindex');
+ el.setAttribute('data-tabindex', originalTabIndex || 'inline');
+ el.setAttribute('tabindex', -1);
+ });
+
+ // Set focus to the first focusable element in the modal
+ const firstElement = modalFocusableElements[0];
+ const lastElement = modalFocusableElements[modalFocusableElements.length - 1];
+
+ const handleKeyDown = (event) => {
+ if (event.key === 'Tab') {
+ if (event.shiftKey && document.activeElement === firstElement) {
+ event.preventDefault();
+ lastElement.focus();
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
+ event.preventDefault();
+ firstElement.focus();
+ }
+ }
+ };
+
+ modalElement.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ modalElement.removeEventListener('keydown', handleKeyDown);
+
+ // Restore original tabindex values
+ elementsToHide.forEach(el => {
+ const originalTabIndex = el.getAttribute('data-tabindex');
+ el.setAttribute('tabindex', originalTabIndex === 'inline' ? '' : originalTabIndex);
+ });
+ };
+ }, [modalRef]);
+};
+
+export default useFocusTrap;
diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js
index 0f3f553d6..331638070 100644
--- a/packages/bruno-app/src/pages/_app.js
+++ b/packages/bruno-app/src/pages/_app.js
@@ -23,6 +23,8 @@ import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
+import { setupPolyfills } from 'utils/common/setupPolyfills';
+setupPolyfills();
function SafeHydrate({ children }) {
return
{typeof window === 'undefined' ? null : children}
;
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index f4a04030f..80ea83283 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -19,10 +19,11 @@ import {
runRequestEvent,
scriptEnvironmentUpdateEvent
} from 'providers/ReduxStore/slices/collections';
-import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
+import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
+import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
const useIpcEvents = () => {
const dispatch = useDispatch();
@@ -109,6 +110,10 @@ const useIpcEvents = () => {
dispatch(scriptEnvironmentUpdateEvent(val));
});
+ const removeGlobalEnvironmentVariablesUpdateListener = ipcRenderer.on('main:global-environment-variables-update', (val) => {
+ dispatch(globalEnvironmentsUpdateEvent(val));
+ });
+
const removeCollectionRenamedListener = ipcRenderer.on('main:collection-renamed', (val) => {
dispatch(collectionRenamedEvent(val));
});
@@ -149,12 +154,21 @@ const useIpcEvents = () => {
dispatch(updateCookies(val));
});
+ const removeGlobalEnvironmentsUpdatesListener = ipcRenderer.on('main:load-global-environments', (val) => {
+ dispatch(updateGlobalEnvironments(val));
+ });
+
+ const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
+ dispatch(hydrateCollectionWithUiStateSnapshot(val));
+ })
+
return () => {
removeCollectionTreeUpdateListener();
removeOpenCollectionListener();
removeCollectionAlreadyOpenedListener();
removeDisplayErrorListener();
removeScriptEnvUpdateListener();
+ removeGlobalEnvironmentVariablesUpdateListener();
removeCollectionRenamedListener();
removeRunFolderEventListener();
removeRunRequestEventListener();
@@ -165,6 +179,8 @@ const useIpcEvents = () => {
removePreferencesUpdatesListener();
removeCookieUpdateListener();
removeSystemProxyEnvUpdatesListener();
+ removeGlobalEnvironmentsUpdatesListener();
+ removeSnapshotHydrationListener();
};
}, [isElectron]);
};
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index efd8e6a76..2d79e0cab 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.27.0'
+ version: '1.34.2'
}
});
};
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index 1b28b891b..87d187ddd 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -3,13 +3,13 @@ import toast from 'react-hot-toast';
import find from 'lodash/find';
import Mousetrap from 'mousetrap';
import { useSelector, useDispatch } from 'react-redux';
-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 { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
+import { getKeyBindingsForActionAllOS } from './keyMappings';
export const HotkeysContext = React.createContext();
@@ -19,19 +19,9 @@ export const HotkeysProvider = (props) => {
const collections = useSelector((state) => state.collections.collections);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
- const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
- const getCurrentCollectionItems = () => {
- const activeTab = find(tabs, (t) => t.uid === activeTabUid);
- if (activeTab) {
- const collection = findCollectionByUid(collections, activeTab.collectionUid);
-
- return collection ? collection.items : [];
- }
- };
-
const getCurrentCollection = () => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
@@ -43,7 +33,7 @@ export const HotkeysProvider = (props) => {
// save hotkey
useEffect(() => {
- Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => {
if (isEnvironmentSettingsModalOpen) {
console.log('todo: save environment settings');
} else {
@@ -54,9 +44,8 @@ export const HotkeysProvider = (props) => {
const item = findItemInCollection(collection, activeTab.uid);
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
- } else {
- // todo: when ephermal requests go live
- // setShowSaveRequestModal(true);
+ } else if (activeTab.type === 'collection-settings') {
+ dispatch(saveCollectionRoot(collection.uid));
}
}
}
@@ -66,13 +55,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+s', 'ctrl+s']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]);
};
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]);
// send request (ctrl/cmd + enter)
useEffect(() => {
- Mousetrap.bind(['command+enter', 'ctrl+enter'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -93,13 +82,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+enter', 'ctrl+enter']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]);
};
}, [activeTabUid, tabs, saveRequest, collections]);
// edit environments (ctrl/cmd + e)
useEffect(() => {
- Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -113,13 +102,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+e', 'ctrl+e']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]);
};
}, [activeTabUid, tabs, collections, setShowEnvSettingsModal]);
// new request (ctrl/cmd + b)
useEffect(() => {
- Mousetrap.bind(['command+b', 'ctrl+b'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('newRequest')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -133,13 +122,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+b', 'ctrl+b']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]);
};
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
// close tab hotkey
useEffect(() => {
- Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => {
dispatch(
closeTabs({
tabUids: [activeTabUid]
@@ -150,13 +139,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+w', 'ctrl+w']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]);
};
}, [activeTabUid]);
// Switch to the previous tab
useEffect(() => {
- Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => {
dispatch(
switchTab({
direction: 'pageup'
@@ -167,13 +156,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]);
};
}, [dispatch]);
// Switch to the next tab
useEffect(() => {
- Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => {
dispatch(
switchTab({
direction: 'pagedown'
@@ -184,13 +173,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]);
};
}, [dispatch]);
// Close all tabs
useEffect(() => {
- Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -209,15 +198,12 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]);
};
}, [activeTabUid, tabs, collections, dispatch]);
return (
- {showSaveRequestModal && (
- setShowSaveRequestModal(false)} />
- )}
{showEnvSettingsModal && (
setShowEnvSettingsModal(false)} />
)}
diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
new file mode 100644
index 000000000..05ad4531b
--- /dev/null
+++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
@@ -0,0 +1,60 @@
+const KeyMapping = {
+ save: { mac: 'command+s', windows: 'ctrl+s', name: 'Save' },
+ sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' },
+ editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' },
+ newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' },
+ closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' },
+ openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' },
+ minimizeWindow: {
+ mac: 'command+Shift+Q',
+ windows: 'control+Shift+Q',
+ name: 'Minimize Window'
+ },
+ switchToPreviousTab: {
+ mac: 'command+pageup',
+ windows: 'ctrl+pageup',
+ name: 'Switch to Previous Tab'
+ },
+ switchToNextTab: {
+ mac: 'command+pagedown',
+ windows: 'ctrl+pagedown',
+ name: 'Switch to Next Tab'
+ },
+ closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' }
+};
+
+/**
+ * Retrieves the key bindings for a specific operating system.
+ *
+ * @param {string} os - The operating system (e.g., 'mac', 'windows').
+ * @returns {Object} An object containing the key bindings for the specified OS.
+ */
+export const getKeyBindingsForOS = (os) => {
+ const keyBindings = {};
+ for (const [action, { name, ...keys }] of Object.entries(KeyMapping)) {
+ if (keys[os]) {
+ keyBindings[action] = {
+ keys: keys[os],
+ name
+ };
+ }
+ }
+ return keyBindings;
+};
+
+/**
+ * Retrieves the key bindings for a specific action across all operating systems.
+ *
+ * @param {string} action - The action for which to retrieve key bindings.
+ * @returns {Object|null} An object containing the key bindings for macOS, Windows, or null if the action is not found.
+ */
+export const getKeyBindingsForActionAllOS = (action) => {
+ const actionBindings = KeyMapping[action];
+
+ if (!actionBindings) {
+ console.warn(`Action "${action}" not found in KeyMapping.`);
+ return null;
+ }
+
+ return [actionBindings.mac, actionBindings.windows];
+};
diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js
index f8ae75d64..1af71f7bf 100644
--- a/packages/bruno-app/src/providers/ReduxStore/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/index.js
@@ -6,6 +6,7 @@ import appReducer from './slices/app';
import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications';
+import globalEnvironmentsReducer from './slices/global-environments';
const { publicRuntimeConfig } = getConfig();
const isDevEnv = () => {
@@ -22,7 +23,8 @@ export const store = configureStore({
app: appReducer,
collections: collectionsReducer,
tabs: tabsReducer,
- notifications: notificationsReducer
+ notifications: notificationsReducer,
+ globalEnvironments: globalEnvironmentsReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});
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 054b4fbd4..75c6f2cb9 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -43,6 +43,9 @@ import { resolveRequestFilename } from 'utils/common/platform';
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
+import slash from 'utils/common/slash';
+import { getGlobalEnvironmentVariables } from 'utils/collections/index';
+import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -182,6 +185,7 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {
const state = getState();
+ const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
@@ -189,11 +193,15 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
return reject(new Error('Collection not found'));
}
- const collectionCopy = cloneDeep(collection);
+ let collectionCopy = cloneDeep(collection);
+
+ // add selected global env variables to the collection object
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
- _sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
+ _sendCollectionOauth2Request(collectionCopy, environment, collectionCopy.runtimeVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@@ -211,6 +219,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const state = getState();
+ const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
@@ -219,7 +228,11 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
}
const itemCopy = cloneDeep(item || {});
- const collectionCopy = cloneDeep(collection);
+ let collectionCopy = cloneDeep(collection);
+
+ // add selected global env variables to the collection object
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
@@ -284,6 +297,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
const state = getState();
+ const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
@@ -291,7 +305,12 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
return reject(new Error('Collection not found'));
}
- const collectionCopy = cloneDeep(collection);
+ let collectionCopy = cloneDeep(collection);
+
+ // add selected global env variables to the collection object
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
+
const folder = findItemInCollection(collectionCopy, folderUid);
if (folderUid && !folder) {
@@ -401,7 +420,7 @@ 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', slash(item.pathname), newPathname, newName).then(resolve).catch(reject);
});
};
@@ -969,12 +988,16 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g
}
const collectionCopy = cloneDeep(collection);
- if (environmentUid) {
- const environment = findEnvironmentInCollection(collectionCopy, environmentUid);
- if (!environment) {
- return reject(new Error('Environment not found'));
- }
- }
+
+ const environmentName = environmentUid
+ ? findEnvironmentInCollection(collectionCopy, environmentUid)?.name
+ : null;
+
+ if (environmentUid && !environmentName) {
+ return reject(new Error('Environment not found'));
+ }
+
+ ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }});
dispatch(_selectEnvironment({ environmentUid, collectionUid }));
resolve();
@@ -1140,3 +1163,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
.catch(reject);
});
};
+
+
+export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {
+ const collectionSnapshotData = payload;
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ try {
+ if(!collectionSnapshotData) resolve();
+ const { pathname, selectedEnvironment } = collectionSnapshotData;
+ const collection = findCollectionByPathname(state.collections.collections, pathname);
+ const collectionCopy = cloneDeep(collection);
+ const collectionUid = collectionCopy?.uid;
+
+ // update selected environment
+ if (selectedEnvironment) {
+ const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
+ if (environment) {
+ dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
+ }
+ }
+
+ // todo: add any other redux state that you want to save
+
+ resolve();
+ }
+ catch(error) {
+ reject(error);
+ }
+ });
+ };
\ No newline at end of file
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index ca66ac2ea..b7ef2f86e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -477,6 +477,14 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'oauth2';
item.draft.request.auth.oauth2 = action.payload.content;
break;
+ case 'wsse':
+ item.draft.request.auth.mode = 'wsse';
+ item.draft.request.auth.wsse = action.payload.content;
+ break;
+ case 'apikey':
+ item.draft.request.auth.mode = 'apikey';
+ item.draft.request.auth.apikey = action.payload.content;
+ break;
}
}
}
@@ -503,6 +511,39 @@ export const collectionsSlice = createSlice({
}
}
},
+
+ moveQueryParam: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ // Ensure item.draft is a deep clone of item if not already present
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ // Extract payload data
+ const { updateReorderedItem } = action.payload;
+ const params = item.draft.request.params;
+
+ item.draft.request.params = updateReorderedItem.map((uid) => {
+ return params.find((param) => param.uid === uid);
+ });
+
+ // update request url
+ const parts = splitOnFirst(item.draft.request.url, '?');
+ const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
+ if (query && query.length) {
+ item.draft.request.url = parts[0] + '?' + query;
+ } else {
+ item.draft.request.url = parts[0];
+ }
+ }
+ }
+ },
+
updateQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1104,6 +1145,12 @@ export const collectionsSlice = createSlice({
case 'oauth2':
set(collection, 'root.request.auth.oauth2', action.payload.content);
break;
+ case 'wsse':
+ set(collection, 'root.request.auth.wsse', action.payload.content);
+ break;
+ case 'apikey':
+ set(collection, 'root.request.auth.apikey', action.payload.content);
+ break;
}
}
},
@@ -1302,6 +1349,71 @@ export const collectionsSlice = createSlice({
set(collection, 'root.request.headers', headers);
}
},
+ addCollectionVar: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ const type = action.payload.type;
+ if (collection) {
+ if (type === 'request') {
+ const vars = get(collection, 'root.request.vars.req', []);
+ vars.push({
+ uid: uuid(),
+ name: '',
+ value: '',
+ enabled: true
+ });
+ set(collection, 'root.request.vars.req', vars);
+ } else if (type === 'response') {
+ const vars = get(collection, 'root.request.vars.res', []);
+ vars.push({
+ uid: uuid(),
+ name: '',
+ value: '',
+ enabled: true
+ });
+ set(collection, 'root.request.vars.res', vars);
+ }
+ }
+ },
+ updateCollectionVar: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ const type = action.payload.type;
+ if (type === 'request') {
+ let vars = get(collection, 'root.request.vars.req', []);
+ const _var = find(vars, (h) => h.uid === action.payload.var.uid);
+ if (_var) {
+ _var.name = action.payload.var.name;
+ _var.value = action.payload.var.value;
+ _var.description = action.payload.var.description;
+ _var.enabled = action.payload.var.enabled;
+ }
+ set(collection, 'root.request.vars.req', vars);
+ } else if (type === 'response') {
+ let vars = get(collection, 'root.request.vars.res', []);
+ const _var = find(vars, (h) => h.uid === action.payload.var.uid);
+ if (_var) {
+ _var.name = action.payload.var.name;
+ _var.value = action.payload.var.value;
+ _var.description = action.payload.var.description;
+ _var.enabled = action.payload.var.enabled;
+ }
+ set(collection, 'root.request.vars.res', vars);
+ }
+ },
+ deleteCollectionVar: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ const type = action.payload.type;
+ if (collection) {
+ if (type === 'request') {
+ let vars = get(collection, 'root.request.vars.req', []);
+ vars = filter(vars, (h) => h.uid !== action.payload.varUid);
+ set(collection, 'root.request.vars.req', vars);
+ } else if (type === 'response') {
+ let vars = get(collection, 'root.request.vars.res', []);
+ vars = filter(vars, (h) => h.uid !== action.payload.varUid);
+ set(collection, 'root.request.vars.res', vars);
+ }
+ }
+ },
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
@@ -1655,6 +1767,7 @@ export const {
requestUrlChanged,
updateAuth,
addQueryParam,
+ moveQueryParam,
updateQueryParam,
deleteQueryParam,
updatePathParam,
@@ -1694,6 +1807,9 @@ export const {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
+ addCollectionVar,
+ updateCollectionVar,
+ deleteCollectionVar,
updateCollectionAuthMode,
updateCollectionAuth,
updateCollectionRequestScript,
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
new file mode 100644
index 000000000..6364390f5
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
@@ -0,0 +1,242 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { stringifyIfNot, uuid } from 'utils/common/index';
+import { environmentSchema } from '@usebruno/schema';
+import { cloneDeep } from 'lodash';
+
+const initialState = {
+ globalEnvironments: [],
+ activeGlobalEnvironmentUid: null
+};
+
+export const globalEnvironmentsSlice = createSlice({
+ name: 'global-environments',
+ initialState,
+ reducers: {
+ updateGlobalEnvironments: (state, action) => {
+ state.globalEnvironments = action.payload?.globalEnvironments;
+ state.activeGlobalEnvironmentUid = action.payload?.activeGlobalEnvironmentUid;
+ },
+ _addGlobalEnvironment: (state, action) => {
+ const { name, uid, variables = [] } = action.payload;
+ if (name?.length) {
+ state.globalEnvironments.push({
+ uid,
+ name,
+ variables
+ });
+ }
+ },
+ _saveGlobalEnvironment: (state, action) => {
+ const { environmentUid: globalEnvironmentUid, variables } = action.payload;
+ if (globalEnvironmentUid) {
+ const environment = state.globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ if (environment) {
+ environment.variables = variables;
+ }
+ }
+ },
+ _renameGlobalEnvironment: (state, action) => {
+ const { environmentUid: globalEnvironmentUid, name } = action.payload;
+ if (globalEnvironmentUid) {
+ const environment = state.globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ if (environment) {
+ environment.name = name;
+ }
+ }
+ },
+ _copyGlobalEnvironment: (state, action) => {
+ const { name, uid, variables } = action.payload;
+ if (name?.length && uid) {
+ state.globalEnvironments.push({
+ uid,
+ name,
+ variables
+ });
+ }
+ },
+ _selectGlobalEnvironment: (state, action) => {
+ const { environmentUid: globalEnvironmentUid } = action.payload;
+ if (globalEnvironmentUid) {
+ const environment = state.globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ if (environment) {
+ state.activeGlobalEnvironmentUid = globalEnvironmentUid;
+ }
+ } else {
+ state.activeGlobalEnvironmentUid = null;
+ }
+ },
+ _deleteGlobalEnvironment: (state, action) => {
+ const { environmentUid: uid } = action.payload;
+ if (uid) {
+ state.globalEnvironments = state.globalEnvironments.filter(env => env?.uid !== uid);
+ if (uid === state.activeGlobalEnvironmentUid) {
+ state.activeGlobalEnvironmentUid = null;
+ }
+ }
+ }
+ }
+});
+
+export const {
+ updateGlobalEnvironments,
+ _addGlobalEnvironment,
+ _saveGlobalEnvironment,
+ _renameGlobalEnvironment,
+ _copyGlobalEnvironment,
+ _selectGlobalEnvironment,
+ _deleteGlobalEnvironment
+} = globalEnvironmentsSlice.actions;
+
+export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const uid = uuid();
+ ipcRenderer
+ .invoke('renderer:create-global-environment', { name, uid, variables })
+ .then(
+ dispatch(_addGlobalEnvironment({ name, uid, variables }))
+ )
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ const globalEnvironments = state.globalEnvironments.globalEnvironments;
+ const baseEnv = globalEnvironments?.find(env => env?.uid == baseEnvUid)
+ const uid = uuid();
+ ipcRenderer
+ .invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables })
+ .then(() => {
+ dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables }))
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ const globalEnvironments = state.globalEnvironments.globalEnvironments;
+ const environment = globalEnvironments?.find(env => env?.uid == environmentUid)
+ if (!environment) {
+ return reject(new Error('Environment not found'));
+ }
+ environmentSchema
+ .validate(environment)
+ .then(() => ipcRenderer.invoke('renderer:rename-global-environment', { name: newName, environmentUid }))
+ .then(
+ dispatch(_renameGlobalEnvironment({ name: newName, environmentUid }))
+ )
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ const globalEnvironments = state.globalEnvironments.globalEnvironments;
+ const environment = globalEnvironments?.find(env => env?.uid == environmentUid);
+
+ if (!environment) {
+ return reject(new Error('Environment not found'));
+ }
+
+ environmentSchema
+ .validate(environment)
+ .then(() => ipcRenderer.invoke('renderer:save-global-environment', {
+ environmentUid,
+ variables
+ }))
+ .then(
+ dispatch(_saveGlobalEnvironment({ environmentUid, variables }))
+ )
+ .then(resolve)
+ .catch((error) => {
+ reject(error);
+ });
+ });
+};
+
+export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ ipcRenderer
+ .invoke('renderer:select-global-environment', { environmentUid })
+ .then(
+ dispatch(_selectGlobalEnvironment({ environmentUid }))
+ )
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ ipcRenderer
+ .invoke('renderer:delete-global-environment', { environmentUid })
+ .then(
+ dispatch(_deleteGlobalEnvironment({ environmentUid }))
+ )
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ if (!globalEnvironmentVariables) resolve();
+
+ const state = getState();
+ const globalEnvironments = state?.globalEnvironments?.globalEnvironments || [];
+ const environmentUid = state?.globalEnvironments?.activeGlobalEnvironmentUid;
+ const environment = globalEnvironments?.find(env => env?.uid == environmentUid);
+
+ if (!environment || !environmentUid) {
+ console.error('Global Environment not found');
+ return resolve();
+ }
+
+ let variables = cloneDeep(environment?.variables);
+
+ // update existing values
+ variables = variables?.map?.(variable => ({
+ ...variable,
+ value: stringifyIfNot(globalEnvironmentVariables?.[variable?.name])
+ }));
+
+ // add new env values
+ Object.entries(globalEnvironmentVariables)?.forEach?.(([key, value]) => {
+ let isAnExistingVariable = variables?.find(v => v?.name == key)
+ if (!isAnExistingVariable) {
+ variables.push({
+ uid: uuid(),
+ name: key,
+ value: stringifyIfNot(value),
+ type: 'text',
+ secret: false,
+ enabled: true
+ });
+ }
+ });
+
+ environmentSchema
+ .validate(environment)
+ .then(() => ipcRenderer.invoke('renderer:save-global-environment', {
+ environmentUid,
+ variables
+ }))
+ .then(
+ dispatch(_saveGlobalEnvironment({ environmentUid, variables }))
+ )
+ .then(resolve)
+ .catch((error) => {
+ reject(error);
+ });
+ });
+}
+
+
+export default globalEnvironmentsSlice.reducer;
\ No newline at end of file
diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css
index 8396c48b5..cf132dcd9 100644
--- a/packages/bruno-app/src/styles/globals.css
+++ b/packages/bruno-app/src/styles/globals.css
@@ -23,6 +23,54 @@
--color-method-options: rgb(52 52 52);
--color-method-head: rgb(52 52 52);
}
+:root,.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,reach-portal {
+ /* Required CSS variables after upgrading GraphiQL from v1.5.9 to v2.4.7 */
+ /* Colors */
+ --color-primary: 320, 95%, 43% !important;
+ --color-secondary: 242, 51%, 61% !important;
+ --color-tertiary: 188, 100%, 36% !important;
+ --color-info: 208, 100%, 46% !important;
+ --color-success: 158, 60%, 42% !important;
+ --color-warning: 36, 100%, 41% !important;
+ --color-error: 13, 93%, 58% !important;
+ --color-neutral: 219, 28%, 32% !important;
+ --color-base: 219, 28%, 100% !important;
+
+ /* Color alpha values */
+ --alpha-secondary: 0.76 !important;
+ --alpha-tertiary: 0.5 !important;
+ --alpha-background-heavy: 0.15 !important;
+ --alpha-background-medium: 0.1 !important;
+ --alpha-background-light: 0.07 !important;
+
+ --font-size-hint: .75rem;
+ --font-size-inline-code: .8125rem;
+ --font-size-body: .9375rem;
+ --font-size-h4: 1.125rem;
+ --font-size-h3: 1.375rem;
+ --font-size-h2: 1.8125rem;
+ --font-weight-regular: 400;
+ --font-weight-medium: 500;
+ --line-height: 1.5;
+ --px-2: 2px;
+ --px-4: 4px;
+ --px-6: 6px;
+ --px-8: 8px;
+ --px-10: 10px;
+ --px-12: 12px;
+ --px-16: 16px;
+ --px-20: 20px;
+ --px-24: 24px;
+ --border-radius-2: 2px !important;
+ --border-radius-4: 2px !important;
+ --border-radius-8: 2px !important;
+ --border-radius-12: 2px !important;
+ --popover-box-shadow: 0px 0px 1px #000 !important;
+ --popover-border: none;
+ --sidebar-width: 60px;
+ --toolbar-width: 40px;
+ --session-header-height: 51px
+}
html,
body {
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index fa0738503..9bbd0eea9 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -31,6 +31,7 @@ const createHeaders = (request, headers) => {
if (contentType !== '') {
enabledHeaders.push({ name: 'content-type', value: contentType });
}
+
return enabledHeaders;
};
@@ -43,7 +44,14 @@ const createQuery = (queryParams = []) => {
}));
};
-const createPostData = (body) => {
+const createPostData = (body, type) => {
+ if (type === 'graphql-request') {
+ return {
+ mimeType: 'application/json',
+ text: JSON.stringify(body[body.mode])
+ };
+ }
+
const contentType = createContentType(body.mode);
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
return {
@@ -64,7 +72,7 @@ const createPostData = (body) => {
}
};
-export const buildHarRequest = ({ request, headers }) => {
+export const buildHarRequest = ({ request, headers, type }) => {
return {
method: request.method,
url: encodeURI(request.url),
@@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => {
cookies: [],
headers: createHeaders(request, headers),
queryString: createQuery(request.params),
- postData: createPostData(request.body),
+ postData: createPostData(request.body, type),
headersSize: 0,
bodySize: 0
};
diff --git a/packages/bruno-app/src/utils/codegenerator/targets.js b/packages/bruno-app/src/utils/codegenerator/targets.js
new file mode 100644
index 000000000..95b222dbc
--- /dev/null
+++ b/packages/bruno-app/src/utils/codegenerator/targets.js
@@ -0,0 +1,31 @@
+import { targets } from 'httpsnippet';
+
+export const getLanguages = () => {
+ const allLanguages = [];
+ for (const target of Object.values(targets)) {
+ const { key, title } = target.info;
+ const clients = Object.keys(target.clientsById);
+ const languages =
+ (clients.length === 1)
+ ? [{
+ name: title,
+ target: key,
+ client: clients[0]
+ }]
+ : clients.map(client => ({
+ name: `${title}-${client}`,
+ target: key,
+ client
+ }));
+ allLanguages.push(...languages);
+
+ // Move "Shell-curl" to the top of the array
+ const shellCurlIndex = allLanguages.findIndex(lang => lang.name === "Shell-curl");
+ if (shellCurlIndex !== -1) {
+ const [shellCurl] = allLanguages.splice(shellCurlIndex, 1);
+ allLanguages.unshift(shellCurl);
+ }
+ }
+
+ return allLanguages;
+};
\ No newline at end of file
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 7dc9f2e57..cd925054d 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -132,6 +132,10 @@ export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid);
};
+export const findEnvironmentInCollectionByName = (collection, name) => {
+ return find(collection.environments, (e) => e.name === name);
+};
+
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
@@ -372,6 +376,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
break;
}
break;
+ case 'apikey':
+ di.request.auth.apikey = {
+ key: get(si.request, 'auth.apikey.key', ''),
+ value: get(si.request, 'auth.apikey.value', ''),
+ placement: get(si.request, 'auth.apikey.placement', 'header')
+ };
+ break;
+ case 'wsse':
+ di.request.auth.wsse = {
+ username: get(si.request, 'auth.wsse.username', ''),
+ password: get(si.request, 'auth.wsse.password', '')
+ };
+ break;
default:
break;
}
@@ -661,6 +678,30 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'OAuth 2.0';
break;
}
+ case 'wsse': {
+ label = 'WSSE Auth';
+ break;
+ }
+ case 'apikey': {
+ label = 'API Key';
+ break;
+ }
+ }
+
+ return label;
+};
+
+export const humanizeRequestAPIKeyPlacement = (placement) => {
+ let label = 'Header';
+ switch (placement) {
+ case 'header': {
+ label = 'Header';
+ break;
+ }
+ case 'queryparams': {
+ label = 'Query Params';
+ break;
+ }
}
return label;
@@ -745,6 +786,19 @@ export const getDefaultRequestPaneTab = (item) => {
}
};
+export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
+ let variables = {};
+ const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
+ if (environment) {
+ each(environment.variables, (variable) => {
+ if (variable.name && variable.value && variable.enabled) {
+ variables[variable.name] = variable.value;
+ }
+ });
+ }
+ return variables;
+};
+
export const getEnvironmentVariables = (collection) => {
let variables = {};
if (collection) {
@@ -761,6 +815,7 @@ export const getEnvironmentVariables = (collection) => {
return variables;
};
+
const getPathParams = (item) => {
let pathParams = {};
if (item && item.request && item.request.params) {
@@ -787,24 +842,28 @@ export const getTotalRequestCountInCollection = (collection) => {
};
export const getAllVariables = (collection, item) => {
- const environmentVariables = getEnvironmentVariables(collection);
- let requestVariables = {};
- if (item?.request) {
- const requestTreePath = getTreePathFromCollectionToItem(collection, item);
- requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
- }
+ if(!collection) return {};
+ const envVariables = getEnvironmentVariables(collection);
+ const requestTreePath = getTreePathFromCollectionToItem(collection, item);
+ let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
const pathParams = getPathParams(item);
+ const { globalEnvironmentVariables = {} } = collection;
+
+ const { processEnvVariables = {}, runtimeVariables = {} } = collection;
return {
- ...environmentVariables,
+ ...globalEnvironmentVariables,
+ ...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
...requestVariables,
- ...collection.runtimeVariables,
+ ...runtimeVariables,
pathParams: {
...pathParams
},
process: {
env: {
- ...collection.processEnvVariables
+ ...processEnvVariables
}
}
};
@@ -831,14 +890,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
-const mergeFolderLevelVars = (request, requestTreePath = []) => {
+const mergeVars = (collection, requestTreePath = []) => {
+ let collectionVariables = {};
+ let folderVariables = {};
let requestVariables = {};
+ let collectionRequestVars = get(collection, 'root.request.vars.req', []);
+ collectionRequestVars.forEach((_var) => {
+ if (_var.enabled) {
+ collectionVariables[_var.name] = _var.value;
+ }
+ });
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
- requestVariables[_var.name] = _var.value;
+ folderVariables[_var.name] = _var.value;
}
});
} else {
@@ -850,6 +917,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
});
}
}
-
- return requestVariables;
+ return {
+ collectionVariables,
+ folderVariables,
+ requestVariables
+ };
};
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 05f1bad2f..893c6f7fe 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -158,3 +158,11 @@ export const humanizeDate = (dateString) => {
day: 'numeric'
});
};
+
+export const generateUidBasedOnHash = (str) => {
+ const hash = simpleHash(str);
+
+ return `${hash}`.padEnd(21, '0');
+};
+
+export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
diff --git a/packages/bruno-app/src/utils/common/setupPolyfills.js b/packages/bruno-app/src/utils/common/setupPolyfills.js
new file mode 100644
index 000000000..8d42d17b9
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/setupPolyfills.js
@@ -0,0 +1,24 @@
+export const setupPolyfills = () => {
+ // polyfill required to make react-pdf
+ if (typeof Promise.withResolvers === "undefined") {
+ if (typeof window !== 'undefined') {
+ window.Promise.withResolvers = function () {
+ let resolve, reject
+ const promise = new Promise((res, rej) => {
+ resolve = res
+ reject = rej
+ })
+ return { promise, resolve, reject }
+ }
+ } else {
+ global.Promise.withResolvers = function () {
+ let resolve, reject
+ const promise = new Promise((res, rej) => {
+ resolve = res
+ reject = rej
+ })
+ return { promise, resolve, reject }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js
index 7260942f4..9511ffce0 100644
--- a/packages/bruno-app/src/utils/exporters/postman-collection.js
+++ b/packages/bruno-app/src/utils/exporters/postman-collection.js
@@ -1,6 +1,105 @@
import map from 'lodash/map';
import * as FileSaver from 'file-saver';
-import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export';
+import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export';
+
+/**
+ * Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
+ *
+ * @param {string} url - The raw URL to be transformed.
+ * @param {Object} params - The params object.
+ * @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.
+ */
+export const transformUrl = (url, params) => {
+ if (typeof url !== 'string' || !url.trim()) {
+ throw new Error("Invalid URL input");
+ }
+
+ const urlRegexPatterns = {
+ protocolAndRestSeparator: /:\/\//,
+ hostAndPathSeparator: /\/(.+)/,
+ domainSegmentSeparator: /\./,
+ pathSegmentSeparator: /\//,
+ queryStringSeparator: /\?/
+ };
+
+ const postmanUrl = { raw: url };
+
+ /**
+ * Splits a URL into its protocol, host and path.
+ *
+ * @param {string} url - The URL to be split.
+ * @returns {Object} An object containing the protocol and the raw host/path string.
+ */
+ const splitUrl = (url) => {
+ const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
+ if (urlParts.length === 1) {
+ return { protocol: '', rawHostAndPath: urlParts[0] };
+ } else if (urlParts.length === 2) {
+ const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
+ return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
+ } else {
+ throw new Error(`Invalid URL format: ${url}`);
+ }
+ };
+
+ /**
+ * Splits the host and path from a raw host/path string.
+ *
+ * @param {string} rawHostAndPath - The raw host and path string to be split.
+ * @returns {Object} An object containing the host and path.
+ */
+ const splitHostAndPath = (rawHostAndPath) => {
+ const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
+ return { host, path };
+ };
+
+ try {
+ const { protocol, rawHostAndPath } = splitUrl(url);
+ postmanUrl.protocol = protocol;
+
+ const { host, path } = splitHostAndPath(rawHostAndPath);
+ postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];
+ postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];
+ } catch (error) {
+ console.error(error.message);
+ return {};
+ }
+
+ // Construct query params.
+ postmanUrl.query = params
+ .filter((param) => param.type === 'query')
+ .map(({ name, value, description }) => ({ key: name, value, description }));
+
+ // Construct path params.
+ postmanUrl.variable = params
+ .filter((param) => param.type === 'path')
+ .map(({ name, value, description }) => ({ key: name, value, description }));
+
+ return postmanUrl;
+};
+
+/**
+ * Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
+ *
+ * @param {String} url - A URL string
+ * @returns {String} The sanitized URL
+ *
+ */
+const collapseDuplicateSlashes = (url) => {
+ return url.replace(/(? {
+ let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
+ return sanitizedUrl;
+};
export const exportCollection = (collection) => {
delete collection.uid;
@@ -147,7 +246,7 @@ export const exportCollection = (collection) => {
};
const generateAuth = (itemAuth) => {
- switch (itemAuth) {
+ switch (itemAuth?.mode) {
case 'bearer':
return {
type: 'bearer',
@@ -174,52 +273,41 @@ export const exportCollection = (collection) => {
]
};
}
+ case 'apikey': {
+ return {
+ type: 'apikey',
+ apikey: [
+ {
+ key: 'key',
+ value: itemAuth.apikey.key,
+ type: 'string'
+ },
+ {
+ key: 'value',
+ value: itemAuth.apikey.value,
+ type: 'string'
+ }
+ ]
+ };
+ }
+ default: {
+ console.error('Unsupported auth mode:', itemAuth.mode);
+ return null;
+ }
}
};
- const generateHost = (url) => {
- try {
- const { hostname } = new URL(url);
- return hostname.split('.');
- } catch (error) {
- console.error(`Invalid URL: ${url}`, error);
- return [];
- }
- };
-
- const generatePathParams = (params) => {
- return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`);
- };
-
- const generateQueryParams = (params) => {
- return params
- .filter((param) => param.type === 'query')
- .map(({ name, value, description }) => ({ key: name, value, description }));
- };
-
- const generateVariables = (params) => {
- return params
- .filter((param) => param.type === 'path')
- .map(({ name, value, description }) => ({ key: name, value, description }));
- };
-
const generateRequestSection = (itemRequest) => {
const requestObject = {
method: itemRequest.method,
header: generateHeaders(itemRequest.headers),
auth: generateAuth(itemRequest.auth),
description: itemRequest.docs,
- url: {
- raw: itemRequest.url,
- host: generateHost(itemRequest.url),
- path: generatePathParams(itemRequest.params),
- query: generateQueryParams(itemRequest.params),
- variable: generateVariables(itemRequest.params)
- },
- auth: generateAuth(itemRequest.auth)
+ // We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.
+ url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
};
- if (itemRequest.body.mode != 'none') {
+ if (itemRequest.body.mode !== 'none') {
requestObject.body = generateBody(itemRequest.body);
}
return requestObject;
diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.spec.js b/packages/bruno-app/src/utils/exporters/postman-collection.spec.js
new file mode 100644
index 000000000..23c6690cc
--- /dev/null
+++ b/packages/bruno-app/src/utils/exporters/postman-collection.spec.js
@@ -0,0 +1,81 @@
+const { sanitizeUrl, transformUrl } = require('./postman-collection');
+
+describe('transformUrl', () => {
+ it('should handle basic URL with path variables', () => {
+ const url = 'https://example.com/{{username}}/api/resource/:id';
+ const params = [
+ { name: 'id', value: '123', type: 'path' },
+ ];
+
+ const result = transformUrl(url, params);
+
+ expect(result).toEqual({
+ raw: 'https://example.com/{{username}}/api/resource/:id',
+ protocol: 'https',
+ host: ['example', 'com'],
+ path: ['{{username}}', 'api', 'resource', ':id'],
+ query: [],
+ variable: [
+ { key: 'id', value: '123' },
+ ]
+ });
+ });
+
+ it('should handle URL with query parameters', () => {
+ const url = 'https://example.com/api/resource?limit=10&offset=20';
+ const params = [
+ { name: 'limit', value: '10', type: 'query' },
+ { name: 'offset', value: '20', type: 'query' }
+ ];
+
+ const result = transformUrl(url, params);
+
+ expect(result).toEqual({
+ raw: 'https://example.com/api/resource?limit=10&offset=20',
+ protocol: 'https',
+ host: ['example', 'com'],
+ path: ['api', 'resource'],
+ query: [
+ { key: 'limit', value: '10' },
+ { key: 'offset', value: '20' }
+ ],
+ variable: []
+ });
+ });
+
+ it('should handle URL without protocol', () => {
+ const url = 'example.com/api/resource';
+ const params = [];
+
+ const result = transformUrl(url, params);
+
+ expect(result).toEqual({
+ raw: 'example.com/api/resource',
+ protocol: '',
+ host: ['example', 'com'],
+ path: ['api', 'resource'],
+ query: [],
+ variable: []
+ });
+ });
+});
+
+describe('sanitizeUrl', () => {
+ it('should replace backslashes with slashes', () => {
+ const input = 'http:\\\\example.com\\path\\to\\file';
+ const expected = 'http://example.com/path/to/file';
+ expect(sanitizeUrl(input)).toBe(expected);
+ });
+
+ it('should collapse multiple slashes into a single slash', () => {
+ const input = 'http://example.com//path///to////file';
+ const expected = 'http://example.com/path/to/file';
+ expect(sanitizeUrl(input)).toBe(expected);
+ });
+
+ it('should handle URLs with mixed slashes', () => {
+ const input = 'http:\\example.com//path\\to//file';
+ const expected = 'http://example.com/path/to/file';
+ expect(sanitizeUrl(input)).toBe(expected);
+ });
+})
diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js
index 0fec995ca..74b56009d 100644
--- a/packages/bruno-app/src/utils/importers/insomnia-collection.js
+++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js
@@ -17,7 +17,7 @@ const readFile = (files) => {
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
- const parsedData = jsyaml.load(e.target.result);
+ const parsedData = jsyaml.load(e.target.result, { schema: jsyaml.CORE_SCHEMA });
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
@@ -60,6 +60,7 @@ const addSuffixToDuplicateName = (item, index, allItems) => {
const regexVariable = new RegExp('{{.*?}}', 'g');
const normalizeVariables = (value) => {
+ value = value || '';
const variables = value.match(regexVariable) || [];
each(variables, (variable) => {
value = value.replace(variable, variable.replace('_.', '').replaceAll(' ', ''));
@@ -174,7 +175,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
} else if (mimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = request.body.text;
- } else if (mimeType === 'text/xml') {
+ } else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = request.body.text;
} else if (mimeType === 'application/graphql') {
diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js
index 5966563a7..01407878a 100644
--- a/packages/bruno-app/src/utils/importers/openapi-collection.js
+++ b/packages/bruno-app/src/utils/importers/openapi-collection.js
@@ -31,9 +31,8 @@ const readFile = (files) => {
};
const ensureUrl = (url) => {
- let protUrl = url.startsWith('http') ? url : `http://${url}`;
- // replace any double or triple slashes
- return protUrl.replace(/([^:]\/)\/+/g, '$1');
+ // emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
+ return url.replace(/([^:])\/{2,}/g, '$1/');
};
const buildEmptyJsonBody = (bodySchema) => {
@@ -41,9 +40,12 @@ const buildEmptyJsonBody = (bodySchema) => {
each(bodySchema.properties || {}, (prop, name) => {
if (prop.type === 'object') {
_jsonBody[name] = buildEmptyJsonBody(prop);
- // handle arrays
} else if (prop.type === 'array') {
- _jsonBody[name] = [];
+ if (prop.items && prop.items.type === 'object') {
+ _jsonBody[name] = [buildEmptyJsonBody(prop.items)];
+ } else {
+ _jsonBody[name] = [];
+ }
} else {
_jsonBody[name] = '';
}
@@ -67,7 +69,7 @@ const transformOpenapiRequestItem = (request) => {
name: operationName,
type: 'http-request',
request: {
- url: ensureUrl(request.global.server + '/' + path),
+ url: ensureUrl(request.global.server + path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
@@ -165,6 +167,9 @@ const transformOpenapiRequestItem = (request) => {
let _jsonBody = buildEmptyJsonBody(bodySchema);
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
}
+ if (bodySchema && bodySchema.type === 'array') {
+ brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
+ }
} else if (mimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
if (bodySchema && bodySchema.type === 'object') {
@@ -224,7 +229,7 @@ const transformOpenapiRequestItem = (request) => {
return brunoRequestItem;
};
-const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
+const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
@@ -248,7 +253,7 @@ const resolveRefs = (spec, components = spec.components, visitedItems = new Set(
let ref = components;
for (const key of refKeys) {
- if (ref[key]) {
+ if (ref && ref[key]) {
ref = ref[key];
} else {
// Handle invalid references gracefully?
@@ -278,11 +283,16 @@ const groupRequestsByTags = (requests) => {
each(requests, (request) => {
let tags = request.operationObject.tags || [];
if (tags.length > 0) {
- let tag = tags[0]; // take first tag
- if (!_groups[tag]) {
- _groups[tag] = [];
+ let tag = tags[0].trim(); // take first tag and trim whitespace
+
+ if (tag) {
+ if (!_groups[tag]) {
+ _groups[tag] = [];
+ }
+ _groups[tag].push(request);
+ } else {
+ ungrouped.push(request);
}
- _groups[tag].push(request);
} else {
ungrouped.push(request);
}
@@ -306,7 +316,7 @@ const getDefaultUrl = (serverObject) => {
url = url.replace(`{${variableName}}`, sub);
});
}
- return url;
+ return url.endsWith('/') ? url : `${url}/`;
};
const getSecurity = (apiSpec) => {
@@ -363,7 +373,7 @@ const parseOpenApiCollection = (data) => {
// Currently parsing of openapi spec is "do your best", that is
// allows "invalid" openapi spec
- // assumes v3 if not defined. v2 no supported yet
+ // Assumes v3 if not defined. v2 is not supported yet
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
return;
@@ -372,7 +382,28 @@ const parseOpenApiCollection = (data) => {
// TODO what if info.title not defined?
brunoCollection.name = collectionData.info.title;
let servers = collectionData.servers || [];
- let baseUrl = servers[0] ? getDefaultUrl(servers[0]) : '';
+
+ // Create environments based on the servers
+ servers.forEach((server, index) => {
+ let baseUrl = getDefaultUrl(server);
+ let environmentName = server.description ? server.description : `Environment ${index + 1}`;
+
+ brunoCollection.environments.push({
+ uid: uuid(),
+ name: environmentName,
+ variables: [
+ {
+ uid: uuid(),
+ name: 'baseUrl',
+ value: baseUrl,
+ type: 'text',
+ enabled: true,
+ secret: false
+ },
+ ]
+ });
+ });
+
let securityConfig = getSecurity(collectionData);
let allRequests = Object.entries(collectionData.paths)
@@ -389,7 +420,7 @@ const parseOpenApiCollection = (data) => {
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
operationObject: operationObject,
global: {
- server: baseUrl,
+ server: '{{baseUrl}}',
security: securityConfig
}
};
diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js
index 3f10aea9c..5ab2dc6c0 100644
--- a/packages/bruno-app/src/utils/importers/postman-collection.js
+++ b/packages/bruno-app/src/utils/importers/postman-collection.js
@@ -1,10 +1,10 @@
-import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
+import each from 'lodash/each';
const readFile = (files) => {
return new Promise((resolve, reject) => {
@@ -23,7 +23,7 @@ const parseGraphQLRequest = (graphqlSource) => {
};
if (typeof graphqlSource === 'string') {
- graphqlSource = JSON.parse(text);
+ graphqlSource = JSON.parse(graphqlSource);
}
if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {
@@ -54,11 +54,122 @@ const convertV21Auth = (array) => {
}, {});
};
+const constructUrlFromParts = (url) => {
+ const { protocol = 'http', host, path, port, query, hash } = url || {};
+ const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';
+ const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || '';
+ const portStr = port ? `:${port}` : '';
+ const queryStr =
+ query && Array.isArray(query) && query.length > 0
+ ? `?${query
+ .filter((q) => q.key)
+ .map((q) => `${q.key}=${q.value || ''}`)
+ .join('&')}`
+ : '';
+ const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;
+ return urlStr;
+};
+
+const constructUrl = (url) => {
+ if (!url) return '';
+
+ if (typeof url === 'string') {
+ return url;
+ }
+
+ if (typeof url === 'object') {
+ const { raw } = url;
+
+ if (raw && typeof raw === 'string') {
+ // If the raw URL contains url-fragments remove it
+ if (raw.includes('#')) {
+ return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.
+ }
+ return raw;
+ }
+
+ // If no raw value exists, construct the URL from parts
+ return constructUrlFromParts(url);
+ }
+
+ return '';
+};
+
let translationLog = {};
+/* struct of translation log
+ {
+ [collectionName]: {
+ script: [index1, index2],
+ test: [index1, index2]
+ }
+ }
+ */
+
+const pushTranslationLog = (type, index) => {
+ if (!translationLog[i.name]) {
+ translationLog[i.name] = {};
+ }
+ if (!translationLog[i.name][type]) {
+ translationLog[i.name][type] = [];
+ }
+ translationLog[i.name][type].push(index + 1);
+};
+
+const importScriptsFromEvents = (events, requestObject, options, pushTranslationLog) => {
+ events.forEach((event) => {
+ if (event.script && event.script.exec) {
+ if (event.listen === 'prerequest') {
+ if (!requestObject.script) {
+ requestObject.script = {};
+ }
+
+ if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
+ requestObject.script.req = event.script.exec
+ .map((line, index) =>
+ options.enablePostmanTranslations.enabled
+ ? postmanTranslation(line, () => pushTranslationLog('script', index))
+ : `// ${line}`
+ )
+ .join('\n');
+ } else if (typeof event.script.exec === 'string') {
+ requestObject.script.req = options.enablePostmanTranslations.enabled
+ ? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
+ : `// ${event.script.exec}`;
+ } else {
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
+ }
+ }
+
+ if (event.listen === 'test') {
+ if (!requestObject.tests) {
+ requestObject.tests = {};
+ }
+
+ if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
+ requestObject.tests = event.script.exec
+ .map((line, index) =>
+ options.enablePostmanTranslations.enabled
+ ? postmanTranslation(line, () => pushTranslationLog('test', index))
+ : `// ${line}`
+ )
+ .join('\n');
+ } else if (typeof event.script.exec === 'string') {
+ requestObject.tests = options.enablePostmanTranslations.enabled
+ ? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
+ : `// ${event.script.exec}`;
+ } else {
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
+ }
+ }
+ }
+ });
+};
+
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
+ const requestMap = {};
each(item, (i) => {
if (isItemAFolder(i)) {
@@ -75,29 +186,55 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
uid: uuid(),
name: folderName,
type: 'folder',
- items: []
+ items: [],
+ root: {
+ meta: {
+ name: folderName
+ },
+ request: {
+ auth: {
+ mode: 'none',
+ basic: null,
+ bearer: null,
+ awsv4: null
+ },
+ headers: [],
+ script: {},
+ tests: '',
+ vars: {}
+ }
+ }
};
- brunoParent.items.push(brunoFolderItem);
- folderMap[folderName] = brunoFolderItem;
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
}
+
+ if (i.event) {
+ importScriptsFromEvents(i.event, brunoFolderItem.root.request, options, pushTranslationLog);
+ }
+
+ brunoParent.items.push(brunoFolderItem);
+ folderMap[folderName] = brunoFolderItem;
} else {
if (i.request) {
- let url = '';
- if (typeof i.request.url === 'string') {
- url = i.request.url;
- } else {
- url = get(i, 'request.url.raw') || '';
+ const baseRequestName = i.name;
+ let requestName = baseRequestName;
+ let count = 1;
+
+ while (requestMap[requestName]) {
+ requestName = `${baseRequestName}_${count}`;
+ count++;
}
+ const url = constructUrl(i.request.url);
+
const brunoRequestItem = {
uid: uuid(),
- name: i.name,
+ name: requestName,
type: 'http-request',
request: {
url: url,
- method: i.request.method,
+ method: i?.request?.method?.toUpperCase(),
auth: {
mode: 'none',
basic: null,
@@ -117,32 +254,14 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
docs: i.request.description
}
};
- /* struct of translation log
- {
- [collectionName]: {
- script: [index1, index2],
- test: [index1, index2]
- }
- }
- */
- // type could be script or test
- const pushTranslationLog = (type, index) => {
- if (!translationLog[i.name]) {
- translationLog[i.name] = {};
- }
- if (!translationLog[i.name][type]) {
- translationLog[i.name][type] = [];
- }
- translationLog[i.name][type].push(index + 1);
- };
if (i.event) {
i.event.forEach((event) => {
if (event.listen === 'prerequest' && event.script && event.script.exec) {
if (!brunoRequestItem.request.script) {
brunoRequestItem.request.script = {};
}
- if (Array.isArray(event.script.exec)) {
+ if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
brunoRequestItem.request.script.req = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
@@ -150,17 +269,19 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
: `// ${line}`
)
.join('\n');
- } else {
+ } else if (typeof event.script.exec === 'string') {
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
- ? postmanTranslation(event.script.exec[0], () => pushTranslationLog('script', 0))
- : `// ${event.script.exec[0]} `;
+ ? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
+ : `// ${event.script.exec}`;
+ } else {
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
if (event.listen === 'test' && event.script && event.script.exec) {
if (!brunoRequestItem.request.tests) {
brunoRequestItem.request.tests = {};
}
- if (Array.isArray(event.script.exec)) {
+ if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
brunoRequestItem.request.tests = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
@@ -168,10 +289,12 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
: `// ${line}`
)
.join('\n');
- } else {
+ } else if (typeof event.script.exec === 'string') {
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
- ? postmanTranslation(event.script.exec[0], () => pushTranslationLog('test', 0))
- : `// ${event.script.exec[0]} `;
+ ? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
+ : `// ${event.script.exec}`;
+ } else {
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
});
@@ -282,6 +405,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
region: authValues.region,
profileName: ''
};
+ } else if (auth.type === 'apikey'){
+ brunoRequestItem.request.auth.mode = 'apikey';
+ brunoRequestItem.request.auth.apikey = {
+ key: authValues.key,
+ value: authValues.value,
+ placement: "header" //By default we are placing the apikey values in headers!
+ }
}
}
@@ -296,18 +426,24 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
});
});
- each(get(i, 'request.url.variable'), (param) => {
+ each(get(i, 'request.url.variable', []), (param) => {
+ if (!param.key) {
+ // If no key, skip this iteration and discard the param
+ return;
+ }
+
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.key,
- value: param.value,
- description: param.description,
+ value: param.value ?? '',
+ description: param.description ?? '',
type: 'path',
enabled: true
});
});
brunoParent.items.push(brunoRequestItem);
+ requestMap[requestName] = brunoRequestItem;
}
}
});
@@ -334,9 +470,30 @@ const importPostmanV2Collection = (collection, options) => {
uid: uuid(),
version: '1',
items: [],
- environments: []
+ environments: [],
+ root: {
+ meta: {
+ name: collection.info.name
+ },
+ request: {
+ auth: {
+ mode: 'none',
+ basic: null,
+ bearer: null,
+ awsv4: null
+ },
+ headers: [],
+ script: {},
+ tests: '',
+ vars: {}
+ }
+ }
};
+ if (collection.event) {
+ importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
+ }
+
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
return brunoCollection;
diff --git a/packages/bruno-app/src/utils/importers/translators/postman_translation.js b/packages/bruno-app/src/utils/importers/translators/postman_translation.js
index bd60fbcac..ae5cd093d 100644
--- a/packages/bruno-app/src/utils/importers/translators/postman_translation.js
+++ b/packages/bruno-app/src/utils/importers/translators/postman_translation.js
@@ -5,6 +5,8 @@ const replacements = {
'pm\\.variables\\.set\\(': 'bru.setVar(',
'pm\\.collectionVariables\\.get\\(': 'bru.getVar(',
'pm\\.collectionVariables\\.set\\(': 'bru.setVar(',
+ 'pm\\.collectionVariables\\.has\\(': 'bru.hasVar(',
+ 'pm\\.collectionVariables\\.unset\\(': 'bru.deleteVar(',
'pm\\.setNextRequest\\(': 'bru.setNextRequest(',
'pm\\.test\\(': 'test(',
'pm.response.to.have\\.status\\(': 'expect(res.getStatus()).to.equal(',
diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js
index f9557b3c4..852b5fab3 100644
--- a/packages/bruno-app/src/utils/url/index.js
+++ b/packages/bruno-app/src/utils/url/index.js
@@ -42,15 +42,15 @@ export const parsePathParams = (url) => {
uri = `http://${uri}`;
}
+ let paths;
+
try {
uri = new URL(uri);
+ paths = uri.pathname.split('/');
} catch (e) {
- // URL is non-parsable, is it incomplete? Ignore.
- return [];
+ paths = uri.split('/');
}
- let paths = uri.pathname.split('/');
-
paths = paths.reduce((acc, path) => {
if (path !== '' && path[0] === ':') {
let name = path.slice(1, path.length);
@@ -63,7 +63,6 @@ export const parsePathParams = (url) => {
}
return acc;
}, []);
-
return paths;
};
@@ -109,12 +108,13 @@ export const isValidUrl = (url) => {
}
};
-export const interpolateUrl = ({ url, envVars, runtimeVariables, processEnvVars }) => {
+export const interpolateUrl = ({ url, globalEnvironmentVariables = {}, envVars, runtimeVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return;
}
return interpolate(url, {
+ ...globalEnvironmentVariables,
...envVars,
...runtimeVariables,
process: {
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index e856846c8..5020f8475 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -24,12 +24,12 @@
"package.json"
],
"dependencies": {
- "@aws-sdk/credential-providers": "3.525.0",
+ "@aws-sdk/credential-providers": "3.658.1",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"aws4-axios": "^3.3.0",
- "axios": "^1.5.1",
+ "axios": "1.7.5",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"decomment": "^0.9.5",
@@ -37,13 +37,11 @@
"fs-extra": "^10.1.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
- "inquirer": "^9.1.4",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
- "mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
- "vm2": "^3.9.13",
+ "@usebruno/vm2": "^3.9.13",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 53871bc57..58b3cdf80 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -235,6 +235,18 @@ const builder = async (yargs) => {
default: 'json',
type: 'string'
})
+ .option('reporter-json', {
+ describe: 'Path to write json file results to',
+ type: 'string'
+ })
+ .option('reporter-junit', {
+ describe: 'Path to write junit file results to',
+ type: 'string'
+ })
+ .option('reporter-html', {
+ describe: 'Path to write html file results to',
+ type: 'string'
+ })
.option('insecure', {
type: 'boolean',
description: 'Allow insecure server connections'
@@ -267,6 +279,10 @@ const builder = async (yargs) => {
'$0 run request.bru --output results.html --format html',
'Run a request and write the results to results.html in html format in the current directory'
)
+ .example(
+ '$0 run request.bru --reporter-junit results.xml --reporter-html results.html',
+ 'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory'
+ )
.example('$0 run request.bru --tests-only', 'Run all requests that have a test')
.example(
@@ -291,6 +307,9 @@ const handler = async function (argv) {
r: recursive,
output: outputPath,
format,
+ reporterJson,
+ reporterJunit,
+ reporterHtml,
sandbox,
testsOnly,
bail
@@ -392,6 +411,25 @@ const handler = async function (argv) {
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
}
+ let formats = {};
+
+ // Maintains back compat with --format and --output
+ if (outputPath && outputPath.length) {
+ formats[format] = outputPath;
+ }
+
+ if (reporterHtml && reporterHtml.length) {
+ formats['html'] = reporterHtml;
+ }
+
+ if (reporterJson && reporterJson.length) {
+ formats['json'] = reporterJson;
+ }
+
+ if (reporterJunit && reporterJunit.length) {
+ formats['junit'] = reporterJunit;
+ }
+
// load .env file at root of collection if it exists
const dotEnvPath = path.join(collectionPath, '.env');
const dotEnvExists = await exists(dotEnvPath);
@@ -524,28 +562,45 @@ const handler = async function (argv) {
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);
- const outputDirExists = await exists(outputDir);
- if (!outputDirExists) {
- console.error(chalk.red(`Output directory ${outputDir} does not exist`));
- process.exit(constants.EXIT_STATUS.ERROR_MISSING_OUTPUT_DIR);
- }
-
+ const formatKeys = Object.keys(formats);
+ if (formatKeys && formatKeys.length > 0) {
const outputJson = {
summary,
results
};
- if (format === 'json') {
- fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
- } else if (format === 'junit') {
- makeJUnitOutput(results, outputPath);
- } else if (format === 'html') {
- makeHtmlOutput(outputJson, outputPath);
+ const reporters = {
+ 'json': (path) => fs.writeFileSync(path, JSON.stringify(outputJson, null, 2)),
+ 'junit': (path) => makeJUnitOutput(results, path),
+ 'html': (path) => makeHtmlOutput(outputJson, path),
}
- console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
+ for (const formatter of Object.keys(formats))
+ {
+ const reportPath = formats[formatter];
+ const reporter = reporters[formatter];
+
+ // Skip formatters lacking an output path.
+ if (!reportPath || reportPath.length === 0) {
+ continue;
+ }
+
+ const outputDir = path.dirname(reportPath);
+ const outputDirExists = await exists(outputDir);
+ if (!outputDirExists) {
+ console.error(chalk.red(`Output directory ${outputDir} does not exist`));
+ process.exit(constants.EXIT_STATUS.ERROR_MISSING_OUTPUT_DIR);
+ }
+
+ if (!reporter) {
+ console.error(chalk.red(`Reporter ${formatter} does not exist`));
+ process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
+ }
+
+ reporter(reportPath);
+
+ console.log(chalk.dim(chalk.grey(`Wrote ${formatter} results to ${reportPath}`)));
+ }
}
if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) {
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index c35456993..2b727e671 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -1,5 +1,6 @@
const { interpolate } = require('@usebruno/common');
const { each, forOwn, cloneDeep, find } = require('lodash');
+const FormData = require('form-data');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -73,9 +74,17 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
} else if (contentType === 'application/x-www-form-urlencoded') {
if (typeof request.data === 'object') {
try {
- let parsed = JSON.stringify(request.data);
- parsed = _interpolate(parsed);
- request.data = JSON.parse(parsed);
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
+ } catch (err) {}
+ }
+ } else if (contentType === 'multipart/form-data') {
+ if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
+ try {
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
} catch (err) {}
}
} else {
@@ -113,7 +122,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
})
.join('');
- request.url = url.origin + interpolatedUrlPath + url.search;
+ const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
+ request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search;
}
if (request.proxy) {
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index e30f8337f..bc2b22886 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash');
const fs = require('fs');
var JSONbig = require('json-bigint');
const decomment = require('decomment');
+const crypto = require('node:crypto');
const prepareRequest = (request, collectionRoot) => {
const headers = {};
@@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => {
if (request.auth.mode === 'bearer') {
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
}
+
+ if (request.auth.mode === 'wsse') {
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('hex');
+
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
+ hash.update(nonce + ts + password);
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
+ }
}
request.body = request.body || {};
@@ -120,16 +139,10 @@ const prepareRequest = (request, collectionRoot) => {
}
if (request.body.mode === 'multipartForm') {
+ axiosRequest.headers['content-type'] = 'multipart/form-data';
const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
- each(enabledParams, (p) => {
- if (p.type === 'file') {
- params[p.name] = p.value.map((path) => fs.createReadStream(path));
- } else {
- params[p.name] = p.value;
- }
- });
- axiosRequest.headers['content-type'] = 'multipart/form-data';
+ each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index b138f7b10..83fb1ed06 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -19,6 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const path = require('path');
+const { createFormData } = require('../utils/common');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const onConsoleLog = (type, args) => {
@@ -42,38 +43,11 @@ const runSingleRequest = async function (
request = prepareRequest(bruJson.request, collectionRoot);
+ request.__bruno__executionMode = 'cli';
+
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = runtime;
- // 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') {
- const form = new FormData();
- forOwn(request.data, (value, key) => {
- if (value instanceof Array) {
- each(value, (v) => form.append(key, v));
- } else {
- form.append(key, value);
- }
- });
- extend(request.headers, form.getHeaders());
- request.data = form;
- }
-
- // run pre-request vars
- const preRequestVars = get(bruJson, 'request.vars.req');
- if (preRequestVars?.length) {
- const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
- varsRuntime.runPreRequestVars(
- preRequestVars,
- request,
- envVariables,
- runtimeVariables,
- collectionPath,
- processEnvVars
- );
- }
-
// run pre request script
const requestScriptFile = compact([
get(collectionRoot, 'request.script.req'),
@@ -209,6 +183,14 @@ const runSingleRequest = async function (
request.data = qs.stringify(request.data);
}
+ if (request?.headers?.['content-type'] === 'multipart/form-data') {
+ if (!(request?.data instanceof FormData)) {
+ let form = createFormData(request.data, collectionPath);
+ request.data = form;
+ extend(request.headers, form.getHeaders());
+ }
+ }
+
let response, responseTime;
try {
// run request
@@ -265,7 +247,7 @@ const runSingleRequest = async function (
url: null,
responseTime: 0
},
- error: err.message,
+ error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
assertionResults: [],
testResults: [],
nextRequestName: nextRequestName
@@ -277,7 +259,7 @@ const runSingleRequest = async function (
console.log(
chalk.green(stripExtension(filename)) +
- chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
+ chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
);
// run post-response vars
diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js
index 225156484..8f61066c5 100644
--- a/packages/bruno-cli/src/utils/axios-instance.js
+++ b/packages/bruno-cli/src/utils/axios-instance.js
@@ -1,4 +1,5 @@
const axios = require('axios');
+const { CLI_VERSION } = require('../constants');
/**
* Function that configures axios with timing interceptors
@@ -8,7 +9,11 @@ const axios = require('axios');
*/
function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */
- const instance = axios.create();
+ const instance = axios.create({
+ headers: {
+ "User-Agent": `bruno-runtime/${CLI_VERSION}`
+ }
+ });
instance.interceptors.request.use((config) => {
config.headers['request-start-time'] = Date.now();
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index 713ef928a..75707fef9 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -1,12 +1,6 @@
const _ = require('lodash');
-const Mustache = require('mustache');
const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
const collectionBruToJson = (bru) => {
try {
const json = _collectionBruToJson(bru);
@@ -95,7 +89,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
_.each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js
index 704928022..16c2d1a7b 100644
--- a/packages/bruno-cli/src/utils/common.js
+++ b/packages/bruno-cli/src/utils/common.js
@@ -1,3 +1,8 @@
+const fs = require('fs');
+const FormData = require('form-data');
+const { forOwn } = require('lodash');
+const path = require('path');
+
const lpad = (str, width) => {
let paddedStr = str;
while (paddedStr.length < width) {
@@ -14,7 +19,33 @@ const rpad = (str, width) => {
return paddedStr;
};
+const createFormData = (datas, collectionPath) => {
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ const form = new FormData();
+ forOwn(datas, (value, key) => {
+ if (typeof value == 'string') {
+ form.append(key, value);
+ return;
+ }
+
+ const filePaths = value || [];
+ filePaths?.forEach?.((filePath) => {
+ let trimmedFilePath = filePath.trim();
+
+ if (!path.isAbsolute(trimmedFilePath)) {
+ trimmedFilePath = path.join(collectionPath, trimmedFilePath);
+ }
+
+ form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
+ });
+ });
+ return form;
+};
+
+
module.exports = {
lpad,
- rpad
+ rpad,
+ createFormData
};
diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json
index 1964f5ec4..df2d6f969 100644
--- a/packages/bruno-common/package.json
+++ b/packages/bruno-common/package.json
@@ -22,13 +22,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
- "rollup":"3.29.4",
+ "rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
- "rollup":"3.29.4"
+ "rollup":"3.29.5"
}
}
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index 63d3edc3a..7c4945a44 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v1.27.0",
+ "version": "v1.34.2",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -22,7 +22,7 @@
"modulePaths": ["node_modules"]
},
"dependencies": {
- "@aws-sdk/credential-providers": "3.525.0",
+ "@aws-sdk/credential-providers": "3.658.1",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
@@ -30,7 +30,7 @@
"@usebruno/schema": "0.7.0",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
- "axios": "^1.5.1",
+ "axios": "1.7.5",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
"content-disposition": "^0.5.4",
@@ -48,16 +48,14 @@
"iconv-lite": "^0.6.3",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
- "json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
- "mustache": "^4.2.0",
"nanoid": "3.3.4",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
"uuid": "^9.0.0",
- "vm2": "^3.9.13",
+ "@usebruno/vm2": "^3.9.13",
"yup": "^0.32.11"
},
"optionalDependencies": {
@@ -65,6 +63,6 @@
},
"devDependencies": {
"electron": "31.2.1",
- "electron-builder": "23.0.2"
+ "electron-builder": "25.1.8"
}
}
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 589cd29d8..82d116d81 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -12,6 +12,7 @@ const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
+const UiStateSnapshot = require('../store/ui-state-snapshot');
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -201,7 +202,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
const payload = {
collectionUid,
processEnvVariables: {
- ...process.env,
...jsonData
}
};
@@ -331,7 +331,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
const payload = {
collectionUid,
processEnvVariables: {
- ...process.env,
...jsonData
}
};
@@ -423,6 +422,13 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
};
+const onWatcherSetupComplete = (win, collectionPath) => {
+ const UiStateSnapshotStore = new UiStateSnapshot();
+ const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
+ const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath);
+ win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState);
+};
+
class Watcher {
constructor() {
this.watchers = {};
@@ -458,6 +464,7 @@ class Watcher {
let startedNewWatcher = false;
watcher
+ .on('ready', () => onWatcherSetupComplete(win, watchPath))
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index db5deecae..47ab8ae7c 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -23,6 +23,7 @@ const registerPreferencesIpc = require('./ipc/preferences');
const Watcher = require('./app/watcher');
const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window');
const registerNotificationsIpc = require('./ipc/notifications');
+const registerGlobalEnvironmentsIpc = require('./ipc/global-environments');
const lastOpenedCollections = new LastOpenedCollections();
@@ -129,13 +130,21 @@ app.on('ready', async () => {
}
});
- mainWindow.webContents.setWindowOpenHandler((details) => {
- require('electron').shell.openExternal(details.url);
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ try {
+ const { protocol } = new URL(url);
+ if (['https:', 'http:'].includes(protocol)) {
+ require('electron').shell.openExternal(url);
+ }
+ } catch (e) {
+ console.error(e);
+ }
return { action: 'deny' };
});
// register all ipc handlers
registerNetworkIpc(mainWindow);
+ registerGlobalEnvironmentsIpc(mainWindow);
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
registerNotificationsIpc(mainWindow, watcher);
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 945c21559..30a891015 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -16,6 +16,8 @@ const {
sanitizeDirectoryName,
isWSLPath,
normalizeWslPath,
+ normalizeAndResolvePath,
+ safeToRename
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@@ -23,9 +25,11 @@ const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
+const UiStateSnapshotStore = require('../store/ui-state-snapshot');
const environmentSecretsStore = new EnvironmentSecretsStore();
const collectionSecurityStore = new CollectionSecurityStore();
+const uiStateSnapshotStore = new UiStateSnapshotStore();
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -61,14 +65,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
- throw new Error(`collection: ${dirPath} already exists`);
+ const files = fs.readdirSync(dirPath);
+
+ if (files.length > 0) {
+ throw new Error(`collection: ${dirPath} already exists and is not empty`);
+ }
}
if (!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
}
- await createDirectory(dirPath);
+ if (!fs.existsSync(dirPath)) {
+ await createDirectory(dirPath);
+ }
const uid = generateUidBasedOnHash(dirPath);
const brunoConfig = {
@@ -296,7 +306,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
- if (fs.existsSync(newEnvFilePath)) {
+ if (!safeToRename(envFilePath, newEnvFilePath)) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
@@ -329,21 +339,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
// Normalize paths if they are WSL paths
- if (isWSLPath(oldPath)) {
- oldPath = normalizeWslPath(oldPath);
- }
- if (isWSLPath(newPath)) {
- newPath = normalizeWslPath(newPath);
- }
+ oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
+ newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
+ // Check if the old path exists
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
- if (fs.existsSync(newPath)) {
- throw new Error(`path: ${oldPath} already exists`);
+
+ if (!safeToRename(oldPath, newPath)) {
+ throw new Error(`path: ${newPath} already exists`);
}
- // if its directory, rename and return
if (isDirectory(oldPath)) {
const bruFilesAtSource = await searchForBruFiles(oldPath);
@@ -364,12 +371,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const jsonData = bruToJson(data);
jsonData.name = newName;
-
moveRequestUid(oldPath, newPath);
const content = jsonToBru(jsonData);
- await writeFile(newPath, content);
await fs.unlinkSync(oldPath);
+ await writeFile(newPath, content);
+
+ return newPath;
} catch (error) {
return Promise.reject(error);
}
@@ -695,6 +703,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:update-ui-state-snapshot', (event, { type, data }) => {
+ try {
+ uiStateSnapshotStore.update({ type, data });
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
diff --git a/packages/bruno-electron/src/ipc/global-environments.js b/packages/bruno-electron/src/ipc/global-environments.js
new file mode 100644
index 000000000..dc7258ee1
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/global-environments.js
@@ -0,0 +1,50 @@
+require('dotenv').config();
+const { ipcMain } = require('electron');
+const { globalEnvironmentsStore } = require('../store/global-environments');
+
+const registerGlobalEnvironmentsIpc = (mainWindow) => {
+
+ // GLOBAL ENVIRONMENTS
+
+ ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables }) => {
+ try {
+ globalEnvironmentsStore.addGlobalEnvironment({ uid, name, variables });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:save-global-environment', async (event, { environmentUid, variables }) => {
+ try {
+ globalEnvironmentsStore.saveGlobalEnvironment({ environmentUid, variables })
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:rename-global-environment', async (event, { environmentUid, name }) => {
+ try {
+ globalEnvironmentsStore.renameGlobalEnvironment({ environmentUid, name });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:delete-global-environment', async (event, { environmentUid }) => {
+ try {
+ globalEnvironmentsStore.deleteGlobalEnvironment({ environmentUid });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:select-global-environment', async (event, { environmentUid }) => {
+ try {
+ globalEnvironmentsStore.selectGlobalEnvironment({ environmentUid });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+};
+
+module.exports = registerGlobalEnvironmentsIpc;
\ No newline at end of file
diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js
index da1584d42..4f7f9f8f6 100644
--- a/packages/bruno-electron/src/ipc/network/axios-instance.js
+++ b/packages/bruno-electron/src/ipc/network/axios-instance.js
@@ -2,10 +2,12 @@ const URL = require('url');
const Socket = require('net').Socket;
const axios = require('axios');
const connectionCache = new Map(); // Cache to store checkConnection() results
+const electronApp = require("electron");
const LOCAL_IPV6 = '::1';
const LOCAL_IPV4 = '127.0.0.1';
const LOCALHOST = 'localhost';
+const version = electronApp?.app?.getVersion()?.substring(1) ?? "";
const getTld = (hostname) => {
if (!hostname) {
@@ -50,7 +52,25 @@ const checkConnection = (host, port) =>
function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */
const instance = axios.create({
- proxy: false
+ transformRequest: function transformRequest(data, headers) {
+ // doesn't apply the default transformRequest if the data is a string, so that axios doesn't add quotes see :
+ // https://github.com/usebruno/bruno/issues/2043
+ // https://github.com/axios/axios/issues/4034
+ const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
+ const hasJSONContentType = contentType.includes('json');
+ if (typeof data === 'string' && hasJSONContentType) {
+ return data;
+ }
+
+ axios.defaults.transformRequest.forEach(function (tr) {
+ data = tr.call(this, data, headers);
+ }, this);
+ return data;
+ },
+ proxy: false,
+ headers: {
+ "User-Agent": `bruno-runtime/${version}`
+ }
});
instance.interceptors.request.use(async (config) => {
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 91078bb00..4d2a28077 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -6,11 +6,10 @@ const tls = require('tls');
const axios = require('axios');
const path = require('path');
const decomment = require('decomment');
-const Mustache = require('mustache');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
-const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
+const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareCollectionRequest = require('./prepare-collection-request');
@@ -38,11 +37,8 @@ const {
} = require('./oauth2-helper');
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
-
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
+const FormData = require('form-data');
+const { createFormData } = prepareRequest;
const safeStringifyJSON = (data) => {
try {
@@ -71,7 +67,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
@@ -88,6 +84,22 @@ const getJsSandboxRuntime = (collection) => {
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
+const saveCookies = (url, headers) => {
+ if (preferencesUtil.shouldStoreCookies()) {
+ let setCookieHeaders = [];
+ if (headers['set-cookie']) {
+ setCookieHeaders = Array.isArray(headers['set-cookie'])
+ ? headers['set-cookie']
+ : [headers['set-cookie']];
+ for (let setCookieHeader of setCookieHeaders) {
+ if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
+ addCookieToJar(setCookieHeader, url);
+ }
+ }
+ }
+ }
+}
+
const configureRequest = async (
collectionUid,
request,
@@ -221,6 +233,10 @@ const configureRequest = async (
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
+ } else {
+ request.httpsAgent = new https.Agent({
+ ...httpsAgentRequestFields
+ });
}
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
@@ -245,6 +261,10 @@ const configureRequest = async (
} catch (error) {
throw new Error('Invalid system https_proxy');
}
+ } else {
+ request.httpsAgent = new https.Agent({
+ ...httpsAgentRequestFields
+ });
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
@@ -307,9 +327,24 @@ const configureRequest = async (
}
}
+ // Add API key to the URL
+ if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') {
+ const urlObj = new URL(request.url);
+
+ // Interpolate key and value as they can be variables before adding to the URL.
+ const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions);
+ const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions);
+
+ urlObj.searchParams.set(key, value);
+ request.url = urlObj.toString();
+ }
+
// Remove pathParams, already in URL (Issue #2439)
delete request.pathParams;
+ // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL
+ delete request.apiKeyAuthValueForQueryParams;
+
return axiosInstance;
};
@@ -331,10 +366,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
- if(!disableParsingResponseJson) {
+ if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
- } catch {}
+ } catch { }
return { data, dataBuffer };
};
@@ -360,20 +395,6 @@ const registerNetworkIpc = (mainWindow) => {
processEnvVars,
scriptingConfig
) => {
- // run pre-request vars
- const preRequestVars = get(request, 'vars.req', []);
- if (preRequestVars?.length) {
- const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
- varsRuntime.runPreRequestVars(
- preRequestVars,
- request,
- envVars,
- runtimeVariables,
- collectionPath,
- processEnvVars
- );
- }
-
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
@@ -396,6 +417,10 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
collectionUid
});
+
+ mainWindow.webContents.send('main:global-environment-variables-update', {
+ globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
+ });
}
// interpolate variables inside request
@@ -412,6 +437,14 @@ const registerNetworkIpc = (mainWindow) => {
request.data = qs.stringify(request.data);
}
+ if (request.headers['content-type'] === 'multipart/form-data') {
+ if (!(request.data instanceof FormData)) {
+ let form = createFormData(request.data, collectionPath);
+ request.data = form;
+ extend(request.headers, form.getHeaders());
+ }
+ }
+
return scriptResult;
};
@@ -448,6 +481,10 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
collectionUid
});
+
+ mainWindow.webContents.send('main:global-environment-variables-update', {
+ globalEnvironmentVariables: result.globalEnvironmentVariables
+ });
}
if (result?.error) {
@@ -483,6 +520,10 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
collectionUid
});
+
+ mainWindow.webContents.send('main:global-environment-variables-update', {
+ globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
+ });
}
return scriptResult;
};
@@ -504,6 +545,7 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {});
const request = prepareRequest(item, collection);
+ request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
@@ -590,17 +632,7 @@ const registerNetworkIpc = (mainWindow) => {
// save cookies
if (preferencesUtil.shouldStoreCookies()) {
- let setCookieHeaders = [];
- if (response.headers['set-cookie']) {
- setCookieHeaders = Array.isArray(response.headers['set-cookie'])
- ? response.headers['set-cookie']
- : [response.headers['set-cookie']];
- for (let setCookieHeader of setCookieHeaders) {
- if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
- addCookieToJar(setCookieHeader, request.url);
- }
- }
- }
+ saveCookies(request.url, response.headers);
}
// send domain cookies to renderer
@@ -679,6 +711,10 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
collectionUid
});
+
+ mainWindow.webContents.send('main:global-environment-variables-update', {
+ globalEnvironmentVariables: testResults.globalEnvironmentVariables
+ });
}
return {
@@ -706,7 +742,8 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {});
const _request = collectionRoot?.request;
- const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
+ const request = prepareCollectionRequest(_request, collection, collectionPath);
+ request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
@@ -951,6 +988,8 @@ const registerNetworkIpc = (mainWindow) => {
});
const request = prepareRequest(item, collection);
+ request.__bruno__executionMode = 'runner';
+
const requestUid = uuid();
const processEnvVars = getProcessEnvVars(collectionUid);
@@ -1018,6 +1057,16 @@ const registerNetworkIpc = (mainWindow) => {
response.data = data;
response.responseTime = response.headers.get('request-duration');
+ // save cookies
+ if (preferencesUtil.shouldStoreCookies()) {
+ saveCookies(request.url, response.headers);
+ }
+
+ // send domain cookies to renderer
+ const domainsWithCookies = await getDomainsWithCookies();
+
+ mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
+
mainWindow.webContents.send('main:run-folder-event', {
type: 'response-received',
responseReceived: {
@@ -1138,6 +1187,10 @@ const registerNetworkIpc = (mainWindow) => {
runtimeVariables: testResults.runtimeVariables,
collectionUid
});
+
+ mainWindow.webContents.send('main:global-environment-variables-update', {
+ globalEnvironmentVariables: testResults.globalEnvironmentVariables
+ });
}
} catch (error) {
mainWindow.webContents.send('main:run-folder-event', {
@@ -1204,7 +1257,7 @@ const registerNetworkIpc = (mainWindow) => {
try {
const disposition = contentDispositionParser.parse(contentDisposition);
return disposition && disposition.parameters['filename'];
- } catch (error) {}
+ } catch (error) { }
};
const getFileNameFromUrlPath = () => {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 899b3d0f2..59f494416 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -1,5 +1,6 @@
const { interpolate } = require('@usebruno/common');
const { each, forOwn, cloneDeep, find } = require('lodash');
+const FormData = require('form-data');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -12,15 +13,18 @@ const getContentType = (headers = {}) => {
return contentType;
};
-const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
+const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
// we clone envVars because we don't want to modify the original object
- envVars = cloneDeep(envVars);
+ envVariables = cloneDeep(envVariables);
// 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] = interpolate(value, {
+ forOwn(envVariables, (value, key) => {
+ envVariables[key] = interpolate(value, {
process: {
env: {
...processEnvVars
@@ -36,7 +40,10 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
// runtimeVariables take precedence over envVars
const combinedVars = {
- ...envVars,
+ ...globalEnvironmentVariables,
+ ...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
...requestVariables,
...runtimeVariables,
process: {
@@ -63,15 +70,29 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
if (request.data.length) {
request.data = _interpolate(request.data);
}
- }
- } else if (contentType === 'application/x-www-form-urlencoded') {
- if (typeof request.data === 'object') {
+ } else if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (contentType === 'application/x-www-form-urlencoded') {
+ if (typeof request.data === 'object') {
+ try {
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
+ } catch (err) {}
+ }
+ } else if (contentType === 'multipart/form-data') {
+ if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
+ try {
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
+ } catch (err) {}
+ }
} else {
request.data = _interpolate(request.data);
}
@@ -107,7 +128,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
})
.join('');
- request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search;
+ const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
+ request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + url.search;
}
if (request.proxy) {
@@ -202,6 +224,12 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
}
+ // interpolate vars for wsse auth
+ if (request.wsse) {
+ request.wsse.username = _interpolate(request.wsse.username) || '';
+ request.wsse.password = _interpolate(request.wsse.password) || '';
+ }
+
return request;
};
diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
index 7a1a5b503..144542418 100644
--- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js
+++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
@@ -23,14 +23,13 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
let requestCopy = cloneDeep(request);
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
const oAuth = get(requestCopy, 'oauth2', {});
- const { clientId, clientSecret, callbackUrl, scope, state, pkce } = oAuth;
+ const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: callbackUrl,
client_id: clientId,
- client_secret: clientSecret,
- state: state
+ client_secret: clientSecret
};
if (pkce) {
data['code_verifier'] = codeVerifier;
diff --git a/packages/bruno-electron/src/ipc/network/prepare-collection-request.js b/packages/bruno-electron/src/ipc/network/prepare-collection-request.js
index 5fd630594..e4d06c625 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-collection-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-collection-request.js
@@ -1,7 +1,8 @@
const { get, each } = require('lodash');
const { setAuthHeaders } = require('./prepare-request');
-const prepareCollectionRequest = (request, collectionRoot) => {
+const prepareCollectionRequest = (request, collection) => {
+ const collectionRoot = get(collection, 'root', {});
const headers = {};
let contentTypeDefined = false;
let url = request.url;
@@ -34,6 +35,8 @@ const prepareCollectionRequest = (request, collectionRoot) => {
};
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
+
+ axiosRequest.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
if (request.script) {
axiosRequest.script = request.script;
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 74cb85ac6..c8b36bb89 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,10 +1,12 @@
const os = require('os');
-const { get, each, filter, extend, compact } = require('lodash');
+const { get, each, filter, compact, forOwn } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
+const crypto = require('node:crypto');
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
+const { buildFormUrlEncodedPayload } = require('../../utils/common');
const mergeFolderLevelHeaders = (request, requestTreePath) => {
let folderHeaders = new Map();
@@ -44,73 +46,75 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
};
-const mergeFolderLevelVars = (request, requestTreePath) => {
- let folderReqVars = new Map();
+const mergeVars = (collection, request, requestTreePath) => {
+ let reqVars = new Map();
+ let collectionRequestVars = get(collection, 'root.request.vars.req', []);
+ let collectionVariables = {};
+ collectionRequestVars.forEach((_var) => {
+ if (_var.enabled) {
+ reqVars.set(_var.name, _var.value);
+ collectionVariables[_var.name] = _var.value;
+ }
+ });
+ let folderVariables = {};
+ let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
- folderReqVars.set(_var.name, _var.value);
+ reqVars.set(_var.name, _var.value);
+ folderVariables[_var.name] = _var.value;
}
});
- } else if (i.uid === request.uid) {
+ } else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
- folderReqVars.set(_var.name, _var.value);
+ reqVars.set(_var.name, _var.value);
+ requestVariables[_var.name] = _var.value;
}
});
}
}
- let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
- let requestReqVars = request?.vars?.req || [];
- let requestReqVarsMap = new Map();
- for (let _var of requestReqVars) {
- if (_var.enabled) {
- requestReqVarsMap.set(_var.name, _var.value);
- }
- }
- mergedFolderReqVars.forEach((_var) => {
- requestReqVarsMap.set(_var.name, _var.value);
- });
- request.vars.req = Array.from(requestReqVarsMap, ([name, value]) => ({
+
+ request.collectionVariables = collectionVariables;
+ request.folderVariables = folderVariables;
+ request.requestVariables = requestVariables;
+
+ request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
- let folderResVars = new Map();
+ let resVars = new Map();
+ let collectionResponseVars = get(collection, 'root.request.vars.res', []);
+ collectionResponseVars.forEach((_var) => {
+ if (_var.enabled) {
+ resVars.set(_var.name, _var.value);
+ }
+ });
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
- folderResVars.set(_var.name, _var.value);
+ resVars.set(_var.name, _var.value);
}
});
- } else if (i.uid === request.uid) {
+ } else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
- folderResVars.set(_var.name, _var.value);
+ resVars.set(_var.name, _var.value);
}
});
}
}
- let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
- let requestResVars = request?.vars?.res || [];
- let requestResVarsMap = new Map();
- for (let _var of requestResVars) {
- if (_var.enabled) {
- requestResVarsMap.set(_var.name, _var.value);
- }
- }
- mergedFolderResVars.forEach((_var) => {
- requestResVarsMap.set(_var.name, _var.value);
- });
- request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
+
+ request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
@@ -162,27 +166,26 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
}
};
-const parseFormData = (datas, collectionPath) => {
+const createFormData = (datas, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
- datas.forEach((item) => {
- const value = item.value;
- const name = item.name;
- if (item.type === 'file') {
- const filePaths = value || [];
- filePaths.forEach((filePath) => {
- let trimmedFilePath = filePath.trim();
-
- if (!path.isAbsolute(trimmedFilePath)) {
- trimmedFilePath = path.join(collectionPath, trimmedFilePath);
- }
-
- form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
- });
- } else {
- form.append(name, value);
+ forOwn(datas, (value, key) => {
+ if (typeof value == 'string') {
+ form.append(key, value);
+ return;
}
+
+ const filePaths = value || [];
+ filePaths?.forEach?.((filePath) => {
+ let trimmedFilePath = filePath.trim();
+
+ if (!path.isAbsolute(trimmedFilePath)) {
+ trimmedFilePath = path.join(collectionPath, trimmedFilePath);
+ }
+
+ form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
+ });
});
return form;
};
@@ -216,6 +219,32 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'digest.password')
};
break;
+ case 'wsse':
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('hex');
+
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
+ hash.update(nonce + ts + password);
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
+ break;
+ case 'apikey':
+ const apiKeyAuth = get(collectionAuth, 'apikey');
+ if (apiKeyAuth.placement === 'header') {
+ axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
+ } else if (apiKeyAuth.placement === 'queryparams') {
+ // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
+ axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
+ }
+ break;
}
}
@@ -284,6 +313,32 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
break;
}
break;
+ case 'wsse':
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('hex');
+
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
+ hash.update(nonce + ts + password);
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
+ break;
+ case 'apikey':
+ const apiKeyAuth = get(request, 'auth.apikey');
+ if (apiKeyAuth.placement === 'header') {
+ axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
+ } else if (apiKeyAuth.placement === 'queryparams') {
+ // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
+ axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
+ }
+ break;
}
}
@@ -298,7 +353,7 @@ const prepareRequest = (item, collection) => {
let contentTypeDefined = false;
let url = request.url;
- // collection headers
+ // Collection level headers
each(get(collectionRoot, 'request.headers', []), (h) => {
if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value;
@@ -314,9 +369,11 @@ const prepareRequest = (item, collection) => {
if (requestTreePath && requestTreePath.length > 0) {
mergeFolderLevelHeaders(request, requestTreePath);
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
- mergeFolderLevelVars(request, requestTreePath);
+ mergeVars(collection, request, requestTreePath);
+ request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
}
+ // Request level headers
each(request.headers, (h) => {
if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value;
@@ -370,18 +427,19 @@ const prepareRequest = (item, collection) => {
}
if (request.body.mode === 'formUrlEncoded') {
- axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
- const params = {};
+ if (!contentTypeDefined) {
+ axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
+ }
const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
- each(enabledParams, (p) => (params[p.name] = p.value));
- axiosRequest.data = params;
+ axiosRequest.data = buildFormUrlEncodedPayload(enabledParams);
}
if (request.body.mode === 'multipartForm') {
+ axiosRequest.headers['content-type'] = 'multipart/form-data';
+ const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
- const form = parseFormData(enabledParams, collectionPath);
- extend(axiosRequest.headers, form.getHeaders());
- axiosRequest.data = form;
+ each(enabledParams, (p) => (params[p.name] = p.value));
+ axiosRequest.data = params;
}
if (request.body.mode === 'graphql') {
@@ -401,6 +459,10 @@ const prepareRequest = (item, collection) => {
}
axiosRequest.vars = request.vars;
+ axiosRequest.collectionVariables = request.collectionVariables;
+ axiosRequest.folderVariables = request.folderVariables;
+ axiosRequest.requestVariables = request.requestVariables;
+ axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
axiosRequest.assertions = request.assertions;
return axiosRequest;
@@ -408,3 +470,4 @@ const prepareRequest = (item, collection) => {
module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders;
+module.exports.createFormData = createFormData;
diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js
index 0486ead5e..4c9c34d99 100644
--- a/packages/bruno-electron/src/ipc/preferences.js
+++ b/packages/bruno-electron/src/ipc/preferences.js
@@ -2,6 +2,7 @@ const { ipcMain } = require('electron');
const { getPreferences, savePreferences, preferencesUtil } = require('../store/preferences');
const { isDirectory } = require('../utils/filesystem');
const { openCollection } = require('../app/collections');
+const { globalEnvironmentsStore } = require('../store/global-environments');
``;
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
ipcMain.handle('renderer:ready', async (event) => {
@@ -9,10 +10,17 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
const preferences = getPreferences();
mainWindow.webContents.send('main:load-preferences', preferences);
+ // load system proxy vars
const systemProxyVars = preferencesUtil.getSystemProxyEnvVariables();
const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {};
mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy });
+ // load global environments
+ const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
+ let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();
+ activeGlobalEnvironmentUid = globalEnvironments?.find(env => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null;
+ mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid });
+
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();
diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js
new file mode 100644
index 000000000..7c0540d95
--- /dev/null
+++ b/packages/bruno-electron/src/store/global-environments.js
@@ -0,0 +1,132 @@
+const _ = require('lodash');
+const Store = require('electron-store');
+const { encryptString, decryptString } = require('../utils/encryption');
+
+class GlobalEnvironmentsStore {
+ constructor() {
+ this.store = new Store({
+ name: 'global-environments',
+ clearInvalidConfig: true
+ });
+ }
+
+ isValidValue(val) {
+ return typeof val === 'string' && val.length >= 0;
+ }
+
+ encryptGlobalEnvironmentVariables({ globalEnvironments }) {
+ return globalEnvironments?.map(env => {
+ const variables = env.variables?.map(v => ({
+ ...v,
+ value: v?.secret ? (this.isValidValue(v.value) ? encryptString(v.value) : '') : v?.value
+ })) || [];
+
+ return {
+ ...env,
+ variables
+ };
+ });
+ }
+
+ decryptGlobalEnvironmentVariables({ globalEnvironments }) {
+ return globalEnvironments?.map(env => {
+ const variables = env.variables?.map(v => ({
+ ...v,
+ value: v?.secret ? (this.isValidValue(v.value) ? decryptString(v.value) : '') : v?.value
+ })) || [];
+
+ return {
+ ...env,
+ variables
+ };
+ });
+ }
+
+ getGlobalEnvironments() {
+ let globalEnvironments = this.store.get('environments', []);
+ globalEnvironments = this.decryptGlobalEnvironmentVariables({ globalEnvironments });
+ return globalEnvironments;
+ }
+
+ getActiveGlobalEnvironmentUid() {
+ return this.store.get('activeGlobalEnvironmentUid', null);
+ }
+
+ setGlobalEnvironments(globalEnvironments) {
+ globalEnvironments = this.encryptGlobalEnvironmentVariables({ globalEnvironments });
+ return this.store.set('environments', globalEnvironments);
+ }
+
+ setActiveGlobalEnvironmentUid(uid) {
+ return this.store.set('activeGlobalEnvironmentUid', uid);
+ }
+
+ addGlobalEnvironment({ uid, name, variables = [] }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ globalEnvironments.push({
+ uid,
+ name,
+ variables
+ });
+ this.setGlobalEnvironments(globalEnvironments);
+ }
+
+ saveGlobalEnvironment({ environmentUid: globalEnvironmentUid, variables }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ const environment = globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ globalEnvironments = globalEnvironments.filter(env => env?.uid !== globalEnvironmentUid);
+ if (environment) {
+ environment.variables = variables;
+ }
+ globalEnvironments.push(environment);
+ this.setGlobalEnvironments(globalEnvironments);
+
+ }
+
+ renameGlobalEnvironment({ environmentUid: globalEnvironmentUid, name }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ const environment = globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ globalEnvironments = globalEnvironments.filter(env => env?.uid !== globalEnvironmentUid);
+ if (environment) {
+ environment.name = name;
+ }
+ globalEnvironments.push(environment);
+ this.setGlobalEnvironments(globalEnvironments);
+ }
+
+ copyGlobalEnvironment({ uid, name, variables }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ globalEnvironments.push({
+ uid,
+ name,
+ variables
+ });
+ this.setGlobalEnvironments(globalEnvironments);
+ }
+
+ selectGlobalEnvironment({ environmentUid: globalEnvironmentUid }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ const environment = globalEnvironments.find(env => env?.uid == globalEnvironmentUid);
+ if (environment) {
+ this.setActiveGlobalEnvironmentUid(globalEnvironmentUid);
+ } else {
+ this.setActiveGlobalEnvironmentUid(null);
+ }
+ }
+
+ deleteGlobalEnvironment({ environmentUid }) {
+ let globalEnvironments = this.getGlobalEnvironments();
+ let activeGlobalEnvironmentUid = this.getActiveGlobalEnvironmentUid();
+ globalEnvironments = globalEnvironments.filter(env => env?.uid !== environmentUid);
+ if (environmentUid == activeGlobalEnvironmentUid) {
+ this.setActiveGlobalEnvironmentUid(null);
+ }
+ this.setGlobalEnvironments(globalEnvironments);
+ }
+}
+
+const globalEnvironmentsStore = new GlobalEnvironmentsStore();
+
+module.exports = {
+ globalEnvironmentsStore
+};
diff --git a/packages/bruno-electron/src/store/ui-state-snapshot.js b/packages/bruno-electron/src/store/ui-state-snapshot.js
new file mode 100644
index 000000000..a130c36de
--- /dev/null
+++ b/packages/bruno-electron/src/store/ui-state-snapshot.js
@@ -0,0 +1,60 @@
+const Store = require('electron-store');
+
+class UiStateSnapshotStore {
+ constructor() {
+ this.store = new Store({
+ name: 'ui-state-snapshot',
+ clearInvalidConfig: true
+ });
+ }
+
+ getCollections() {
+ return this.store.get('collections') || [];
+ }
+
+ saveCollections(collections) {
+ this.store.set('collections', collections);
+ }
+
+ getCollectionByPathname({ pathname }) {
+ let collections = this.getCollections();
+
+ let collection = collections.find(c => c?.pathname === pathname);
+ if (!collection) {
+ collection = { pathname };
+ collections.push(collection);
+ this.saveCollections(collections);
+ }
+
+ return collection;
+ }
+
+ setCollectionByPathname({ collection }) {
+ let collections = this.getCollections();
+
+ collections = collections.filter(c => c?.pathname !== collection.pathname);
+ collections.push({ ...collection });
+ this.saveCollections(collections);
+
+ return collection;
+ }
+
+ updateCollectionEnvironment({ collectionPath, environmentName }) {
+ const collection = this.getCollectionByPathname({ pathname: collectionPath });
+ collection.selectedEnvironment = environmentName;
+ this.setCollectionByPathname({ collection });
+ }
+
+ update({ type, data }) {
+ switch(type) {
+ case 'COLLECTION_ENVIRONMENT':
+ const { collectionPath, environmentName } = data;
+ this.updateCollectionEnvironment({ collectionPath, environmentName });
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+module.exports = UiStateSnapshotStore;
diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js
index 50b17bb38..1962ac260 100644
--- a/packages/bruno-electron/src/utils/common.js
+++ b/packages/bruno-electron/src/utils/common.js
@@ -85,6 +85,24 @@ const flattenDataForDotNotation = (data) => {
return result;
};
+/**
+ * @param {Array.} params The request body Array
+ * @returns {object} Returns an obj with repeating key as a array of values
+ * {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
+ */
+const buildFormUrlEncodedPayload = (params) => {
+ return params.reduce((acc, p) => {
+ if (!acc[p.name]) {
+ acc[p.name] = p.value;
+ } else if (Array.isArray(acc[p.name])) {
+ acc[p.name].push(p.value);
+ } else {
+ acc[p.name] = [acc[p.name], p.value];
+ }
+ return acc;
+ }, {});
+};
+
module.exports = {
uuid,
stringifyJson,
@@ -93,5 +111,6 @@ module.exports = {
safeParseJSON,
simpleHash,
generateUidBasedOnHash,
- flattenDataForDotNotation
+ flattenDataForDotNotation,
+ buildFormUrlEncodedPayload
};
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 752cb339c..0263939ae 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const fsPromises = require('fs/promises');
const { dialog } = require('electron');
const isValidPathname = require('is-valid-path');
+const os = require('os');
const exists = async (p) => {
try {
@@ -155,12 +156,34 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
-// const isW
-
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
+const safeToRename = (oldPath, newPath) => {
+ try {
+ // If the new path doesn't exist, it's safe to rename
+ if (!fs.existsSync(newPath)) {
+ return true;
+ }
+
+ const oldStat = fs.statSync(oldPath);
+ const newStat = fs.statSync(newPath);
+
+ if (os.platform() === 'win32') {
+ // Windows-specific comparison:
+ // Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes)
+
+ return oldStat.birthtimeMs === newStat.birthtimeMs && oldStat.size === newStat.size;
+ }
+ // Unix/Linux/MacOS: Check inode to see if they are the same file
+ return oldStat.ino === newStat.ino;
+ } catch (error) {
+ console.error(`Error checking file rename safety for ${oldPath} and ${newPath}:`, error);
+ return false;
+ }
+};
+
module.exports = {
isValidPathname,
exists,
@@ -180,5 +203,6 @@ module.exports = {
chooseFileToSave,
searchForFiles,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ safeToRename
};
diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js
index 808a127d9..b61d42c50 100644
--- a/packages/bruno-electron/tests/network/prepare-request.spec.js
+++ b/packages/bruno-electron/tests/network/prepare-request.spec.js
@@ -1,6 +1,7 @@
const { describe, it, expect } = require('@jest/globals');
const prepareRequest = require('../../src/ipc/network/prepare-request');
+const { buildFormUrlEncodedPayload } = require('../../src/utils/common');
describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
@@ -17,5 +18,43 @@ describe('prepare-request: prepareRequest', () => {
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});
+
+ it('should handle single key-value pair', () => {
+ const requestObj = [{ name: 'item', value: 2 }];
+ const expected = { item: 2 };
+ const result = buildFormUrlEncodedPayload(requestObj);
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle multiple key-value pairs with unique keys', () => {
+ const requestObj = [
+ { name: 'item1', value: 2 },
+ { name: 'item2', value: 3 }
+ ];
+ const expected = { item1: 2, item2: 3 };
+ const result = buildFormUrlEncodedPayload(requestObj);
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle multiple key-value pairs with the same key', () => {
+ const requestObj = [
+ { name: 'item', value: 2 },
+ { name: 'item', value: 3 }
+ ];
+ const expected = { item: [2, 3] };
+ const result = buildFormUrlEncodedPayload(requestObj);
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle mixed key-value pairs with unique and duplicate keys', () => {
+ const requestObj = [
+ { name: 'item1', value: 2 },
+ { name: 'item2', value: 3 },
+ { name: 'item1', value: 4 }
+ ];
+ const expected = { item1: [2, 4], item2: 3 };
+ const result = buildFormUrlEncodedPayload(requestObj);
+ expect(result).toEqual(expected);
+ });
});
-});
+});
\ No newline at end of file
diff --git a/packages/bruno-graphql-docs/package.json b/packages/bruno-graphql-docs/package.json
index ba609393b..9cd60964b 100644
--- a/packages/bruno-graphql-docs/package.json
+++ b/packages/bruno-graphql-docs/package.json
@@ -19,10 +19,10 @@
"@types/react": "^18.0.25",
"graphql": "^16.6.0",
"markdown-it": "^13.0.1",
- "postcss": "^8.4.18",
+ "postcss": "8.4.47",
"react": "18.2.0",
"react-dom": "18.2.0",
- "rollup":"3.29.4",
+ "rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
@@ -34,6 +34,6 @@
"markdown-it": "^13.0.1"
},
"overrides": {
- "rollup":"3.29.4"
+ "rollup":"3.29.5"
}
}
diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json
index fdd8bfbd9..1378ea95c 100644
--- a/packages/bruno-js/package.json
+++ b/packages/bruno-js/package.json
@@ -8,7 +8,7 @@
"package.json"
],
"peerDependencies": {
- "@n8n/vm2": "^3.9.23"
+ "@usebruno/vm2": "^3.9.13"
},
"scripts": {
"test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
@@ -20,7 +20,7 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
- "axios": "^1.5.1",
+ "axios": "1.7.5",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
@@ -38,16 +38,10 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
- "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
- "rollup": "3.2.5",
- "rollup-plugin-node-builtins": "^2.1.2",
- "rollup-plugin-node-globals": "^1.4.0",
- "rollup-plugin-polyfill-node": "^0.13.0",
+ "rollup": "3.29.5",
"rollup-plugin-terser": "^7.0.2",
"stream": "^0.0.2",
- "terser": "^5.31.1",
- "uglify-js": "^3.18.0",
"util": "^0.12.5"
}
}
diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js
index 8dfc69486..fc6f81378 100644
--- a/packages/bruno-js/src/bru.js
+++ b/packages/bruno-js/src/bru.js
@@ -4,11 +4,14 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;
class Bru {
- constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables) {
+ constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables) {
this.envVariables = envVariables || {};
this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
+ this.collectionVariables = collectionVariables || {};
+ this.folderVariables = folderVariables || {};
this.requestVariables = requestVariables || {};
+ this.globalEnvironmentVariables = globalEnvironmentVariables || {};
this.collectionPath = collectionPath;
}
@@ -18,7 +21,10 @@ class Bru {
}
const combinedVars = {
+ ...this.globalEnvironmentVariables,
+ ...this.collectionVariables,
...this.envVariables,
+ ...this.folderVariables,
...this.requestVariables,
...this.runtimeVariables,
process: {
@@ -59,6 +65,18 @@ class Bru {
this.envVariables[key] = value;
}
+ getGlobalEnvVar(key) {
+ return this._interpolate(this.globalEnvironmentVariables[key]);
+ }
+
+ setGlobalEnvVar(key, value) {
+ if (!key) {
+ throw new Error('Creating a env variable without specifying a name is not allowed.');
+ }
+
+ this.globalEnvironmentVariables[key] = value;
+ }
+
hasVar(key) {
return Object.hasOwn(this.runtimeVariables, key);
}
@@ -71,7 +89,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
- ' Names must only contain alpha-numeric characters, "-", "_", "."'
+ ' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -82,7 +100,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
- ' Names must only contain alpha-numeric characters, "-", "_", "."'
+ ' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -93,6 +111,22 @@ class Bru {
delete this.runtimeVariables[key];
}
+ deleteAllVars() {
+ for (let key in this.runtimeVariables) {
+ if (this.runtimeVariables.hasOwnProperty(key)) {
+ delete this.runtimeVariables[key];
+ }
+ }
+ }
+
+ getCollectionVar(key) {
+ return this._interpolate(this.collectionVariables[key]);
+ }
+
+ getFolderVar(key) {
+ return this._interpolate(this.folderVariables[key]);
+ }
+
getRequestVar(key) {
return this._interpolate(this.requestVariables[key]);
}
diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js
index cf5f59aca..32e40c19e 100644
--- a/packages/bruno-js/src/bruno-request.js
+++ b/packages/bruno-js/src/bruno-request.js
@@ -43,7 +43,6 @@ class BrunoRequest {
getMethod() {
return this.req.method;
}
-
getAuthMode() {
if (this.req?.oauth2) {
return 'oauth2';
@@ -55,6 +54,8 @@ class BrunoRequest {
return 'awsv4';
} else if (this.req?.digestConfig) {
return 'digest';
+ } else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
+ return 'wsse';
} else {
return 'none';
}
@@ -172,6 +173,10 @@ class BrunoRequest {
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;
}
+
+ getExecutionMode() {
+ return this.req.__bruno__executionMode;
+ }
}
module.exports = BrunoRequest;
diff --git a/packages/bruno-js/src/bruno-response.js b/packages/bruno-js/src/bruno-response.js
index 99c7bcfec..8db45147f 100644
--- a/packages/bruno-js/src/bruno-response.js
+++ b/packages/bruno-js/src/bruno-response.js
@@ -32,6 +32,15 @@ class BrunoResponse {
getUrl() {
return this.res ? this.url : null;
}
+
+ setBody(data) {
+ if (!this.res) {
+ return;
+ }
+
+ this.body = data;
+ this.res.data = data;
+ }
}
module.exports = BrunoResponse;
diff --git a/packages/bruno-js/src/interpolate-string.js b/packages/bruno-js/src/interpolate-string.js
index 637b03832..f75daf57c 100644
--- a/packages/bruno-js/src/interpolate-string.js
+++ b/packages/bruno-js/src/interpolate-string.js
@@ -2,14 +2,17 @@ const { interpolate } = require('@usebruno/common');
const interpolateString = (
str,
- { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
+ { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {}, globalEnvironmentVariables = {} }
) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const combinedVars = {
+ ...globalEnvironmentVariables,
+ ...collectionVariables,
...envVariables,
+ ...folderVariables,
...requestVariables,
...runtimeVariables,
process: {
diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js
index 5b904dee4..b338730cc 100644
--- a/packages/bruno-js/src/runtime/assert-runtime.js
+++ b/packages/bruno-js/src/runtime/assert-runtime.js
@@ -192,6 +192,9 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
}
const interpolationContext = {
+ globalEnvironmentVariables: context.bru.globalEnvironmentVariables,
+ collectionVariables: context.bru.collectionVariables,
+ folderVariables: context.bru.folderVariables,
requestVariables: context.bru.requestVariables,
runtimeVariables: context.bru.runtimeVariables,
envVariables: context.bru.envVariables,
@@ -238,13 +241,25 @@ class AssertRuntime {
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
if (!enabledAssertions.length) {
return [];
}
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
+ const bru = new Bru(
+ envVariables,
+ runtimeVariables,
+ processEnvVars,
+ undefined,
+ collectionVariables,
+ folderVariables,
+ requestVariables,
+ globalEnvironmentVariables
+ );
const req = new BrunoRequest(request);
const res = createResponseParser(response);
@@ -255,7 +270,10 @@ class AssertRuntime {
};
const context = {
+ ...globalEnvironmentVariables,
+ ...collectionVariables,
...envVariables,
+ ...folderVariables,
...requestVariables,
...runtimeVariables,
...processEnvVars,
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index 9a9338c10..0f82114b2 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -1,4 +1,4 @@
-const { NodeVM } = require('vm2');
+const { NodeVM } = require('@usebruno/vm2');
const path = require('path');
const http = require('http');
const https = require('https');
@@ -47,8 +47,11 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
+ const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
const req = new BrunoRequest(request);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
@@ -100,6 +103,7 @@ class ScriptRuntime {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
nextRequestName: bru.nextRequest
};
}
@@ -147,6 +151,7 @@ class ScriptRuntime {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
nextRequestName: bru.nextRequest
};
}
@@ -162,8 +167,11 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
+ const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
@@ -212,6 +220,7 @@ class ScriptRuntime {
response,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
nextRequestName: bru.nextRequest
};
}
@@ -259,6 +268,7 @@ class ScriptRuntime {
response,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
nextRequestName: bru.nextRequest
};
}
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index 7fa9941ed..5391f7e10 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -1,4 +1,4 @@
-const { NodeVM } = require('vm2');
+const { NodeVM } = require('@usebruno/vm2');
const chai = require('chai');
const path = require('path');
const http = require('http');
@@ -48,8 +48,11 @@ class TestRuntime {
processEnvVars,
scriptingConfig
) {
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
+ const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
@@ -79,12 +82,12 @@ class TestRuntime {
request,
envVariables,
runtimeVariables,
+ globalEnvironmentVariables,
results: __brunoTestResults.getResults(),
nextRequestName: bru.nextRequest
};
}
-
const context = {
test,
bru,
@@ -160,6 +163,7 @@ class TestRuntime {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
results: cleanJson(__brunoTestResults.getResults()),
nextRequestName: bru.nextRequest
};
diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js
index 4f4bd3719..94e45f46e 100644
--- a/packages/bruno-js/src/runtime/vars-runtime.js
+++ b/packages/bruno-js/src/runtime/vars-runtime.js
@@ -1,22 +1,10 @@
const _ = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
-const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
+const { evaluateJsExpression, createResponseParser } = require('../utils');
const { executeQuickJsVm } = require('../sandbox/quickjs');
-const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
- if (runtime === 'quickjs') {
- return executeQuickJsVm({
- script: literal,
- context,
- scriptType: 'template-literal'
- });
- }
-
- return evaluateJsTemplateLiteral(literal, context);
-};
-
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
@@ -35,35 +23,6 @@ class VarsRuntime {
this.mode = props?.mode || 'developer';
}
- runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
- if (!request?.requestVariables) {
- request.requestVariables = {};
- }
- const enabledVars = _.filter(vars, (v) => v.enabled);
- if (!enabledVars.length) {
- return;
- }
-
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars);
- const req = new BrunoRequest(request);
-
- const bruContext = {
- bru,
- req
- };
-
- const context = {
- ...envVariables,
- ...runtimeVariables,
- ...bruContext
- };
-
- _.each(enabledVars, (v) => {
- const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
- request?.requestVariables && (request.requestVariables[v.name] = value);
- });
- }
-
runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledVars = _.filter(vars, (v) => v.enabled);
@@ -91,7 +50,9 @@ class VarsRuntime {
_.each(enabledVars, (v) => {
try {
const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);
- bru.setVar(v.name, value);
+ if (v.name) {
+ bru.setVar(v.name, value);
+ }
} catch (error) {
errors.set(v.name, error);
}
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
index ced639893..d55c37439 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
@@ -21,6 +21,12 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
getProcessEnv.dispose();
+ let hasEnvVar = vm.newFunction('hasEnvVar', function (key) {
+ return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'hasEnvVar', hasEnvVar);
+ hasEnvVar.dispose();
+
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
});
@@ -33,6 +39,24 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'setEnvVar', setEnvVar);
setEnvVar.dispose();
+ let getGlobalEnvVar = vm.newFunction('getGlobalEnvVar', function (key) {
+ return marshallToVm(bru.getGlobalEnvVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar);
+ getGlobalEnvVar.dispose();
+
+ let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
+ bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));
+ });
+ vm.setProp(bruObject, 'setGlobalEnvVar', setGlobalEnvVar);
+ setGlobalEnvVar.dispose();
+
+ let hasVar = vm.newFunction('hasVar', function (key) {
+ return marshallToVm(bru.hasVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'hasVar', hasVar);
+ hasVar.dispose();
+
let getVar = vm.newFunction('getVar', function (key) {
return marshallToVm(bru.getVar(vm.dump(key)), vm);
});
@@ -45,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'setVar', setVar);
setVar.dispose();
+ let deleteVar = vm.newFunction('deleteVar', function (key) {
+ bru.deleteVar(vm.dump(key));
+ });
+ vm.setProp(bruObject, 'deleteVar', deleteVar);
+ deleteVar.dispose();
+
+ let deleteAllVars = vm.newFunction('deleteAllVars', function () {
+ bru.deleteAllVars();
+ });
+ vm.setProp(bruObject, 'deleteAllVars', deleteAllVars);
+ deleteAllVars.dispose();
+
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
bru.setNextRequest(vm.dump(nextRequest));
});
@@ -69,6 +105,18 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
getRequestVar.dispose();
+ let getFolderVar = vm.newFunction('getFolderVar', function (key) {
+ return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getFolderVar', getFolderVar);
+ getFolderVar.dispose();
+
+ let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
+ return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
+ getCollectionVar.dispose();
+
const sleep = vm.newFunction('sleep', (timer) => {
const t = vm.getString(timer);
const promise = vm.newPromise();
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
index 9be27fae7..e3f364fe7 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
@@ -105,6 +105,18 @@ const addBrunoRequestShimToContext = (vm, req) => {
vm.setProp(reqObject, 'setTimeout', setTimeout);
setTimeout.dispose();
+ let disableParsingResponseJson = vm.newFunction('disableParsingResponseJson', function () {
+ req.disableParsingResponseJson();
+ });
+ vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
+ disableParsingResponseJson.dispose();
+
+ let getExecutionMode = vm.newFunction('getExecutionMode', function () {
+ return marshallToVm(req.getExecutionMode(), vm);
+ });
+ vm.setProp(reqObject, 'getExecutionMode', getExecutionMode);
+ getExecutionMode.dispose();
+
vm.setProp(vm.global, 'req', reqObject);
reqObject.dispose();
};
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
index 5dfaffa22..9b81a7374 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
@@ -1,7 +1,9 @@
const { marshallToVm } = require('../utils');
const addBrunoResponseShimToContext = (vm, res) => {
- const resObject = vm.newObject();
+ let resFn = vm.newFunction('res', function (exprStr) {
+ return marshallToVm(res(vm.dump(exprStr)), vm);
+ });
const status = marshallToVm(res?.status, vm);
const headers = marshallToVm(res?.headers, vm);
@@ -9,11 +11,11 @@ const addBrunoResponseShimToContext = (vm, res) => {
const responseTime = marshallToVm(res?.responseTime, vm);
const url = marshallToVm(res?.url, vm);
- vm.setProp(resObject, 'status', status);
- vm.setProp(resObject, 'headers', headers);
- vm.setProp(resObject, 'body', body);
- vm.setProp(resObject, 'responseTime', responseTime);
- vm.setProp(resObject, 'url', url);
+ vm.setProp(resFn, 'status', status);
+ vm.setProp(resFn, 'headers', headers);
+ vm.setProp(resFn, 'body', body);
+ vm.setProp(resFn, 'responseTime', responseTime);
+ vm.setProp(resFn, 'url', url);
status.dispose();
headers.dispose();
@@ -24,41 +26,47 @@ const addBrunoResponseShimToContext = (vm, res) => {
let getStatus = vm.newFunction('getStatus', function () {
return marshallToVm(res.getStatus(), vm);
});
- vm.setProp(resObject, 'getStatus', getStatus);
+ vm.setProp(resFn, 'getStatus', getStatus);
getStatus.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(res.getHeader(vm.dump(name)), vm);
});
- vm.setProp(resObject, 'getHeader', getHeader);
+ vm.setProp(resFn, 'getHeader', getHeader);
getHeader.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(res.getHeaders(), vm);
});
- vm.setProp(resObject, 'getHeaders', getHeaders);
+ vm.setProp(resFn, 'getHeaders', getHeaders);
getHeaders.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(res.getBody(), vm);
});
- vm.setProp(resObject, 'getBody', getBody);
+ vm.setProp(resFn, 'getBody', getBody);
getBody.dispose();
let getResponseTime = vm.newFunction('getResponseTime', function () {
return marshallToVm(res.getResponseTime(), vm);
});
- vm.setProp(resObject, 'getResponseTime', getResponseTime);
+ vm.setProp(resFn, 'getResponseTime', getResponseTime);
getResponseTime.dispose();
let getUrl = vm.newFunction('getUrl', function () {
return marshallToVm(res.getUrl(), vm);
});
- vm.setProp(resObject, 'getUrl', getUrl);
+ vm.setProp(resFn, 'getUrl', getUrl);
getUrl.dispose();
- vm.setProp(vm.global, 'res', resObject);
- resObject.dispose();
+ let setBody = vm.newFunction('setBody', function (data) {
+ res.setBody(vm.dump(data));
+ });
+ vm.setProp(resFn, 'setBody', setBody);
+ setBody.dispose();
+
+ vm.setProp(vm.global, 'res', resFn);
+ resFn.dispose();
};
module.exports = addBrunoResponseShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/utils/index.js b/packages/bruno-js/src/sandbox/quickjs/utils/index.js
index e376c3252..47be92f5f 100644
--- a/packages/bruno-js/src/sandbox/quickjs/utils/index.js
+++ b/packages/bruno-js/src/sandbox/quickjs/utils/index.js
@@ -25,6 +25,8 @@ const marshallToVm = (value, vm) => {
}
return obj;
}
+ } else if (typeof value === 'function') {
+ return vm.newString('[Function (anonymous)]');
}
};
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 08e4332c5..c84d36d07 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
+ auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
params = paramspath | paramsquery
@@ -88,6 +88,8 @@ const grammar = ohm.grammar(`Bru {
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
+ authwsse = "auth:wsse" dictionary
+ authapikey = "auth:apikey" dictionary
body = "body" st* "{" nl* textblock tagend
bodyjson = "body:json" st* "{" nl* textblock tagend
@@ -483,6 +485,45 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ authwsse(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+
+ const userKey = _.find(auth, { name: 'username' });
+ const secretKey = _.find(auth, { name: 'password' });
+ const username = userKey ? userKey.value : '';
+ const password = secretKey ? secretKey.value : '';
+
+ return {
+ auth: {
+ wsse: {
+ username,
+ password
+ }
+ }
+ };
+ },
+ authapikey(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+
+ const findValueByName = (name) => {
+ const item = _.find(auth, { name });
+ return item ? item.value : '';
+ };
+
+ const key = findValueByName('key');
+ const value = findValueByName('value');
+ const placement = findValueByName('placement');
+
+ return {
+ auth: {
+ apikey: {
+ key,
+ value,
+ placement
+ }
+ }
+ };
+ },
bodyformurlencoded(_1, dictionary) {
return {
body: {
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
index 3c02a6225..5180f0193 100644
--- a/packages/bruno-lang/v2/src/collectionBruToJson.js
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
const grammar = ohm.grammar(`Bru {
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
+ auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
nl = "\\r"? "\\n"
st = " " | "\\t"
@@ -43,6 +43,8 @@ const grammar = ohm.grammar(`Bru {
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
+ authwsse = "auth:wsse" dictionary
+ authapikey = "auth:apikey" dictionary
script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
@@ -293,6 +295,43 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ authwsse(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+ const userKey = _.find(auth, { name: 'username' });
+ const secretKey = _.find(auth, { name: 'password' });
+ const username = userKey ? userKey.value : '';
+ const password = secretKey ? secretKey.value : '';
+ return {
+ auth: {
+ wsse: {
+ username,
+ password
+ }
+ }
+ }
+ },
+ authapikey(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+
+ const findValueByName = (name) => {
+ const item = _.find(auth, { name });
+ return item ? item.value : '';
+ };
+
+ const key = findValueByName('key');
+ const value = findValueByName('value');
+ const placement = findValueByName('placement');
+
+ return {
+ auth: {
+ apikey: {
+ key,
+ value,
+ placement
+ }
+ }
+ };
+ },
varsreq(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast);
_.each(vars, (v) => {
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index dd3be5947..8d3a5fdee 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)}
${indentString(`password: ${auth?.basic?.password || ''}`)}
}
+`;
+ }
+
+ if (auth && auth.wsse) {
+ bru += `auth:wsse {
+${indentString(`username: ${auth?.wsse?.username || ''}`)}
+${indentString(`password: ${auth?.wsse?.password || ''}`)}
+}
+
`;
}
@@ -200,6 +209,16 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
}
}
+ if (auth && auth.apikey) {
+ bru += `auth:apikey {
+${indentString(`key: ${auth?.apikey?.key || ''}`)}
+${indentString(`value: ${auth?.apikey?.value || ''}`)}
+${indentString(`placement: ${auth?.apikey?.placement || ''}`)}
+}
+
+`;
+ }
+
if (body && body.json && body.json.length) {
bru += `body:json {
${indentString(body.json)}
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
index 11df88da4..8b162b7a6 100644
--- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -94,6 +94,15 @@ ${indentString(`username: ${auth.basic.username}`)}
${indentString(`password: ${auth.basic.password}`)}
}
+`;
+ }
+
+ if (auth && auth.wsse) {
+ bru += `auth:wsse {
+${indentString(`username: ${auth.wsse.username}`)}
+${indentString(`password: ${auth.wsse.password}`)}
+}
+
`;
}
@@ -111,6 +120,15 @@ ${indentString(`username: ${auth.digest.username}`)}
${indentString(`password: ${auth.digest.password}`)}
}
+`;
+ }
+
+ if (auth && auth.apikey) {
+ bru += `auth:apikey {
+${indentString(`key: ${auth?.apikey?.key || ''}`)}
+${indentString(`value: ${auth?.apikey?.value || ''}`)}
+${indentString(`placement: ${auth?.apikey?.placement || ''}`)}
+}
`;
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru
index 44a66c8dc..f11954ebf 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru
@@ -17,6 +17,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json
index 7bda2534d..102ee295c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.json
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.json
@@ -31,6 +31,10 @@
"digest": {
"username": "john",
"password": "secret"
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"vars": {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index c4ff61558..1a3efeab7 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -40,6 +40,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index d0bd996f6..24997a90c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -83,6 +83,10 @@
"scope": "read write",
"state": "807061d5f0be",
"pkce": false
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"body": {
diff --git a/packages/bruno-query/package.json b/packages/bruno-query/package.json
index b7ff020bf..437513a78 100644
--- a/packages/bruno-query/package.json
+++ b/packages/bruno-query/package.json
@@ -21,13 +21,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
- "rollup":"3.29.4",
+ "rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
- "rollup":"3.29.4"
+ "rollup":"3.29.5"
}
}
\ No newline at end of file
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index eeb4e83d6..11561c528 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({
.noUnknown(true)
.strict();
+const authWsseSchema = Yup.object({
+ username: Yup.string().nullable(),
+ password: Yup.string().nullable()
+})
+ .noUnknown(true)
+ .strict();
+
const authBearerSchema = Yup.object({
token: Yup.string().nullable()
})
@@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({
.noUnknown(true)
.strict();
+const authApiKeySchema = Yup.object({
+ key: Yup.string().nullable(),
+ value: Yup.string().nullable(),
+ placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
+})
+ .noUnknown(true)
+ .strict();
+
const oauth2Schema = Yup.object({
grantType: Yup.string()
.oneOf(['client_credentials', 'password', 'authorization_code'])
@@ -179,13 +194,15 @@ const oauth2Schema = Yup.object({
const authSchema = Yup.object({
mode: Yup.string()
- .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2'])
+ .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey'])
.required('mode is required'),
awsv4: authAwsV4Schema.nullable(),
basic: authBasicSchema.nullable(),
bearer: authBearerSchema.nullable(),
digest: authDigestSchema.nullable(),
- oauth2: oauth2Schema.nullable()
+ oauth2: oauth2Schema.nullable(),
+ wsse: authWsseSchema.nullable(),
+ apikey: authApiKeySchema.nullable()
})
.noUnknown(true)
.strict()
diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json
index b6d437bbb..ada36145a 100644
--- a/packages/bruno-tests/collection/bruno.json
+++ b/packages/bruno-tests/collection/bruno.json
@@ -15,7 +15,7 @@
"bypassProxy": ""
},
"scripts": {
- "moduleWhitelist": ["crypto", "buffer"],
+ "moduleWhitelist": ["crypto", "buffer", "form-data"],
"filesystemAccess": {
"allow": true
}
diff --git a/packages/bruno-tests/collection/bruno.png b/packages/bruno-tests/collection/bruno.png
new file mode 100644
index 000000000..c2a7f878f
Binary files /dev/null and b/packages/bruno-tests/collection/bruno.png differ
diff --git a/packages/bruno-tests/collection/collection.bru b/packages/bruno-tests/collection/collection.bru
index ab9776995..d4b353eb8 100644
--- a/packages/bruno-tests/collection/collection.bru
+++ b/packages/bruno-tests/collection/collection.bru
@@ -1,5 +1,6 @@
headers {
check: again
+ token: {{collection_pre_var_token}}
}
auth {
@@ -10,6 +11,11 @@ auth:bearer {
token: {{bearer_auth_token}}
}
+vars:pre-request {
+ collection_pre_var: collection_pre_var_value
+ collection_pre_var_token: {{request_pre_var_token}}
+}
+
docs {
# bruno-testbench 🐶
diff --git a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
new file mode 100644
index 000000000..7c0ce77eb
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
@@ -0,0 +1,26 @@
+meta {
+ name: echo form-url-encoded
+ type: http
+ seq: 9
+}
+
+post {
+ url: {{echo-host}}
+ body: formUrlEncoded
+ auth: none
+}
+
+body:form-urlencoded {
+ form-data-key: {{form-data-key}}
+ form-data-stringified-object: {{form-data-stringified-object}}
+}
+
+assert {
+ res.body: eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D
+}
+
+script:pre-request {
+ let obj = JSON.stringify({foo:123});
+ bru.setVar('form-data-key', 'form-data-value');
+ bru.setVar('form-data-stringified-object', obj);
+}
diff --git a/packages/bruno-tests/collection/echo/echo multipart scripting.bru b/packages/bruno-tests/collection/echo/echo multipart scripting.bru
new file mode 100644
index 000000000..13c1f2051
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo multipart scripting.bru
@@ -0,0 +1,22 @@
+meta {
+ name: echo multipart via scripting
+ type: http
+ seq: 10
+}
+
+post {
+ url: {{echo-host}}
+ body: multipartForm
+ auth: none
+}
+
+assert {
+ res.body: contains form-data-value
+}
+
+script:pre-request {
+ const FormData = require("form-data");
+ const form = new FormData();
+ form.append('form-data-key', 'form-data-value');
+ req.setBody(form);
+}
diff --git a/packages/bruno-tests/collection/echo/echo multipart.bru b/packages/bruno-tests/collection/echo/echo multipart.bru
new file mode 100644
index 000000000..1edb2ca8a
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo multipart.bru
@@ -0,0 +1,28 @@
+meta {
+ name: echo multipart
+ type: http
+ seq: 8
+}
+
+post {
+ url: {{echo-host}}
+ body: multipartForm
+ auth: none
+}
+
+body:multipart-form {
+ form-data-key: {{form-data-key}}
+ form-data-stringified-object: {{form-data-stringified-object}}
+ file: @file(bruno.png)
+}
+
+assert {
+ res.body: contains form-data-value
+ res.body: contains {"foo":123}
+}
+
+script:pre-request {
+ let obj = JSON.stringify({foo:123});
+ bru.setVar('form-data-key', 'form-data-value');
+ bru.setVar('form-data-stringified-object', obj);
+}
diff --git a/packages/bruno-tests/collection/echo/echo xml parsed.bru b/packages/bruno-tests/collection/echo/echo xml parsed.bru
index 586541664..acd24a292 100644
--- a/packages/bruno-tests/collection/echo/echo xml parsed.bru
+++ b/packages/bruno-tests/collection/echo/echo xml parsed.bru
@@ -25,9 +25,7 @@ tests {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": {
- "world": [
- "bruno"
- ]
+ "world": ["bruno"]
}
});
});
diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru
index 4bea1e77a..ce8fa60cc 100644
--- a/packages/bruno-tests/collection/environments/Prod.bru
+++ b/packages/bruno-tests/collection/environments/Prod.bru
@@ -7,4 +7,5 @@ vars {
bark: {{process.env.PROC_ENV_VAR}}
foo: bar
testSetEnvVar: bruno-29653
+ echo-host: https://echo.usebruno.com
}
diff --git a/packages/bruno-tests/collection/scripting/js/data types - request vars.bru b/packages/bruno-tests/collection/scripting/js/data types - request vars.bru
index d8a8af9f2..a0f7c91a8 100644
--- a/packages/bruno-tests/collection/scripting/js/data types - request vars.bru
+++ b/packages/bruno-tests/collection/scripting/js/data types - request vars.bru
@@ -25,16 +25,6 @@ body:json {
}
}
-vars:pre-request {
- boolean: false
- undefined: undefined
- null: null
- string: foo
- number_1: 1
- number_2: 0
- number_3: -1
-}
-
assert {
req.body.boolean: isBoolean false
req.body.number_1: isNumber 1
@@ -51,35 +41,4 @@ assert {
req.body.number_3: eq -1
req.body.number_2: isNumber
req.body.number_3: isNumber
- boolean: eq false
- undefined: eq undefined
- null: eq null
- string: eq foo
- number_1: eq 1
- number_2: eq 0
- number_3: eq -1
-}
-
-tests {
- test("boolean pre var", function() {
- expect(bru.getRequestVar('boolean')).to.eql(false);
- });
-
- test("number pre var", function() {
- expect(bru.getRequestVar('number_1')).to.eql(1);
- expect(bru.getRequestVar('number_2')).to.eql(0);
- expect(bru.getRequestVar('number_3')).to.eql(-1);
- });
-
- test("null pre var", function() {
- expect(bru.getRequestVar('null')).to.eql(null);
- });
-
- test("undefined pre var", function() {
- expect(bru.getRequestVar('undefined')).to.eql(undefined);
- });
-
- test("string pre var", function() {
- expect(bru.getRequestVar('string')).to.eql('foo');
- });
}
diff --git a/packages/bruno-tests/collection/scripting/js/vars asserts.bru b/packages/bruno-tests/collection/scripting/js/vars asserts.bru
deleted file mode 100644
index e4bf9e97b..000000000
--- a/packages/bruno-tests/collection/scripting/js/vars asserts.bru
+++ /dev/null
@@ -1,39 +0,0 @@
-meta {
- name: vars asserts
- type: http
- seq: 5
-}
-
-post {
- url: {{host}}/api/echo/json
- body: json
- auth: none
-}
-
-body:json {
- {
- "boolean": false,
- "number": 1,
- "string": "bruno",
- "array": [1, 2, 3, 4, 5],
- "object": {
- "hello": "bruno"
- },
- "null": null
- }
-}
-
-vars:pre-request {
- vars_asserts__request_var: vars_asserts__request_var__value
-}
-
-assert {
- vars_asserts__request_var: eq vars_asserts__request_var__value
- vars_asserts__runtime_var: eq vars_asserts__runtime_var__value
- vars_asserts__env_var: eq vars_asserts__env_var__value
-}
-
-script:pre-request {
- bru.setVar('vars_asserts__runtime_var', 'vars_asserts__runtime_var__value');
- bru.setEnvVar('vars_asserts__env_var', 'vars_asserts__env_var__value');
-}
diff --git a/packages/bruno-tests/collection/string interpolation/folder.bru b/packages/bruno-tests/collection/string interpolation/folder.bru
new file mode 100644
index 000000000..8aef24cb2
--- /dev/null
+++ b/packages/bruno-tests/collection/string interpolation/folder.bru
@@ -0,0 +1,8 @@
+meta {
+ name: string interpolation
+}
+
+vars:pre-request {
+ folder_pre_var: folder_pre_var_value
+ folder_pre_var_2: {{env.var1}}
+}
diff --git a/packages/bruno-tests/package.json b/packages/bruno-tests/package.json
index 84ede3d62..ad819bf1d 100644
--- a/packages/bruno-tests/package.json
+++ b/packages/bruno-tests/package.json
@@ -18,13 +18,13 @@
},
"homepage": "https://github.com/usebruno/bruno-testbench#readme",
"dependencies": {
- "axios": "^1.5.1",
- "body-parser": "^1.20.0",
+ "axios": "1.7.5",
+ "body-parser": "1.20.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
- "express": "^4.18.1",
+ "express": "4.21.1",
"express-basic-auth": "^1.2.1",
- "express-xml-bodyparser": "^0.3.0",
+ "fast-xml-parser": "^4.5.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js
index 6d6ebfb55..e26a65529 100644
--- a/packages/bruno-tests/src/auth/index.js
+++ b/packages/bruno-tests/src/auth/index.js
@@ -3,6 +3,7 @@ const router = express.Router();
const authBearer = require('./bearer');
const authBasic = require('./basic');
+const authWsse = require('./wsse');
const authCookie = require('./cookie');
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
@@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
router.use('/bearer', authBearer);
router.use('/basic', authBasic);
+router.use('/wsse', authWsse);
router.use('/cookie', authCookie);
module.exports = router;
diff --git a/packages/bruno-tests/src/auth/wsse.js b/packages/bruno-tests/src/auth/wsse.js
new file mode 100644
index 000000000..1af574a3d
--- /dev/null
+++ b/packages/bruno-tests/src/auth/wsse.js
@@ -0,0 +1,70 @@
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+
+function sha256(data) {
+ return crypto.createHash('sha256').update(data).digest('base64');
+}
+
+function validateWSSE(req, res, next) {
+ const wsseHeader = req.headers['x-wsse'];
+ if (!wsseHeader) {
+ return unauthorized(res, 'WSSE header is missing');
+ }
+
+ const regex = /UsernameToken Username="(.+?)", PasswordDigest="(.+?)", (?:Nonce|nonce)="(.+?)", Created="(.+?)"/;
+ const matches = wsseHeader.match(regex);
+
+ if (!matches) {
+ return unauthorized(res, 'Invalid WSSE header format');
+ }
+
+ const [_, username, passwordDigest, nonce, created] = matches;
+ const expectedPassword = 'bruno'; // Ideally store in a config or env variable
+ const expectedDigest = sha256(nonce + created + expectedPassword);
+
+ if (passwordDigest !== expectedDigest) {
+ return unauthorized(res, 'Invalid credentials');
+ }
+
+ next();
+}
+
+// Helper to respond with an unauthorized SOAP fault
+function unauthorized(res, message) {
+ const faultResponse = `
+
+
+
+
+ soapenv:Client
+ ${message}
+
+
+
+ `;
+ res.status(401).set('Content-Type', 'text/xml');
+ res.send(faultResponse);
+}
+
+const responses = {
+ success: `
+
+
+
+
+ Success
+
+
+
+ `
+};
+
+router.post('/protected', validateWSSE, (req, res) => {
+ res.set('Content-Type', 'text/xml');
+ res.send(responses.success);
+});
+
+module.exports = router;
diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js
index 9ba6e3170..a6c72dc2b 100644
--- a/packages/bruno-tests/src/index.js
+++ b/packages/bruno-tests/src/index.js
@@ -1,22 +1,21 @@
const express = require('express');
const bodyParser = require('body-parser');
-const xmlparser = require('express-xml-bodyparser');
const cors = require('cors');
const multer = require('multer');
+const authRouter = require('./auth');
+const echoRouter = require('./echo');
+const xmlParser = require('./utils/xmlParser');
const app = new express();
const port = process.env.PORT || 8080;
const upload = multer();
app.use(cors());
-app.use(xmlparser());
+app.use(xmlParser());
app.use(bodyParser.text());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
-const authRouter = require('./auth');
-const echoRouter = require('./echo');
-
app.use('/api/auth', authRouter);
app.use('/api/echo', echoRouter);
diff --git a/packages/bruno-tests/src/utils/xmlParser.js b/packages/bruno-tests/src/utils/xmlParser.js
new file mode 100644
index 000000000..ed765c077
--- /dev/null
+++ b/packages/bruno-tests/src/utils/xmlParser.js
@@ -0,0 +1,30 @@
+const { XMLParser } = require('fast-xml-parser');
+
+const xmlParser = () => {
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ allowBooleanAttributes: true,
+ });
+
+ return (req, res, next) => {
+ if (req.is('application/xml') || req.is('text/xml')) {
+ let data = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => {
+ data += chunk;
+ });
+ req.on('end', () => {
+ try {
+ req.body = parser.parse(data);
+ next();
+ } catch (err) {
+ res.status(400).send('Invalid XML');
+ }
+ });
+ } else {
+ next();
+ }
+ };
+};
+
+module.exports = xmlParser;
\ No newline at end of file
diff --git a/publishing.md b/publishing.md
index 632fe18cc..458077b20 100644
--- a/publishing.md
+++ b/publishing.md
@@ -9,6 +9,7 @@
| [简体中文](docs/publishing/publishing_cn.md)
| [正體中文](docs/publishing/publishing_zhtw.md)
| [日本語](docs/publishing/publishing_ja.md)
+| [Nederlands](docs/publishing/publishing_nl.md)
### Publishing Bruno to a new package manager
diff --git a/readme.md b/readme.md
index 045be6058..8b507ab7b 100644
--- a/readme.md
+++ b/readme.md
@@ -28,6 +28,7 @@
| [العربية](docs/readme/readme_ar.md)
| [日本語](docs/readme/readme_ja.md)
| [ქართული](docs/readme/readme_ka.md)
+| [Nederlands](docs/readme/readme_nl.md)
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.