From 8425f9f37b59a790f05df73500facb1a008832cf Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Mon, 15 Sep 2025 13:49:17 +0530 Subject: [PATCH] feat: ws settings --- .../Settings/ToggleSelector/index.js | 5 +- .../RequestPane/WSRequestPane/index.js | 3 +- .../WSSettingsPane/StyledWrapper.js | 25 +++ .../RequestPane/WSSettingsPane/index.js | 193 ++++++++++++++++++ packages/bruno-lang/v2/src/bruToJson.js | 28 ++- .../bruno-schema/src/collections/index.js | 30 ++- 6 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/WSSettingsPane/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/WSSettingsPane/index.js diff --git a/packages/bruno-app/src/components/RequestPane/Settings/ToggleSelector/index.js b/packages/bruno-app/src/components/RequestPane/Settings/ToggleSelector/index.js index f0294aee9..899fc5cf7 100644 --- a/packages/bruno-app/src/components/RequestPane/Settings/ToggleSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/Settings/ToggleSelector/index.js @@ -57,7 +57,8 @@ const ToggleSelector = ({ `} /> -
+ {(label || description) + &&
@@ -66,7 +67,7 @@ const ToggleSelector = ({ {description}

)} -
+
} ); }; diff --git a/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js index b7351d1ff..e5d182af3 100644 --- a/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js @@ -11,6 +11,7 @@ import { getPropertyFromDraftOrRequest } from 'utils/collections/index'; import WsBody from '../WsBody/index'; import StyledWrapper from './StyledWrapper'; import WSAuth from './WSAuth'; +import WSSettingsPane from '../WSSettingsPane/index'; const WSRequestPane = ({ item, collection, handleRun }) => { const dispatch = useDispatch(); @@ -43,7 +44,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => { return ; } case 'settings': { - return
TBD
; + return ; } case 'auth': { return ; diff --git a/packages/bruno-app/src/components/RequestPane/WSSettingsPane/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/WSSettingsPane/StyledWrapper.js new file mode 100644 index 000000000..2037cd9a0 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/WSSettingsPane/StyledWrapper.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } + + .tooltip-mod { + font-size: 11px !important; + width: 150px !important; + + & ul { + padding-left: 4px; + } + + & ul > li { + list-style: circle; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/WSSettingsPane/index.js b/packages/bruno-app/src/components/RequestPane/WSSettingsPane/index.js new file mode 100644 index 000000000..85348403d --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/WSSettingsPane/index.js @@ -0,0 +1,193 @@ +import React from 'react'; +import { Tooltip } from 'react-tooltip'; +import { useDispatch } from 'react-redux'; +import get from 'lodash/get'; + +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateItemSettings } from 'providers/ReduxStore/slices/collections'; +import { useTheme } from 'providers/Theme'; + +import ToggleSelector from '../Settings/ToggleSelector/index'; +import StyledWrapper from './StyledWrapper'; +import Accordion from 'components/Accordion'; +import InfoTip from 'components/InfoTip/index'; + +/** + * @param {string} propertyKey + * @param {{draft?:Record}} item + * @returns + */ +const getPropertyFromDraftOrRequest = (propertyKey, item) => + item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {}); + +const WSSettingsPane = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme, theme } = useTheme(); + + const { keepAlive, connectionTimeout, keepAliveInterval } = getPropertyFromDraftOrRequest('settings', item); + + const onToggleKeepAlive = () => { + dispatch( + updateItemSettings({ + collectionUid: collection.uid, + itemUid: item.uid, + settings: { keepAlive: !keepAlive } + }) + ); + }; + + const onChangeConnectionTimeout = (val) => { + dispatch( + updateItemSettings({ + collectionUid: collection.uid, + itemUid: item.uid, + settings: { connectionTimeout: val } + }) + ); + }; + + const onChangeKeepAliveInterval = (val) => { + dispatch( + updateItemSettings({ + collectionUid: collection.uid, + itemUid: item.uid, + settings: { keepAliveInterval: val } + }) + ); + }; + + return ( + +
+
+ + +

+ + Timeout in milliseconds + +

+
+ } + /> + +
+
+ onChangeConnectionTimeout(newValue)} + collection={collection} + /> +
+
+ +
+ + +

+ + Keep the websocket alive by sending ping requests to the server at every interval (in millseconds) + +

+

+ 0 (zero) = off +

+
+ } + /> + +
+
+ onChangeKeepAliveInterval(newValue)} + collection={collection} + /> +
+
+
+ + {/* Variation 2 */} + {/* + + +
+ + { + e.preventDefault(); + e.stopPropagation(); + onToggleKeepAlive(); + }} + size="medium" + /> +
+
+ +
+ +
+ onChangeKeepAliveInterval(newValue)} + collection={collection} + /> +
+
+
+
+
*/} + + {/* Variation 3 */} + {/*
+
+ + { + onToggleKeepAlive(); + }} + size="medium" + /> +
+ {keepAlive ? ( + <> +
+
+
+ +
+ onChangeKeepAliveInterval(newValue)} + collection={collection} + /> +
+
+
+ + ) : null} +
*/} +
+ ); +}; + +export default WSSettingsPane; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 99008f286..bc88b11a3 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -279,6 +279,21 @@ const mapPairListToKeyValPair = (pairList = []) => { return _.merge({}, ...pairList[0]); }; +/** + * @param {Record} obj + * @returns {(key:string, opts?:{fallback: number })=>number|undefined} + */ +const createGetNumFromRecord = + (obj) => + (key, { fallback } = {}) => { + if (!(key in obj)) return fallback; + const asNumber = typeof obj[key] === 'number' ? obj[key] : Number(obj.key); + if (isNaN(asNumber)) { + return fallback; + } + return asNumber; + }; + const sem = grammar.createSemantics().addAttribute('ast', { BruFile(tags) { if (!tags || !tags.ast || !tags.ast.length) { @@ -410,9 +425,20 @@ const sem = grammar.createSemantics().addAttribute('ast', { settings(_1, dictionary) { let settings = mapPairListToKeyValPair(dictionary.ast); + const getNumFromRecord = createGetNumFromRecord(settings); + const keepAliveInterval = getNumFromRecord('keepAliveInterval', { + fallback: 10000 + }); + const connectionTimeout = getNumFromRecord('connectionTimeout', { + fallback: 250 + }); + return { settings: { - encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true' + encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true', + keepAlive: typeof settings.keepAlive === 'boolean' ? settings.keepAlive : settings.keepAlive === 'true', + keepAliveInterval, + connectionTimeout } }; }, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3a24808da..b0adb2f4f 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -417,6 +417,22 @@ const wsRequestSchema = Yup.object({ .noUnknown(true) .strict(); +const wsSettingsSchema = Yup.object({ + settings: Yup.object({ + connectionTimeout: Yup.number() + .default(1000) + .min(250), + keepAlive: Yup.boolean().default(false), + keepAliveInterval: Yup.number() + .default(10000) + .min(500) + // max 2 minutes + .max(2 * 60 * 1000), + }).noUnknown(true) + .strict() + .nullable() +}); + const folderRootSchema = Yup.object({ request: Yup.object({ headers: Yup.array().of(keyValueSchema).nullable(), @@ -472,12 +488,16 @@ const itemSchema = Yup.object({ }) }) }), - settings: Yup.object({ - encodeUrl: Yup.boolean().nullable() - }) - .noUnknown(true) + settings: Yup.mixed() + .when('type', { + is: (type) => type === 'ws-request', + then: wsSettingsSchema, + otherwise: Yup.object({ + encodeUrl: Yup.boolean().nullable() + }).noUnknown(true) .strict() - .nullable(), + .nullable() + }), fileContent: Yup.string().when('type', { // If the type is 'js', the fileContent field is expected to be a string. // This can include an empty string, indicating that the JS file may not have any content.