diff --git a/assets/images/landing-2.png b/assets/images/landing-2.png index 156d207e5..2d3fa7f12 100644 Binary files a/assets/images/landing-2.png and b/assets/images/landing-2.png differ diff --git a/e2e-tests/footer/notifications/notifications.spec.js b/e2e-tests/footer/notifications/notifications.spec.js new file mode 100644 index 000000000..238afcc98 --- /dev/null +++ b/e2e-tests/footer/notifications/notifications.spec.js @@ -0,0 +1,25 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Notifications Modal', () => { + test('should open notifications modal when clicking bell icon and close with close button', async ({ page }) => { + // Get the notification bell icon in the status bar + const notificationBell = page.getByLabel('Check all Notifications'); + + // Click on the bell icon to open notifications + await notificationBell.click(); + + // Get modal elements + const notificationsModal = page.locator('.bruno-modal'); + const modalCloseButton = notificationsModal.locator('div.bruno-modal-header div.close'); + + // Verify modal is visible and has the correct title + await expect(notificationsModal).toBeVisible(); + await expect(notificationsModal.locator('.bruno-modal-header-title')).toContainText('NOTIFICATIONS'); + + // Click the close button + await modalCloseButton.click(); + + // Verify modal is closed + await expect(notificationsModal).not.toBeVisible(); + }); +}); \ No newline at end of file diff --git a/e2e-tests/footer/sidebar-toggle/sidebar-toggle.spec.js b/e2e-tests/footer/sidebar-toggle/sidebar-toggle.spec.js new file mode 100644 index 000000000..d8e7dfb21 --- /dev/null +++ b/e2e-tests/footer/sidebar-toggle/sidebar-toggle.spec.js @@ -0,0 +1,36 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Sidebar Toggle', () => { + test('should toggle sidebar visibility when clicking the toggle button', async ({ page }) => { + // Get the sidebar and toggle button elements + const sidebar = page.locator('aside.sidebar'); + const toggleButton = page.getByLabel('Toggle Sidebar'); + const dragHandle = page.locator('.sidebar-drag-handle'); + + // Initial state - sidebar and drag handle should be visible + await expect(sidebar).toBeVisible(); + await expect(dragHandle).toBeVisible(); + + // Click toggle to hide sidebar + await toggleButton.click(); + + // Wait for transition to complete and verify sidebar and drag handle are hidden + await expect(sidebar).not.toBeVisible(); + await expect(dragHandle).not.toBeVisible(); + + // Verify the sidebar has collapsed width + const sidebarBox = await sidebar.boundingBox(); + expect(sidebarBox?.width).toBe(0); + + // Click toggle again to show sidebar + await toggleButton.click(); + + // Wait for transition and verify sidebar and drag handle are visible again + await expect(sidebar).toBeVisible(); + await expect(dragHandle).toBeVisible(); + + // Verify the sidebar has expanded width + const expandedSidebarBox = await sidebar.boundingBox(); + expect(expandedSidebarBox?.width).toBeGreaterThan(0); + }); +}); \ No newline at end of file diff --git a/packages/bruno-app/src/components/Icons/IconSidebarToggle/index.js b/packages/bruno-app/src/components/Icons/IconSidebarToggle/index.js new file mode 100644 index 000000000..bad3221fb --- /dev/null +++ b/packages/bruno-app/src/components/Icons/IconSidebarToggle/index.js @@ -0,0 +1,28 @@ +import React from 'react'; + +const IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => { + return ( + + + + + {!collapsed && ( + + )} + + ); +}; + +export default IconSidebarToggle; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js index 0667a4146..951cb5f6e 100644 --- a/packages/bruno-app/src/components/Notifications/index.js +++ b/packages/bruno-app/src/components/Notifications/index.js @@ -79,7 +79,7 @@ const Notifications = () => { const modalCustomHeader = (
-
NOTIFICATIONS
+
NOTIFICATIONS
{unreadNotifications.length > 0 && ( <>
diff --git a/packages/bruno-app/src/components/Preferences/Beta/index.js b/packages/bruno-app/src/components/Preferences/Beta/index.js index ec0dd5c3a..0f8b8b807 100644 --- a/packages/bruno-app/src/components/Preferences/Beta/index.js +++ b/packages/bruno-app/src/components/Preferences/Beta/index.js @@ -84,7 +84,7 @@ const Beta = ({ close }) => {

Beta Features

- Enable beta features, these features may be unstable or incomplete. + Beta features are experimental previews that may change before full release. Try them and share feedback.

@@ -103,6 +103,16 @@ const Beta = ({ close }) => { + {feature.id === 'grpc' && ( + + Share feedback + + )}
{feature.description} diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/GrpcAuthMode/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/GrpcAuthMode/index.js index 3b95b708f..9819068b3 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/GrpcAuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/GrpcAuthMode/index.js @@ -30,6 +30,10 @@ const GrpcAuthMode = ({ item, collection }) => { name: 'OAuth2', mode: 'oauth2' }, + { + name: 'WSSE Auth', + mode: 'wsse' + }, { name: 'Inherit', mode: 'inherit' diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js index b30be89e5..9f9ea9070 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js @@ -6,6 +6,7 @@ import BearerAuth from '../../Auth/BearerAuth'; import BasicAuth from '../../Auth/BasicAuth'; import ApiKeyAuth from '../../Auth/ApiKeyAuth'; import OAuth2 from '../../Auth/OAuth2/index'; +import WsseAuth from '../../Auth/WsseAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections'; import { getTreePathFromCollectionToItem } from 'utils/collections/index'; @@ -13,7 +14,10 @@ import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/c import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; // List of auth modes supported by gRPC -const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit']; +// Note: Only header-based auth modes work with gRPC +// Complex auth modes like AWS Sig v4, Digest, and NTLM require axios interceptors +// and cannot be supported in gRPC requests as of now +const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'wsse', 'none', 'inherit']; const GrpcAuth = ({ item, collection }) => { const dispatch = useDispatch(); @@ -83,6 +87,9 @@ const GrpcAuth = ({ item, collection }) => { case 'oauth2': { return ; } + case 'wsse': { + return ; + } case 'inherit': { const source = getEffectiveAuthSource(); diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 0b1b9df9c..b5c2c69a7 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -147,7 +147,7 @@ const QueryParams = ({ item, collection }) => { diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/StyledWrapper.js index 10177c0f8..c649f740f 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/StyledWrapper.js @@ -9,7 +9,6 @@ const Wrapper = styled.div` .method-selector { border-radius: 3px; - min-width: 90px; .tippy-box { max-width: 150px !important; @@ -21,6 +20,28 @@ const Wrapper = styled.div` } } + input { + background-color: ${(props) => props.theme.requestTabPanel.url.bg}; + outline: none; + box-shadow: none; + text-align: left; + + &:focus { + outline: none !important; + box-shadow: none !important; + } + } + + .method-span { + width: 70px; + min-width: 70px; + max-width: 90px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + } + .caret { color: rgb(140, 140, 140); fill: rgb(140 140 140); diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js index c2bdf15f1..67e9a42dd 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js @@ -1,52 +1,142 @@ -import React, { useRef, forwardRef } from 'react'; +import React, { useState, useRef, forwardRef } from 'react'; import { IconCaretDown } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const HttpMethodSelector = ({ method, onMethodSelect }) => { - const dropdownTippyRef = useRef(); - const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); +const STANDARD_METHODS = Object.freeze(['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD','TRACE','CONNECT']); - const Icon = forwardRef((props, ref) => { +const KEY = Object.freeze({ ENTER: 'Enter', ESCAPE: 'Escape' }); + +const DEFAULT_METHOD = 'GET'; + +function Verb({ verb, onSelect }) { + return ( +
onSelect(verb)}> + {verb} +
+ ); +} + +const Icon = forwardRef(function IconComponent( + { isCustomMode, inputValue, handleInputChange, handleBlur, handleKeyDown, inputRef }, + ref +) { + if (isCustomMode) { return ( -
-
- {method} -
-
- -
+
+
); - }); + } - const handleMethodSelect = (verb) => onMethodSelect(verb); - - const Verb = ({ verb }) => { - return ( -
{ - dropdownTippyRef.current.hide(); - handleMethodSelect(verb); - }} + return ( +
+
- ); + + {inputValue} + + + +
+ ); +}); + +const HttpMethodSelector = ({ method = DEFAULT_METHOD, onMethodSelect }) => { + const [isCustomMode, setIsCustomMode] = useState(false); + const dropdownTippyRef = useRef(); + const inputRef = useRef(); + + const blurInput = () => inputRef.current?.blur(); + + const handleInputChange = (e) => { + const val = e.target.value.toUpperCase(); + onMethodSelect(val); }; + const handleDropdownSelect = (verb) => { + onMethodSelect(verb); + setIsCustomMode(false); + dropdownTippyRef.current?.hide(); + blurInput(); + }; + + const handleBlur = () => { + setIsCustomMode(false); + }; + + const handleAddCustomMethod = () => { + setIsCustomMode(true); + onMethodSelect(''); + dropdownTippyRef.current?.hide(); + + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, 0); + }; + + const handleKeyDown = (e) => { + switch (e.key) { + case KEY.ESCAPE: + setIsCustomMode(false); + blurInput(); + e.preventDefault(); + e.stopPropagation(); + return; + case KEY.ENTER: + onMethodSelect(e.target.value ? e.target.value.toUpperCase() : DEFAULT_METHOD); + setIsCustomMode(false); + blurInput(); + return; + default: + return; + } + }; + + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + return ( -
- } placement="bottom-start"> - - - - - - - +
+ + } + placement="bottom-start" + > +
+ {STANDARD_METHODS.map((verb) => ( + + ))} +
+ + Add Custom +
+
diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 3fe47b041..2eb6dc375 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -80,7 +80,7 @@ const QueryUrl = ({ item, collection, handleRun }) => { return ( -
+
{isGrpc ? (
gRPC diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index d50d528b3..fcba790a6 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -18,6 +18,7 @@ const RequestTabs = () => { const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const collections = useSelector((state) => state.collections.collections); const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); + const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const screenWidth = useSelector((state) => state.app.screenWidth); const getTabClassname = (tab, index) => { @@ -49,7 +50,8 @@ const RequestTabs = () => { const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid); const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid); - const maxTablistWidth = screenWidth - leftSidebarWidth - 150; + const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth; + const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150; const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon const showChevrons = maxTablistWidth < tabsWidth; diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 3071f2989..7956eb1ad 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -116,7 +116,7 @@ export default function RunnerResults({ collection }) { displayName: getDisplayName(collection.pathname, info.pathname, info.name), tags: [...(info.request?.tags || [])].sort(), }; - if (newItem.status !== 'error' && newItem.status !== 'skipped') { + if (newItem.status !== 'error' && newItem.status !== 'skipped' && newItem.status !== 'running') { newItem.testStatus = getTestStatus(newItem.testResults); newItem.assertionStatus = getTestStatus(newItem.assertionResults); newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults); diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 7b331c42c..3eb6707e0 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -13,6 +13,7 @@ import { IconArrowBackUp, IconEdit } from '@tabler/icons'; import Help from 'components/Help'; import { multiLineMsg } from "utils/common"; import { formatIpcError } from "utils/common/error"; +import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; const CreateCollection = ({ onClose }) => { const inputRef = useRef(); @@ -45,6 +46,7 @@ const CreateCollection = ({ onClose }) => { dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation)) .then(() => { toast.success('Collection created!'); + dispatch(toggleSidebarCollapse()); onClose(); }) .catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e)))); diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 20f1f5ee4..ef4d60db4 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -501,7 +501,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
{!['grpc-request', 'ws-request'].includes(formik.values.requestType) ? ( -
+
formik.setFieldValue('requestMethod', val)} diff --git a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js index e64c7a3f4..2cec70df5 100644 --- a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js @@ -5,6 +5,7 @@ const Wrapper = styled.div` aside { background-color: ${(props) => props.theme.sidebar.bg}; + overflow: hidden; .collection-title { line-height: 1.5; @@ -41,7 +42,7 @@ const Wrapper = styled.div` } } - div.drag-sidebar { + div.sidebar-drag-handle { display: flex; align-items: center; justify-content: center; @@ -50,6 +51,7 @@ const Wrapper = styled.div` background-color: transparent; width: 6px; right: -3px; + transition: opacity 0.2s ease; &:hover div.drag-request-border { width: 2px; diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 1ba71b1ab..bd8b319c3 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -1,36 +1,37 @@ import TitleBar from './TitleBar'; import Collections from './Collections'; import StyledWrapper from './StyledWrapper'; -import { useApp } from 'providers/App'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app'; -import { useTheme } from 'providers/Theme'; const MIN_LEFT_SIDEBAR_WIDTH = 221; const MAX_LEFT_SIDEBAR_WIDTH = 600; const Sidebar = () => { const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); - const { version } = useApp(); + const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); - - const { storedTheme } = useTheme(); + const lastWidthRef = useRef(leftSidebarWidth); const dispatch = useDispatch(); const [dragging, setDragging] = useState(false); + const currentWidth = sidebarCollapsed ? 0 : asideWidth; + + // Clamp helper keeps width in allowed range + const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + const handleMouseMove = (e) => { - if (dragging) { - e.preventDefault(); - let width = e.clientX + 2; - if (width < MIN_LEFT_SIDEBAR_WIDTH || width > MAX_LEFT_SIDEBAR_WIDTH) { - return; - } - setAsideWidth(width); - } + if (!dragging || sidebarCollapsed) return; + e.preventDefault(); + const nextWidth = clamp(e.clientX + 2, MIN_LEFT_SIDEBAR_WIDTH, MAX_LEFT_SIDEBAR_WIDTH); + if (Math.abs(nextWidth - lastWidthRef.current) < 3) return; + lastWidthRef.current = nextWidth; + setAsideWidth(nextWidth); }; + const handleMouseUp = (e) => { if (dragging) { e.preventDefault(); @@ -49,6 +50,9 @@ const Sidebar = () => { }; const handleDragbarMouseDown = (e) => { e.preventDefault(); + if (sidebarCollapsed) { + return; + } setDragging(true); dispatch( updateIsDragging({ @@ -73,7 +77,7 @@ const Sidebar = () => { return ( -