diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index ea63602df..b822babcb 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -73,7 +73,9 @@ if (!SERVER_RENDERED) { 'bru.setNextRequest(requestName)', 'req.disableParsingResponseJson()', 'bru.getRequestVar(key)', - 'bru.sleep(ms)' + 'bru.sleep(ms)', + 'bru.getGlobalEnvVar(key)', + 'bru.setGlobalEnvVar(key, value)' ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); diff --git a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js index 7af8b9081..f784cf527 100644 --- a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js +++ b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js @@ -36,6 +36,13 @@ const Wrapper = styled.div` padding: 0.35rem 0.6rem; cursor: pointer; + &.active { + color: ${(props) => props.theme.colors.text.yellow} !important; + .icon { + color: ${(props) => props.theme.colors.text.yellow} !important; + } + } + .icon { color: ${(props) => props.theme.dropdown.iconColor}; } diff --git a/packages/bruno-app/src/components/Dropdown/index.js b/packages/bruno-app/src/components/Dropdown/index.js index e4f48724c..3deb0e849 100644 --- a/packages/bruno-app/src/components/Dropdown/index.js +++ b/packages/bruno-app/src/components/Dropdown/index.js @@ -2,9 +2,9 @@ import React from 'react'; import Tippy from '@tippyjs/react'; import StyledWrapper from './StyledWrapper'; -const Dropdown = ({ icon, children, onCreate, placement }) => { +const Dropdown = ({ icon, children, onCreate, placement, transparent }) => { return ( - + {
} placement="bottom-end"> +
Collection Environments
{environments && environments.length ? environments.map((e) => (
{ onSelect(e); 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 e45909c94..c245dbfc2 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 @@ -59,7 +59,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original const ErrorMessage = ({ name }) => { const meta = formik.getFieldMeta(name); - if (!meta.error) { + if (!meta.error || !meta.touched) { return null; } 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..52ecad6c4 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js @@ -0,0 +1,94 @@ +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'; + +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 +
+
+
+ +
+ Configure +
+
+
+ {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 ( + + +
e.preventDefault()}> +
+ + + {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+
+
+ ); +}; + +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 ( + + +
e.preventDefault()}> +
+ +
+ +
+ {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+
+
+ ); +}; + +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..7eec1394c --- /dev/null +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js @@ -0,0 +1,61 @@ +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; + } + + 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..c33233bc8 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -0,0 +1,195 @@ +import React, { useRef, useEffect } from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import { IconTrash } 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'; + +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); + 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 ( + +
+ + + + + + + + + + + + {formik.values.map((variable, index) => ( + + + + + + + + ))} + +
EnabledNameValueSecret
+ + + + + +
+ formik.setFieldValue(`${index}.value`, newValue, true)} + /> +
+
+ + + +
+
+ +
+
+ +
+ + +
+
+ ); +}; +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..6516ba833 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js @@ -0,0 +1,138 @@ +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'; + +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 (selectedEnvironment) { + setOriginalEnvironmentVariables(selectedEnvironment.variables); + return; + } + + const environment = environments?.find(env => env?.uid === activeEnvironmentUid); + if (environment) { + setSelectedEnvironment(environment); + } else { + setSelectedEnvironment(environments && environments.length ? environments[0] : null); + } + }, [environments, selectedEnvironment]); + + 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 ( + + + + + + ); +}; + +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 ( + + +
e.preventDefault()}> +
+ + + {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+
+
+ ); +}; + +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..c93285f56 --- /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 ( + + ); +}; + +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/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 51d3194be..346a64aff 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 { cloneDeep } from 'lodash'; const MIN_LEFT_PANE_WIDTH = 300; const MIN_RIGHT_PANE_WIDTH = 350; @@ -34,6 +36,7 @@ const RequestTabPanel = () => { const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const collections = useSelector((state) => state.collections.collections); const screenWidth = useSelector((state) => state.app.screenWidth); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); let asideWidth = useSelector((state) => state.app.leftSidebarWidth); const focusedTab = find(tabs, (t) => t.uid === activeTabUid); @@ -117,7 +120,14 @@ const RequestTabPanel = () => { return
An error occurred!
; } - let collection = find(collections, (c) => c.uid === focusedTab.collectionUid); + let _collection = find(collections, (c) => c.uid === focusedTab.collectionUid); + let collection = cloneDeep(_collection); + + // add selected global env variables to the collection object + const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); + collection.globalEnvironmentVariables = globalEnvironmentVariables; + + 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..e19da78da 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,7 @@ const CollectionToolBar = ({ collection }) => { +
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 9d5648907..28f68a5a7 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,24 @@ 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'; 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 ); + // 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'); diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index c8e022b67..80ea83283 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -23,6 +23,7 @@ import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUi 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,6 +154,10 @@ 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)); }) @@ -159,6 +168,7 @@ const useIpcEvents = () => { removeCollectionAlreadyOpenedListener(); removeDisplayErrorListener(); removeScriptEnvUpdateListener(); + removeGlobalEnvironmentVariablesUpdateListener(); removeCollectionRenamedListener(); removeRunFolderEventListener(); removeRunRequestEventListener(); @@ -169,6 +179,7 @@ const useIpcEvents = () => { removePreferencesUpdatesListener(); removeCookieUpdateListener(); removeSystemProxyEnvUpdatesListener(); + removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); }; }, [isElectron]); 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 79635241e..066889d68 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -44,6 +44,7 @@ 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) => { @@ -184,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) => { @@ -191,7 +193,11 @@ 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); @@ -213,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) => { @@ -221,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) @@ -286,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) => { @@ -293,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) { 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..10184c1e7 --- /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: 'app', + 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', { 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/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index ddb98f531..cd925054d 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -786,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) { @@ -802,6 +815,7 @@ export const getEnvironmentVariables = (collection) => { return variables; }; + const getPathParams = (item) => { let pathParams = {}; if (item && item.request && item.request.params) { @@ -828,14 +842,17 @@ export const getTotalRequestCountInCollection = (collection) => { }; export const getAllVariables = (collection, item) => { + 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 { + ...globalEnvironmentVariables, ...collectionVariables, ...envVariables, ...folderVariables, 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-electron/src/index.js b/packages/bruno-electron/src/index.js index 6efc531c0..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(); @@ -143,6 +144,7 @@ app.on('ready', async () => { // 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/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/index.js b/packages/bruno-electron/src/ipc/network/index.js index 328232e31..97e3b8557 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -409,6 +409,10 @@ const registerNetworkIpc = (mainWindow) => { requestUid, collectionUid }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: scriptResult.globalEnvironmentVariables + }); } // interpolate variables inside request @@ -469,6 +473,10 @@ const registerNetworkIpc = (mainWindow) => { requestUid, collectionUid }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: result.globalEnvironmentVariables + }); } if (result?.error) { @@ -504,6 +512,10 @@ const registerNetworkIpc = (mainWindow) => { requestUid, collectionUid }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: scriptResult.globalEnvironmentVariables + }); } return scriptResult; }; @@ -691,6 +703,10 @@ const registerNetworkIpc = (mainWindow) => { requestUid, collectionUid }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: testResults.globalEnvironmentVariables + }); } return { @@ -1160,6 +1176,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', { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 99b0191cd..59f494416 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -14,6 +14,7 @@ const getContentType = (headers = {}) => { }; const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => { + const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; @@ -39,6 +40,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc // runtimeVariables take precedence over envVars const combinedVars = { + ...globalEnvironmentVariables, ...collectionVariables, ...envVariables, ...folderVariables, diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 93cbed419..c8b36bb89 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -370,6 +370,7 @@ const prepareRequest = (item, collection) => { mergeFolderLevelHeaders(request, requestTreePath); mergeFolderLevelScripts(request, requestTreePath, scriptFlow); mergeVars(collection, request, requestTreePath); + request.globalEnvironmentVariables = collection?.globalEnvironmentVariables; } // Request level headers @@ -461,6 +462,7 @@ const prepareRequest = (item, collection) => { axiosRequest.collectionVariables = request.collectionVariables; axiosRequest.folderVariables = request.folderVariables; axiosRequest.requestVariables = request.requestVariables; + axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables; axiosRequest.assertions = request.assertions; return axiosRequest; 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-js/src/bru.js b/packages/bruno-js/src/bru.js index 30f016e9f..fc6f81378 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -4,13 +4,14 @@ const { interpolate } = require('@usebruno/common'); const variableNameRegex = /^[\w-.]*$/; class Bru { - constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, 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; } @@ -20,6 +21,7 @@ class Bru { } const combinedVars = { + ...this.globalEnvironmentVariables, ...this.collectionVariables, ...this.envVariables, ...this.folderVariables, @@ -63,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); } diff --git a/packages/bruno-js/src/interpolate-string.js b/packages/bruno-js/src/interpolate-string.js index 2692641c2..f75daf57c 100644 --- a/packages/bruno-js/src/interpolate-string.js +++ b/packages/bruno-js/src/interpolate-string.js @@ -2,13 +2,14 @@ const { interpolate } = require('@usebruno/common'); const interpolateString = ( str, - { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} } + { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {}, globalEnvironmentVariables = {} } ) => { if (!str || !str.length || typeof str !== 'string') { return str; } const combinedVars = { + ...globalEnvironmentVariables, ...collectionVariables, ...envVariables, ...folderVariables, diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index aafacfe8a..b338730cc 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -192,6 +192,7 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => { } const interpolationContext = { + globalEnvironmentVariables: context.bru.globalEnvironmentVariables, collectionVariables: context.bru.collectionVariables, folderVariables: context.bru.folderVariables, requestVariables: context.bru.requestVariables, @@ -240,6 +241,7 @@ 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 || {}; @@ -255,7 +257,8 @@ class AssertRuntime { undefined, collectionVariables, folderVariables, - requestVariables + requestVariables, + globalEnvironmentVariables ); const req = new BrunoRequest(request); const res = createResponseParser(response); @@ -267,6 +270,7 @@ class AssertRuntime { }; const context = { + ...globalEnvironmentVariables, ...collectionVariables, ...envVariables, ...folderVariables, diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 9dc47a29d..4a9255783 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -47,10 +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, collectionVariables, folderVariables, 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', []); @@ -102,6 +103,7 @@ class ScriptRuntime { request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), + globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), nextRequestName: bru.nextRequest }; } @@ -149,6 +151,7 @@ class ScriptRuntime { request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), + globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), nextRequestName: bru.nextRequest }; } @@ -164,10 +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, collectionVariables, folderVariables, 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); @@ -216,6 +220,7 @@ class ScriptRuntime { response, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), + globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), nextRequestName: bru.nextRequest }; } @@ -263,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 53fab05eb..b2eb806ce 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -48,10 +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, collectionVariables, folderVariables, 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); @@ -81,12 +82,12 @@ class TestRuntime { request, envVariables, runtimeVariables, + globalEnvironmentVariables, results: __brunoTestResults.getResults(), nextRequestName: bru.nextRequest }; } - const context = { test, bru, @@ -162,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/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 0e489265c..d55c37439 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -39,6 +39,18 @@ 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); });