feat(#224): proxy support feature - gui layer

This commit is contained in:
Anoop M D 2023-09-28 03:06:53 +05:30
parent c0b7dad030
commit 665428a2d0
15 changed files with 383 additions and 28 deletions

View File

@ -0,0 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,190 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import StyledWrapper from './StyledWrapper';
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const formik = useFormik({
initialValues: {
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
},
validationSchema: Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https']),
hostname: Yup.string().max(1024),
port: Yup.number().min(0).max(65535),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
})
}),
onSubmit: (values) => {
onUpdate(values);
}
});
useEffect(() => {
formik.setValues({
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
});
}, [proxyConfig]);
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center mr-4">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
http
</label>
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
https
</label>
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<input
id="auth.password"
type="text"
name="auth.password"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default ProxySettings;

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,34 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
import StyledWrapper from './StyledWrapper';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
return (
<StyledWrapper className="px-4 py-4">
<h1 className="font-semibold mb-4">Collection Settings</h1>
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
</StyledWrapper>
);
};
export default CollectionSettings;

View File

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

View File

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

View File

@ -1,13 +1,31 @@
import React from 'react'; import React from 'react';
import { IconVariable } from '@tabler/icons'; import { IconVariable, IconSettings } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type }) => {
const getTabInfo = (type) => {
switch (type) {
case 'collection-settings': {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Settings</span>
</>
);
}
case 'variables': {
return (
<>
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Variables</span>
</>
);
}
}
};
const SpecialTab = ({ handleCloseClick, text }) => {
return ( return (
<> <>
<div className="flex items-center tab-label pl-2"> <div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
<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)}> <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"> <svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path <path

View File

@ -57,10 +57,10 @@ const RequestTab = ({ tab, collection }) => {
return color; return color;
}; };
if (tab.type === 'variables') { if (['collection-settings', 'variables'].includes(tab.type)) {
return ( return (
<StyledWrapper className="flex items-center justify-between tab-container px-1"> <StyledWrapper className="flex items-center justify-between tab-container px-1">
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" /> <SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
</StyledWrapper> </StyledWrapper>
); );
} }

View File

@ -11,7 +11,8 @@ import {
processEnvUpdateEvent, processEnvUpdateEvent,
collectionRenamedEvent, collectionRenamedEvent,
runRequestEvent, runRequestEvent,
runFolderEvent runFolderEvent,
brunoConfigUpdateEvent
} from 'providers/ReduxStore/slices/collections'; } from 'providers/ReduxStore/slices/collections';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions'; import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
@ -27,8 +28,8 @@ const useCollectionTreeSync = () => {
const { ipcRenderer } = window; const { ipcRenderer } = window;
const _openCollection = (pathname, uid, name) => { const _openCollection = (pathname, uid, brunoConfig) => {
dispatch(openCollectionEvent(uid, pathname, name)); dispatch(openCollectionEvent(uid, pathname, brunoConfig));
}; };
const _collectionTreeUpdated = (type, val) => { const _collectionTreeUpdated = (type, val) => {
@ -128,6 +129,7 @@ const useCollectionTreeSync = () => {
const removeListener10 = ipcRenderer.on('main:console-log', (val) => { const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args); console[val.type](...val.args);
}); });
const removeListener11 = ipcRenderer.on('main:bruno-config-update', (val) => dispatch(brunoConfigUpdateEvent(val)));
return () => { return () => {
removeListener1(); removeListener1();
@ -140,6 +142,7 @@ const useCollectionTreeSync = () => {
removeListener8(); removeListener8();
removeListener9(); removeListener9();
removeListener10(); removeListener10();
removeListener11();
}; };
}, [isElectron]); }, [isElectron]);
}; };

View File

@ -750,15 +750,32 @@ export const browseDirectory = () => (dispatch, getState) => {
}); });
}; };
export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState) => { export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
return reject(new Error('Collection not found'));
}
return new Promise((resolve, reject) => {
ipcRenderer
.invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid)
.then(resolve)
.catch(reject);
});
};
export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {
const collection = { const collection = {
version: '1', version: '1',
uid: uid, uid: uid,
name: name, name: brunoConfig.name,
pathname: pathname, pathname: pathname,
items: [], items: [],
showRunner: false, showRunner: false,
collectionVariables: {} collectionVariables: {},
brunoConfig: brunoConfig
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -52,6 +52,14 @@ export const collectionsSlice = createSlice({
state.collections.push(collection); state.collections.push(collection);
} }
}, },
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.brunoConfig = brunoConfig;
}
},
renameCollection: (state, action) => { renameCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1155,6 +1163,7 @@ export const collectionsSlice = createSlice({
export const { export const {
createCollection, createCollection,
brunoConfigUpdateEvent,
renameCollection, renameCollection,
removeCollection, removeCollection,
updateLastAction, updateLastAction,

View File

@ -59,10 +59,10 @@ const openCollectionDialog = async (win, watcher) => {
const openCollection = async (win, watcher, collectionPath, options = {}) => { const openCollection = async (win, watcher, collectionPath, options = {}) => {
if (!watcher.hasWatcher(collectionPath)) { if (!watcher.hasWatcher(collectionPath)) {
try { try {
const { name } = await getCollectionConfigFile(collectionPath); const brunoConfig = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath); const uid = generateUidBasedOnHash(collectionPath);
win.webContents.send('main:collection-opened', collectionPath, uid, name); win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', win, collectionPath, uid); ipcMain.emit('main:collection-opened', win, collectionPath, uid);
} catch (err) { } catch (err) {
if (!options.dontSendDisplayErrors) { if (!options.dontSendDisplayErrors) {

View File

@ -178,9 +178,9 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
if (isBrunoConfigFile(pathname, collectionPath)) { if (isBrunoConfigFile(pathname, collectionPath)) {
try { try {
const content = fs.readFileSync(pathname, 'utf8'); const content = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(content); const brunoConfig = JSON.parse(content);
setBrunoConfig(collectionUid, jsonData); setBrunoConfig(collectionUid, brunoConfig);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -303,9 +303,15 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
if (isBrunoConfigFile(pathname, collectionPath)) { if (isBrunoConfigFile(pathname, collectionPath)) {
try { try {
const content = fs.readFileSync(pathname, 'utf8'); const content = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(content); const brunoConfig = JSON.parse(content);
setBrunoConfig(collectionUid, jsonData); const payload = {
collectionUid,
brunoConfig: brunoConfig
};
setBrunoConfig(collectionUid, brunoConfig);
win.webContents.send('main:bruno-config-update', payload);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }

View File

@ -57,14 +57,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(dirPath); await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath); const uid = generateUidBasedOnHash(dirPath);
const content = await stringifyJson({ const brunoConfig = {
version: '1', version: '1',
name: collectionName, name: collectionName,
type: 'collection' type: 'collection'
}); };
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(dirPath, 'bruno.json'), content); await writeFile(path.join(dirPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', dirPath, uid, collectionName); mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
return; return;
@ -356,14 +357,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(collectionPath); await createDirectory(collectionPath);
const uid = generateUidBasedOnHash(collectionPath); const uid = generateUidBasedOnHash(collectionPath);
const content = await stringifyJson({ const brunoConfig = {
version: '1', version: '1',
name: collection.name, name: collection.name,
type: 'collection' type: 'collection'
}); };
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(collectionPath, 'bruno.json'), content); await writeFile(path.join(collectionPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, collectionName); mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid); ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid);
lastOpenedCollections.add(collectionPath); lastOpenedCollections.add(collectionPath);
@ -451,6 +453,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:set-preferences', async (event, preferences) => { ipcMain.handle('renderer:set-preferences', async (event, preferences) => {
setPreferences(preferences); setPreferences(preferences);
}); });
ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionUid) => {
try {
const brunoConfigPath = path.join(collectionPath, 'bruno.json');
const content = await stringifyJson(brunoConfig);
await writeFile(brunoConfigPath, content);
} catch (error) {
return Promise.reject(error);
}
});
}; };
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {

View File

@ -128,7 +128,8 @@ const collectionSchema = Yup.object({
runnerResult: Yup.object({ runnerResult: Yup.object({
items: Yup.array() items: Yup.array()
}), }),
collectionVariables: Yup.object() collectionVariables: Yup.object(),
brunoConfig: Yup.object()
}) })
.noUnknown(true) .noUnknown(true)
.strict(); .strict();