Merge branch 'main' into feature/env-secrets

This commit is contained in:
Anoop M D 2023-09-24 23:11:45 +05:30 committed by GitHub
commit 2dadad3af0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 600 additions and 260 deletions

View File

@ -13,10 +13,12 @@
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"about-window": "^1.15.2",
"husky": "^8.0.3",
"jest": "^29.2.0",
"pretty-quick": "^3.1.3",

View File

@ -47,6 +47,7 @@
"react-dom": "18.2.0",
"react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0",
"react-inspector": "^6.0.2",
"react-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",

View File

@ -5,10 +5,20 @@ const Wrapper = styled.div`
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) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
thead {
@ -16,7 +26,7 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
user-select: none;
}
td {
thead td {
padding: 6px 10px;
}
}

View File

@ -2,13 +2,16 @@ import React, { useReducer } from 'react';
import toast from 'react-hot-toast';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import reducer from './reducer';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
const { variables, hasChanges } = state;
@ -100,15 +103,11 @@ const EnvironmentVariables = ({ environment, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={variable.value || ''}
className="mousetrap"
onChange={(e) => handleVarChange(e, variable, 'value')}
<SingleLineEditor
value={variable.value}
theme={storedTheme}
onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
collection={collection}
/>
</td>
<td>

View File

@ -19,7 +19,7 @@ const Wrapper = styled.div`
align-items: flex-start;
justify-content: center;
overflow-y: auto;
z-index: 1003;
z-index: 10;
}
.bruno-modal-card {
@ -28,7 +28,7 @@ const Wrapper = styled.div`
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
z-index: 1003;
z-index: 10;
max-width: calc(100% - var(--spacing-base-unit));
box-shadow: var(--box-shadow-base);
display: flex;

View File

@ -13,6 +13,7 @@ import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import RunnerResults from 'components/RunnerResults';
import VariablesEditor from 'components/VariablesEditor';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
@ -123,6 +124,10 @@ const RequestTabPanel = () => {
return <RunnerResults collection={collection} />;
}
if (focusedTab.type === 'variables') {
return <VariablesEditor collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;

View File

@ -1,7 +1,8 @@
import React from 'react';
import { IconFiles, IconRun } from '@tabler/icons';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import VariablesView from 'components/VariablesView';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
@ -17,6 +18,16 @@ const CollectionToolBar = ({ collection }) => {
);
};
const viewVariables = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'variables'
})
);
};
return (
<StyledWrapper>
<div className="flex items-center p-2">
@ -28,7 +39,9 @@ const CollectionToolBar = ({ collection }) => {
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>
<VariablesView collection={collection} />
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</span>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@ -0,0 +1,23 @@
import React from 'react';
import { IconVariable } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, text }) => {
return (
<>
<div className="flex items-center tab-label pl-2">
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">{text}</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
</>
);
};
export default SpecialTab;

View File

@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
@ -56,6 +57,14 @@ const RequestTab = ({ tab, collection }) => {
return color;
};
if (tab.type === 'variables') {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" />
</StyledWrapper>
);
}
const item = findItemInCollection(collection, tab.uid);
if (!item) {

View File

@ -114,7 +114,7 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
</li>
);
})

View File

@ -19,6 +19,7 @@ const StyledWrapper = styled.div`
.CodeMirror-scroll {
overflow: hidden !important;
padding-bottom: 50px !important;
}
.CodeMirror-hscrollbar {

View File

@ -31,6 +31,7 @@ class SingleLineEditor extends Component {
brunoVarInfo: {
variables: getAllVariables(this.props.collection)
},
scrollbarStyle: null,
extraKeys: {
Enter: () => {
if (this.props.onRun) {

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
table {
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
li {
background-color: ${(props) => props.theme.bg} !important;
}
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,92 @@
import React from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { Inspector } from 'react-inspector';
import { useTheme } from 'providers/Theme';
import { findEnvironmentInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const KeyValueExplorer = ({ data, theme }) => {
data = data || {};
return (
<div>
<table className="border-collapse">
<tbody>
{Object.entries(data).map(([key, value]) => (
<tr key={key}>
<td className="px-2 py-1">{key}</td>
<td className="px-2 py-1">
<Inspector data={value} theme={theme} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const EnvVariables = ({ collection, theme }) => {
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if (!environment) {
return (
<>
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
<div className="muted">No environment selected</div>
</>
);
}
const envVars = get(environment, 'variables', []);
const enabledEnvVars = filter(envVars, (variable) => variable.enabled);
const envVarsObj = enabledEnvVars.reduce((acc, curr) => {
acc[curr.name] = curr.value;
return acc;
}, {});
return (
<>
<div className="flex items-center mt-4 mb-2">
<h1 className="font-semibold">Environment Variables</h1>
<span className="muted ml-2">({environment.name})</span>
</div>
{enabledEnvVars.length > 0 ? (
<KeyValueExplorer data={envVarsObj} theme={theme} />
) : (
<div className="muted">No environment variables found</div>
)}
</>
);
};
const CollectionVariables = ({ collection, theme }) => {
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
return (
<>
<h1 className="font-semibold mb-2">Collection Variables</h1>
{collectionVariablesFound ? (
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
) : (
<div className="muted">No collection variables found</div>
)}
</>
);
};
const VariablesEditor = ({ collection }) => {
const { storedTheme } = useTheme();
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
return (
<StyledWrapper className="px-4 py-4">
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
<EnvVariables collection={collection} theme={reactInspectorTheme} />
</StyledWrapper>
);
};
export default VariablesEditor;

View File

@ -1,19 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: absolute;
min-width: fit-content;
font-size: 14px;
top: 36px;
right: 0;
white-space: nowrap;
z-index: 1000;
background-color: ${(props) => props.theme.variables.bg};
.popover {
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
`;
export default Wrapper;

View File

@ -1,26 +0,0 @@
import React, { useRef } from 'react';
import StyledWrapper from './StyledWrapper';
import useOnClickOutside from 'hooks/useOnClickOutside';
const PopOver = ({ children, iconRef, handleClose }) => {
const popOverRef = useRef(null);
useOnClickOutside(popOverRef, (e) => {
if (iconRef && iconRef.current) {
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
return;
}
}
handleClose();
});
return (
<StyledWrapper>
<div className="popover" ref={popOverRef}>
<div className="popover-content">{children}</div>
</div>
</StyledWrapper>
);
};
export default PopOver;

View File

@ -1,15 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
align-self: stretch;
display: flex;
align-items: center;
.view-environment {
width: 1rem;
font-size: 10px;
}
`;
export default StyledWrapper;

View File

@ -1,19 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.variable-name {
color: ${(props) => props.theme.variables.name.color};
}
.variable-name {
min-width: 180px;
}
.variable-value {
max-width: 600px;
inline-size: 600px;
overflow-wrap: break-word;
}
`;
export default StyledWrapper;

View File

@ -1,53 +0,0 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import StyledWrapper from './StyledWrapper';
const VariablesTable = ({ variables, collectionVariables }) => {
const collectionVars = [];
forOwn(cloneDeep(collectionVariables), (value, key) => {
collectionVars.push({
uid: uuid(),
name: key,
value: value
});
});
return (
<StyledWrapper>
<div className="flex flex-col w-full">
<div className="mb-2 font-medium">Environment Variables</div>
{variables && variables.length ? (
variables.map((variable) => {
return (
<div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
</div>
);
})
) : (
<small>No env variables found</small>
)}
<div className="mt-2 font-medium">Collection Variables</div>
{collectionVars && collectionVars.length ? (
collectionVars.map((variable) => {
return (
<div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
</div>
);
})
) : (
<small>No collection variables found</small>
)}
</div>
</StyledWrapper>
);
};
export default VariablesTable;

View File

@ -1,48 +0,0 @@
import React, { useState, useRef } from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { findEnvironmentInCollection } from 'utils/collections';
import VariablesTable from './VariablesTable';
import StyledWrapper from './StyledWrapper';
import PopOver from './Popover';
import { IconEye } from '@tabler/icons';
const VariablesView = ({ collection }) => {
const iconRef = useRef(null);
const [popOverOpen, setPopOverOpen] = useState(false);
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const variables = get(environment, 'variables', []);
const enabledVariables = filter(variables, (variable) => variable.enabled);
const showVariablesTable =
enabledVariables.length > 0 ||
(collection.collectionVariables && Object.keys(collection.collectionVariables).length > 0);
return (
<StyledWrapper className="mr-2 server-syncstatus-icon" ref={iconRef}>
<div
className="flex p-1 items-center"
onClick={() => setPopOverOpen(true)}
onMouseEnter={() => setPopOverOpen(true)}
onMouseLeave={() => setPopOverOpen(false)}
>
<div className="cursor-pointer view-environment">
<IconEye size={18} strokeWidth={1.5} />
</div>
{popOverOpen && (
<PopOver iconRef={iconRef} handleClose={() => setPopOverOpen(false)}>
<div className="px-2 py-1">
{showVariablesTable ? (
<VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables} />
) : (
'No variables found'
)}
</div>
</PopOver>
)}
</div>
</StyledWrapper>
);
};
export default VariablesView;

View File

@ -8,6 +8,7 @@ import {
collectionUnlinkDirectoryEvent,
collectionUnlinkEnvFileEvent,
scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
collectionRenamedEvent,
runRequestEvent,
runFolderEvent
@ -97,6 +98,10 @@ const useCollectionTreeSync = () => {
dispatch(scriptEnvironmentUpdateEvent(val));
};
const _processEnvUpdate = (val) => {
dispatch(processEnvUpdateEvent(val));
};
const _collectionRenamed = (val) => {
dispatch(collectionRenamedEvent(val));
};
@ -119,7 +124,8 @@ const useCollectionTreeSync = () => {
const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent);
const removeListener9 = ipcRenderer.on('main:console-log', (val) => {
const removeListener9 = ipcRenderer.on('main:process-env-update', _processEnvUpdate);
const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args);
});
@ -133,6 +139,7 @@ const useCollectionTreeSync = () => {
removeListener7();
removeListener8();
removeListener9();
removeListener10();
};
}, [isElectron]);
};

View File

@ -177,6 +177,14 @@ export const collectionsSlice = createSlice({
collection.collectionVariables = collectionVariables;
}
},
processEnvUpdateEvent: (state, action) => {
const { collectionUid, processEnvVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.processEnvVariables = processEnvVariables;
}
},
requestCancelled: (state, action) => {
const { itemUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@ -1158,6 +1166,7 @@ export const {
renameItem,
cloneItem,
scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
requestCancelled,
responseReceived,
saveRequest,

View File

@ -19,12 +19,22 @@ export const tabsSlice = createSlice({
if (alreadyExists) {
return;
}
if (action.payload.type === 'variables') {
const tab = find(state.tabs, (t) => t.collectionUid === action.payload.collectionUid && t.type === 'variables');
if (tab) {
state.activeTabUid = tab.uid;
return;
}
}
state.tabs.push({
uid: action.payload.uid,
collectionUid: action.payload.collectionUid,
requestPaneWidth: null,
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response'
responsePaneTab: 'response',
type: action.payload.type || 'request'
});
state.activeTabUid = action.payload.uid;
},
@ -55,16 +65,22 @@ export const tabsSlice = createSlice({
closeTabs: (state, action) => {
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
const tabUids = action.payload.tabUids || [];
// remove the tabs from the state
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
if (activeTab && state.tabs.length) {
const { collectionUid } = activeTab;
const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
// if the active tab no longer exists, set the active tab to the last tab in the list
// this implies that the active tab was closed
if (!activeTabStillExists) {
// attempt to load sibling tabs (based on collections) of the dead tab
// load sibling tabs of the current collection
const siblingTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);
// if there are sibling tabs, set the active tab to the last sibling tab
// otherwise, set the active tab to the last tab in the list
if (siblingTabs && siblingTabs.length) {
state.activeTabUid = last(siblingTabs).uid;
} else {

View File

@ -1 +1 @@
@import "buttons";
@import 'buttons';

View File

@ -1,4 +1,3 @@
:root {
--color-brand: #546de5;
--color-text: rgb(52 52 52);
@ -21,7 +20,8 @@
--color-method-head: rgb(52 52 52);
}
html, body {
html,
body {
margin: 0;
padding: 0;
font-size: 1rem;
@ -38,15 +38,18 @@ body {
font-size: 0.875rem;
}
body::-webkit-scrollbar, .CodeMirror-vscrollbar::-webkit-scrollbar {
body::-webkit-scrollbar,
.CodeMirror-vscrollbar::-webkit-scrollbar {
width: 0.6rem;
}
body::-webkit-scrollbar-track, .CodeMirror-vscrollbar::-webkit-scrollbar-track {
body::-webkit-scrollbar-track,
.CodeMirror-vscrollbar::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
body::-webkit-scrollbar-thumb, .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
body::-webkit-scrollbar-thumb,
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background-color: #cdcdcd;
border-radius: 5rem;
}

View File

@ -8,6 +8,7 @@
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const { get } = require('lodash');
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
@ -20,7 +21,7 @@ if (!SERVER_RENDERED) {
// str is of format {{variableName}}, extract variableName
// we are seeing that from the gql query editor, the token string is of format variableName
const variableName = str.replace('{{', '').replace('}}', '').trim();
const variableValue = options.variables[variableName];
const variableValue = get(options.variables, variableName);
const into = document.createElement('div');
const descriptionDiv = document.createElement('div');

View File

@ -542,6 +542,11 @@ export const getAllVariables = (collection) => {
return {
...environmentVariables,
...collection.collectionVariables
...collection.collectionVariables,
process: {
env: {
...collection.processEnvVariables
}
}
};
};

View File

@ -1,3 +1,6 @@
import get from 'lodash/get';
import isString from 'lodash/isString';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@ -5,6 +8,11 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
const pathFoundInVariables = (path, obj) => {
const value = get(obj, path);
return isString(value);
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
let variablesOverlay = {
@ -15,7 +23,8 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
while ((ch = stream.next()) != null) {
if (ch == '}' && stream.next() == '}') {
stream.eat('}');
if (word in variables) {
let found = pathFoundInVariables(word, variables);
if (found) {
return 'variable-valid';
} else {
return 'variable-invalid';

View File

@ -1,7 +1,7 @@
{
"version": "v0.14.1",
"name": "bruno",
"description": "Opensource API Client",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
"private": true,
"main": "src/index.js",
@ -28,6 +28,7 @@
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",
"handlebars": "^4.7.8",
"is-valid-path": "^0.1.1",
"lodash": "^4.17.21",
"mustache": "^4.2.0",

View File

@ -1,11 +1,13 @@
const { ipcMain } = require('electron');
const openAboutWindow = require('about-window').default;
const { join } = require('path');
const template = [
{
label: 'Collection',
submenu: [
{
label: 'Open Local Collection',
label: 'Open Collection',
click() {
ipcMain.emit('main:open-collection');
}
@ -21,7 +23,8 @@ const template = [
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
{ role: 'paste' },
{ role: 'selectAll' }
]
},
{
@ -42,7 +45,19 @@ const template = [
},
{
role: 'help',
submenu: [{ label: 'Learn More' }]
submenu: [
{
label: 'About Bruno',
click: () =>
openAboutWindow({
product_name: 'Bruno',
icon_path: join(__dirname, '../../resources/icons/png/256x256.png'),
homepage: 'https://www.usebruno.com/',
package_json_dir: join(__dirname, '../..')
})
},
{ label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }
]
}
];

View File

@ -4,12 +4,14 @@ const path = require('path');
const chokidar = require('chokidar');
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
const { itemSchema } = require('@usebruno/schema');
const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
@ -21,6 +23,13 @@ const isJsonEnvironmentConfig = (pathname, collectionPath) => {
return dirname === collectionPath && basename === 'environments.json';
};
const isDotEnvFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === '.env';
};
const isBruEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
@ -158,6 +167,25 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
const add = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher add: ${pathname}`);
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isJsonEnvironmentConfig(pathname, collectionPath)) {
try {
const dirname = path.dirname(pathname);
@ -253,6 +281,25 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
};
const change = async (win, pathname, collectionUid, collectionPath) => {
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);
}

View File

@ -5,7 +5,7 @@ const { BrowserWindow, app, Menu } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
const menuTemplate = require('./app/menu-template');
const LastOpenedCollections = require('./app/last-opened-collections');
const LastOpenedCollections = require('./store/last-opened-collections');
const registerNetworkIpc = require('./ipc/network');
const registerCollectionsIpc = require('./ipc/collection');
const Watcher = require('./app/watcher');

View File

@ -1,7 +1,7 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain } = require('electron');
const { ipcMain, shell } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const {
@ -17,7 +17,7 @@ const { stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require('../app/preferences');
const { setPreferences } = require('../store/preferences');
const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
@ -460,6 +460,11 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
}
});
ipcMain.on('main:open-docs', () => {
const docsURL = 'https://docs.usebruno.com';
shell.openExternal(docsURL);
});
ipcMain.on('main:collection-opened', (win, pathname, uid) => {
watcher.addWatcher(win, pathname, uid);
lastOpenedCollections.add(pathname);

View File

@ -12,7 +12,8 @@ const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../util
const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { getPreferences } = require('../../app/preferences');
const { getPreferences } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@ -129,12 +130,14 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath
);
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
requestUid,
collectionUid
});
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
requestUid,
collectionUid
});
}
}
// run pre-request script
@ -158,7 +161,9 @@ const registerNetworkIpc = (mainWindow) => {
});
}
interpolateVars(request, envVars, collectionVariables);
const processEnvVars = getProcessEnvVars(collectionUid);
interpolateVars(request, envVars, collectionVariables, processEnvVars);
// stringify the request url encoded params
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
@ -222,12 +227,14 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath
);
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
requestUid,
collectionUid
});
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
requestUid,
collectionUid
});
}
}
// run post-response script
@ -520,7 +527,21 @@ const registerNetworkIpc = (mainWindow) => {
const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars && preRequestVars.length) {
const varsRuntime = new VarsRuntime();
varsRuntime.runPreRequestVars(preRequestVars, request, envVars, collectionVariables, collectionPath);
const result = varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVars,
collectionVariables,
collectionPath
);
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
}
// run pre-request script
@ -543,8 +564,10 @@ const registerNetworkIpc = (mainWindow) => {
});
}
const processEnvVars = getProcessEnvVars(collectionUid);
// interpolate variables inside request
interpolateVars(request, envVars, collectionVariables);
interpolateVars(request, envVars, collectionVariables, processEnvVars);
// todo:
// i have no clue why electron can't send the request object
@ -587,11 +610,13 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath
);
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
}
// run response script

View File

@ -1,24 +1,51 @@
const Mustache = require('mustache');
const { each, get, forOwn } = require('lodash');
const Handlebars = require('handlebars');
const { each, forOwn, cloneDeep } = require('lodash');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
const interpolateEnvVars = (str, processEnvVars) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
return template({
process: {
env: {
...processEnvVars
}
}
});
};
const interpolateVars = (request, envVars = {}, collectionVariables = {}) => {
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
// envVars can inturn have values as {{process.env.VAR_NAME}}
// so we need to interpolate envVars first with processEnvVars
forOwn(envVars, (value, key) => {
envVars[key] = interpolateEnvVars(value, processEnvVars);
});
const interpolate = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
// collectionVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
};
return Mustache.render(str, combinedVars);
return template(combinedVars);
};
request.url = interpolate(request.url);

View File

@ -0,0 +1,37 @@
/**
* This file stores all the process.env variables under collection scope
*
* process.env variables are sourced from 2 places:
* 1. .env file in the root of the project
* 2. process.env variables set in the OS
*
* Multiple collections can be opened in the same electron app.
* Each collection's .env file can have different values for the same process.env variable.
*/
const dotEnvVars = {};
// collectionUid is a hash based on the collection path)
const getProcessEnvVars = (collectionUid) => {
// if there are no .env vars for this collection, return the process.env
if (!dotEnvVars[collectionUid]) {
return {
...process.env
};
}
// if there are .env vars for this collection, return the process.env merged with the .env vars
return {
...process.env,
...dotEnvVars[collectionUid]
};
};
const setDotEnvVars = (collectionUid, envVars) => {
dotEnvVars[collectionUid] = envVars;
};
module.exports = {
getProcessEnvVars,
setDotEnvVars
};

View File

@ -41,10 +41,39 @@ const generateUidBasedOnHash = (str) => {
return `${hash}`.padEnd(21, '0');
};
const flattenDataForDotNotation = (data) => {
var result = {};
function recurse(current, prop) {
if (Object(current) !== current) {
result[prop] = current;
} else if (Array.isArray(current)) {
for (var i = 0, l = current.length; i < l; i++) {
recurse(current[i], prop + '[' + i + ']');
}
if (l == 0) {
result[prop] = [];
}
} else {
var isEmpty = true;
for (var p in current) {
isEmpty = false;
recurse(current[p], prop ? prop + '.' + p : p);
}
if (isEmpty && prop) {
result[prop] = {};
}
}
}
recurse(data, '');
return result;
};
module.exports = {
uuid,
stringifyJson,
parseJson,
simpleHash,
generateUidBasedOnHash
generateUidBasedOnHash,
flattenDataForDotNotation
};

View File

@ -0,0 +1,85 @@
const { flattenDataForDotNotation } = require('../../src/utils/common');
describe('utils: flattenDataForDotNotation', () => {
test('Flatten a simple object with dot notation', () => {
const input = {
person: {
name: 'John',
age: 30,
},
};
const expectedOutput = {
'person.name': 'John',
'person.age': 30,
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with nested arrays', () => {
const input = {
users: [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 28 },
],
};
const expectedOutput = {
'users[0].name': 'Alice',
'users[0].age': 25,
'users[1].name': 'Bob',
'users[1].age': 28,
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an empty object', () => {
const input = {};
const expectedOutput = {};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with nested objects', () => {
const input = {
person: {
name: 'Alice',
address: {
city: 'New York',
zipcode: '10001',
},
},
};
const expectedOutput = {
'person.name': 'Alice',
'person.address.city': 'New York',
'person.address.zipcode': '10001',
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with arrays of objects', () => {
const input = {
teams: [
{ name: 'Team A', members: ['Alice', 'Bob'] },
{ name: 'Team B', members: ['Charlie', 'David'] },
],
};
const expectedOutput = {
'teams[0].name': 'Team A',
'teams[0].members[0]': 'Alice',
'teams[0].members[1]': 'Bob',
'teams[1].name': 'Team B',
'teams[1].members[0]': 'Charlie',
'teams[1].members[1]': 'David',
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
});

View File

@ -10,6 +10,7 @@ const punycode = require('punycode');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const { cleanJson } = require('../utils');
// Inbuilt Library Support
const atob = require('atob');
@ -37,7 +38,7 @@ class ScriptRuntime {
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
onConsoleLog(type, args);
onConsoleLog(type, cleanJson(args));
};
};
context.console = {
@ -81,8 +82,8 @@ class ScriptRuntime {
await asyncVM();
return {
request,
envVariables,
collectionVariables
envVariables: cleanJson(envVariables),
collectionVariables: cleanJson(collectionVariables)
};
}
@ -100,7 +101,7 @@ class ScriptRuntime {
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
onConsoleLog(type, args);
onConsoleLog(type, cleanJson(args));
};
};
context.console = {
@ -136,8 +137,8 @@ class ScriptRuntime {
return {
response,
envVariables,
collectionVariables
envVariables: cleanJson(envVariables),
collectionVariables: cleanJson(collectionVariables)
};
}
}

View File

@ -6,6 +6,7 @@ const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const Test = require('../test');
const TestResults = require('../test-results');
const { cleanJson } = require('../utils');
// Inbuilt Library Support
const atob = require('atob');
@ -49,7 +50,7 @@ class TestRuntime {
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
onConsoleLog(type, args);
onConsoleLog(type, cleanJson(args));
};
};
context.console = {
@ -82,9 +83,9 @@ class TestRuntime {
return {
request,
envVariables,
collectionVariables,
results: __brunoTestResults.getResults()
envVariables: cleanJson(envVariables),
collectionVariables: cleanJson(collectionVariables),
results: cleanJson(__brunoTestResults.getResults())
};
}
}

View File

@ -118,9 +118,28 @@ const createResponseParser = (response = {}) => {
return res;
};
/**
* Objects that are created inside vm2 execution context result in an serilaization error when sent to the renderer process
* Error sending from webFrameMain: Error: Failed to serialize arguments
* at s.send (node:electron/js2c/browser_init:169:631)
* at g.send (node:electron/js2c/browser_init:165:2156)
* How to reproduce
* Remove the cleanJson fix and execute the below post response script
* bru.setVar("a", {b:3});
* Todo: Find a better fix
*/
const cleanJson = (data) => {
try {
return JSON.parse(JSON.stringify(data));
} catch (e) {
return data;
}
};
module.exports = {
evaluateJsExpression,
evaluateJsTemplateLiteral,
createResponseParser,
internalExpressionCache
internalExpressionCache,
cleanJson
};

View File

@ -4,6 +4,7 @@ const bruToJsonV2 = require('../v2/src/bruToJson');
const jsonToBruV2 = require('../v2/src/jsonToBru');
const bruToEnvJsonV2 = require('../v2/src/envToJson');
const envJsonToBruV2 = require('../v2/src/jsonToEnv');
const dotenvToJson = require('../v2/src/dotenvToJson');
module.exports = {
bruToJson,
@ -14,5 +15,7 @@ module.exports = {
bruToJsonV2,
jsonToBruV2,
bruToEnvJsonV2,
envJsonToBruV2
envJsonToBruV2,
dotenvToJson
};