mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ad4f6073 | ||
|
|
a2d9249515 | ||
|
|
a880e030eb | ||
|
|
3ef7df57e2 | ||
|
|
40fad99803 | ||
|
|
43cb2b82f3 | ||
|
|
2fc79e0e7f | ||
|
|
dce96e0f13 | ||
|
|
c0dc329861 | ||
|
|
d4814569ed | ||
|
|
ec5c593de4 | ||
|
|
0fa0b3ef85 | ||
|
|
4211575f01 | ||
|
|
90834b8c7d | ||
|
|
7a77afc64a | ||
|
|
8ab8af6b3f | ||
|
|
4e8e2e87dc | ||
|
|
ce8ebb0c1a | ||
|
|
5c5e3d18fc | ||
|
|
7a5b309664 | ||
|
|
93f8d916c4 | ||
|
|
5afafb5944 | ||
|
|
6bc8acd1e1 | ||
|
|
96e58f2f40 | ||
|
|
4797119657 | ||
|
|
95e56cd9c9 | ||
|
|
02a82c5371 | ||
|
|
e2baed6724 | ||
|
|
d448599a53 | ||
|
|
f35b715c6f | ||
|
|
f088cdb504 | ||
|
|
d8bf27f288 | ||
|
|
a8fce54e97 | ||
|
|
727fa26e44 | ||
|
|
72de78025e |
@@ -10,6 +10,12 @@ const StyledWrapper = styled.div`
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
/* Removes the glow outline around the folded json */
|
||||
.CodeMirror-foldmarker {
|
||||
text-shadow: none;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
|
||||
@@ -69,10 +69,13 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.deleteAllVars()',
|
||||
'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();
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<StyledWrapper className="dropdown">
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
content={children}
|
||||
placement={placement || 'bottom-end'}
|
||||
|
||||
@@ -53,10 +53,11 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center cursor-pointer environment-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="label-item font-medium">Collection Environments</div>
|
||||
{environments && environments.length
|
||||
? environments.map((e) => (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
className={`dropdown-item ${e?.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
key={e.uid}
|
||||
onClick={() => {
|
||||
onSelect(e);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { IconTrash, IconAlertCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
@@ -11,6 +11,7 @@ import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -59,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 (
|
||||
<label htmlFor={name} className="text-red-500">
|
||||
{meta.error}
|
||||
</label>
|
||||
<span>
|
||||
<IconAlertCircle id={id} className="text-red-600 cursor-pointer " size={20} />
|
||||
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -124,19 +126,21 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<div ref={ref} className={`current-environment flex flex-row gap-1 rounded-xl text-xs cursor-pointer items-center justify-center select-none ${activeGlobalEnvironmentUid? 'environment-active': ''}`}>
|
||||
<IconWorld className="globe" size={16} strokeWidth={1.5} />
|
||||
{
|
||||
activeEnvironment ? <div>{activeEnvironment?.name}</div> : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center cursor-pointer environment-selector mr-3">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end" transparent={true}>
|
||||
<div className="label-item font-medium">Global Environments</div>
|
||||
{globalEnvironments && globalEnvironments.length
|
||||
? globalEnvironments.map((e) => (
|
||||
<div
|
||||
className={`dropdown-item ${e?.uid === activeGlobalEnvironmentUid ? 'active' : ''}`}
|
||||
key={e.uid}
|
||||
onClick={() => {
|
||||
onSelect(e);
|
||||
dropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
<IconDatabase size={18} strokeWidth={1.5} /> <span className="ml-2 break-all">{e.name}</span>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2">No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{openSettingsModal && <EnvironmentSettings globalEnvironments={globalEnvironments} activeGlobalEnvironmentUid={activeGlobalEnvironmentUid} onClose={handleModalClose} />}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentSelector;
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Copy Global Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
New Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyEnvironment;
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Create Global Environment'}
|
||||
confirmText="Create"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
</label>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironment;
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Delete Global Environment'}
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
Are you sure you want to delete <span className="font-semibold">{environment.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteEnvironment;
|
||||
@@ -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(
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">You have unsaved changes in this environment.</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCancel}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</Modal>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSwitchEnv;
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<label htmlFor={name} className="text-red-500">
|
||||
{meta.error}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<StyledWrapper className="w-full mt-6 mb-6">
|
||||
<div className="h-[50vh] overflow-y-auto w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button
|
||||
ref={addButtonRef}
|
||||
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={addVariable}
|
||||
>
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default EnvironmentVariables;
|
||||
@@ -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 (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
{openEditModal && (
|
||||
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} />
|
||||
)}
|
||||
{openDeleteModal && (
|
||||
<DeleteEnvironment
|
||||
onClose={() => setOpenDeleteModal(false)}
|
||||
environment={environment}
|
||||
/>
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-semibold break-all">{environment.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-x-4 pl-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables environment={environment} setIsModified={setIsModified} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentDetails;
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
|
||||
<div className="flex">
|
||||
<div>
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="flex items-center justify-between tab-container px-1">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto btn-import-environment">
|
||||
<div className="flex items-center" onClick={() => handleImportClick()}>
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnvironmentDetails
|
||||
environment={selectedEnvironment}
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentList;
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Global Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImportPostmanEnvironment}
|
||||
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
||||
>
|
||||
<IconDatabaseImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||
</button>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironment;
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Rename Environment'}
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameEnvironment;
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTab = ({ setTab }) => {
|
||||
return (
|
||||
<div className="text-center items-center flex flex-col">
|
||||
<IconFileAlert size={64} strokeWidth={1} />
|
||||
<span className="font-semibold mt-2">No Global Environments found</span>
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<SharedButton onClick={() => setTab('create')}>
|
||||
<span>Create Global Environment</span>
|
||||
</SharedButton>
|
||||
|
||||
<span className="mx-4">Or</span>
|
||||
|
||||
<SharedButton onClick={() => setTab('import')}>
|
||||
<span>Import Environment</span>
|
||||
</SharedButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Global Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironment onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<DefaultTab setTab={setTab} />
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
environments={globalEnvironments}
|
||||
activeEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentSettings;
|
||||
@@ -0,0 +1,46 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 2px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 600;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.key-button {
|
||||
display: inline-block;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
font-family: monospace;
|
||||
margin-right: 8px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,45 @@
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import React from 'react';
|
||||
import { getKeyBindingsForOS } from 'providers/Hotkeys/keyMappings';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
|
||||
const Keybindings = ({ close }) => {
|
||||
const keyMapping = getKeyBindingsForOS(isMacOS() ? 'mac' : 'windows');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Command</th>
|
||||
<th>Keybinding</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keyMapping ? (
|
||||
Object.entries(keyMapping).map(([action, { name, keys }], index) => (
|
||||
<tr key={index}>
|
||||
<td>{name}</td>
|
||||
<td>
|
||||
{keys.split('+').map((key, i) => (
|
||||
<div className="key-button" key={i}>
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2">No key bindings available</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Keybindings;
|
||||
@@ -1,11 +1,14 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Support from './Support';
|
||||
import General from './General';
|
||||
import Proxy from './ProxySettings';
|
||||
import Display from './Display';
|
||||
import Keybindings from './Keybindings';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Display from './Display/index';
|
||||
|
||||
const Preferences = ({ onClose }) => {
|
||||
const [tab, setTab] = useState('general');
|
||||
@@ -30,6 +33,10 @@ const Preferences = ({ onClose }) => {
|
||||
return <Display close={onClose} />;
|
||||
}
|
||||
|
||||
case 'keybindings': {
|
||||
return <Keybindings close={onClose} />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
@@ -50,6 +57,9 @@ const Preferences = ({ onClose }) => {
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
<div className={getTabClassname('keybindings')} role="tab" onClick={() => setTab('keybindings')}>
|
||||
Keybindings
|
||||
</div>
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
|
||||
/**
|
||||
@@ -13,17 +13,17 @@ import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
|
||||
const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||
const tbodyRef = useRef();
|
||||
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
|
||||
const [hoveredRow, setHoveredRow] = useState(null);
|
||||
const [dragStart, setDragStart] = useState(null);
|
||||
|
||||
const rowsOrder = useMemo(() => React.Children.toArray(children), [children]);
|
||||
|
||||
/**
|
||||
* useEffect hook to update the rows order and handle row hover states
|
||||
* useEffect hook to handle row hover states
|
||||
*/
|
||||
useEffect(() => {
|
||||
setRowsOrder(React.Children.toArray(children));
|
||||
handleRowHover(null, false);
|
||||
}, [children, dragStart]);
|
||||
}, [children]);
|
||||
|
||||
const handleRowHover = (index, hoverstatus = true) => {
|
||||
setHoveredRow(hoverstatus ? index : null);
|
||||
@@ -48,7 +48,6 @@ const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||
const updatedRowsOrder = [...rowsOrder];
|
||||
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
||||
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
||||
setRowsOrder(updatedRowsOrder);
|
||||
|
||||
updateReorderedItem({
|
||||
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
||||
|
||||
@@ -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 <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
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 <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<div className="flex flex-3 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<JsSandboxMode collection={collection} />
|
||||
</span>
|
||||
@@ -67,6 +68,12 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span>
|
||||
<ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId">
|
||||
<GlobalEnvironmentSelector />
|
||||
</ToolHint>
|
||||
</span>
|
||||
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -3,15 +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';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
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', []);
|
||||
@@ -27,6 +32,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
// interpolate the url
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
globalEnvironmentVariables,
|
||||
envVars,
|
||||
runtimeVariables: collection.runtimeVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
|
||||
@@ -185,7 +185,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.31.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.33.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ const ToolHint = ({
|
||||
...tooltipStyle,
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
zIndex: 9999,
|
||||
backgroundColor: toolhintBackgroundColor,
|
||||
color: toolhintTextColor
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.31.0'
|
||||
version: '1.33.1'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
|
||||
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();
|
||||
|
||||
@@ -43,7 +44,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 {
|
||||
@@ -68,13 +69,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);
|
||||
@@ -95,13 +96,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);
|
||||
@@ -115,13 +116,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);
|
||||
@@ -135,13 +136,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]
|
||||
@@ -152,13 +153,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'
|
||||
@@ -169,13 +170,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'
|
||||
@@ -186,13 +187,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);
|
||||
@@ -211,7 +212,7 @@ export const HotkeysProvider = (props) => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
|
||||
Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]);
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
|
||||
60
packages/bruno-app/src/providers/Hotkeys/keyMappings.js
Normal file
60
packages/bruno-app/src/providers/Hotkeys/keyMappings.js
Normal file
@@ -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];
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -44,6 +44,8 @@ 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();
|
||||
@@ -183,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) => {
|
||||
@@ -190,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);
|
||||
|
||||
@@ -212,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) => {
|
||||
@@ -220,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)
|
||||
@@ -285,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) => {
|
||||
@@ -292,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) {
|
||||
@@ -970,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();
|
||||
@@ -1141,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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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', { 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -782,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) {
|
||||
@@ -798,6 +815,7 @@ export const getEnvironmentVariables = (collection) => {
|
||||
return variables;
|
||||
};
|
||||
|
||||
|
||||
const getPathParams = (item) => {
|
||||
let pathParams = {};
|
||||
if (item && item.request && item.request.params) {
|
||||
@@ -824,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(' ', ''));
|
||||
|
||||
@@ -373,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;
|
||||
@@ -382,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)
|
||||
@@ -399,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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,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: {
|
||||
|
||||
@@ -74,17 +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 {
|
||||
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 {
|
||||
|
||||
@@ -76,17 +76,17 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
const password = get(request, 'auth.wsse.password', '');
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const nonce = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Create the password digest using SHA-256
|
||||
const hash = crypto.createHash('sha256');
|
||||
// Create the password digest using SHA-1 as required for WSSE
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(nonce + ts + password);
|
||||
const digest = hash.digest('base64');
|
||||
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
|
||||
|
||||
// Construct the WSSE header
|
||||
axiosRequest.headers[
|
||||
'X-WSSE'
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.31.0",
|
||||
"version": "v1.33.1",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,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);
|
||||
@@ -695,6 +697,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) => {
|
||||
|
||||
50
packages/bruno-electron/src/ipc/global-environments.js
Normal file
50
packages/bruno-electron/src/ipc/global-environments.js
Normal file
@@ -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;
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
@@ -68,21 +70,27 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -224,17 +224,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
const password = get(request, 'auth.wsse.password', '');
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const nonce = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Create the password digest using SHA-256
|
||||
const hash = crypto.createHash('sha256');
|
||||
// Create the password digest using SHA-1 as required for WSSE
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(nonce + ts + password);
|
||||
const digest = hash.digest('base64');
|
||||
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
|
||||
|
||||
// Construct the WSSE header
|
||||
axiosRequest.headers[
|
||||
'X-WSSE'
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
|
||||
break;
|
||||
case 'apikey':
|
||||
const apiKeyAuth = get(collectionAuth, 'apikey');
|
||||
@@ -318,17 +318,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
const password = get(request, 'auth.wsse.password', '');
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const nonce = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Create the password digest using SHA-256
|
||||
const hash = crypto.createHash('sha256');
|
||||
// Create the password digest using SHA-1 as required for WSSE
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(nonce + ts + password);
|
||||
const digest = hash.digest('base64');
|
||||
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
|
||||
|
||||
// Construct the WSSE header
|
||||
axiosRequest.headers[
|
||||
'X-WSSE'
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
|
||||
break;
|
||||
case 'apikey':
|
||||
const apiKeyAuth = get(request, 'auth.apikey');
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
132
packages/bruno-electron/src/store/global-environments.js
Normal file
132
packages/bruno-electron/src/store/global-environments.js
Normal file
@@ -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
|
||||
};
|
||||
60
packages/bruno-electron/src/store/ui-state-snapshot.js
Normal file
60
packages/bruno-electron/src/store/ui-state-snapshot.js
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -97,6 +111,14 @@ 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]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -50,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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -48,6 +48,12 @@ const addBrunoResponseShimToContext = (vm, res) => {
|
||||
vm.setProp(resObject, 'getResponseTime', getResponseTime);
|
||||
getResponseTime.dispose();
|
||||
|
||||
let setBody = vm.newFunction('setBody', function (data) {
|
||||
res.setBody(vm.dump(data));
|
||||
});
|
||||
vm.setProp(resObject, 'setBody', setBody);
|
||||
setBody.dispose();
|
||||
|
||||
vm.setProp(vm.global, 'res', resObject);
|
||||
resObject.dispose();
|
||||
};
|
||||
|
||||
@@ -12,12 +12,15 @@ post {
|
||||
|
||||
body:form-urlencoded {
|
||||
form-data-key: {{form-data-key}}
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('form-data-key', 'form-data-value');
|
||||
form-data-stringified-object: {{form-data-stringified-object}}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.body: eq form-data-key=form-data-value
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@ post {
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
foo: {{form-data-key}}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user