From 4123f9b344708e478027759bb4f3355ebab01423 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 17 Jan 2025 03:18:42 +0530 Subject: [PATCH] feat: collection search --- .../CollectionSearch/StyledWrapper.js | 6 + .../src/components/CollectionSearch/index.js | 200 ++++++++++++++++++ .../RequestTabs/CollectionToolBar/index.js | 29 ++- 3 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSearch/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSearch/index.js diff --git a/packages/bruno-app/src/components/CollectionSearch/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSearch/StyledWrapper.js new file mode 100644 index 000000000..195b0632f --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSearch/StyledWrapper.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/CollectionSearch/index.js b/packages/bruno-app/src/components/CollectionSearch/index.js new file mode 100644 index 000000000..ffc7fdc4a --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSearch/index.js @@ -0,0 +1,200 @@ +import React, { useState, useMemo, useRef, useEffect } from 'react'; +import Modal from 'components/Modal'; + +const searchCollection = (collection, term) => { + const results = []; + const search = (items, path = []) => { + items.forEach(item => { + const itemPath = [...path, item.name]; + + if (item.type === 'http-request') { + const matches = []; + + // Search in name + if (item.name.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'name', value: item.name }); + } + + // Search in URL + if (item.request?.url?.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'url', value: item.request.url }); + } + + // Search in headers + item.request?.headers?.forEach(header => { + if (header.name.toLowerCase().includes(term.toLowerCase()) || + header.value.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'header', value: `${header.name}: ${header.value}` }); + } + }); + + // Search in body + if (item.request?.body?.mode === 'json' && item.request.body.json) { + const bodyJson = JSON.stringify(item.request.body.json); + if (bodyJson.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'body', value: bodyJson }); + } + } else if (item.request?.body?.mode === 'text' && item.request.body.text) { + if (item.request.body.text.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'body', value: item.request.body.text }); + } + } + + // Search in script + if (item.request?.script?.req?.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'script', value: item.request.script.req }); + } + + // Search in assertions + item.request?.assertions?.forEach(assertion => { + if (assertion.name.toLowerCase().includes(term.toLowerCase()) || + assertion.value.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'assertion', value: `${assertion.name}: ${assertion.value}` }); + } + }); + + // Search in tests + if (item.request?.tests?.toLowerCase().includes(term.toLowerCase())) { + matches.push({ type: 'test', value: item.request.tests }); + } + + if (matches.length > 0) { + results.push({ item, path: itemPath, matches }); + } + } + + if (item.items) { + search(item.items, itemPath); + } + }); + }; + search(collection.items); + return results; +}; + +const HighlightedText = ({ text, highlight }) => { + const parts = text.split(new RegExp(`(${highlight})`, 'gi')); + return ( + + {parts.map((part, i) => + part.toLowerCase() === highlight.toLowerCase() ? + {part} : + part + )} + + ); +}; + +const CollectionSearch = ({ collection, onClose }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const resultsRef = useRef(null); + const inputRef = useRef(null); + + const searchResults = useMemo(() => { + return searchTerm ? searchCollection(collection, searchTerm) : []; + }, [collection, searchTerm]); + + useEffect(() => { + setSelectedIndex(searchResults.length > 0 ? 0 : -1); + }, [searchResults]); + + const handleKeyDown = (e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex(prevIndex => + prevIndex < searchResults.length - 1 ? prevIndex + 1 : prevIndex + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex(prevIndex => prevIndex > 0 ? prevIndex - 1 : 0); + } + }; + + useEffect(() => { + if (selectedIndex >= 0 && resultsRef.current) { + const selectedElement = resultsRef.current.children[selectedIndex]; + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + }, [selectedIndex]); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + +
+
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search HTTP requests..." + className="w-full px-4 py-2 text-gray-900 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none" + /> + + + +
+ {searchResults.length > 0 && ( +
+ {searchResults.map((result, index) => ( +
+
+ + {result.matches.some(m => m.type === 'name') && ( + (name match) + )} +
+
+ {result.path.slice(0, -1).join(' > ')} +
+ {result.matches.filter(match => match.type !== 'name').map((match, matchIndex) => ( +
+ + {match.type.charAt(0).toUpperCase() + match.type.slice(1)} + + + + +
+ ))} +
+ ))} +
+ )} +
+
+ ); +}; + +export default CollectionSearch; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js index 8ca76b15e..44c5b0508 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { uuid } from 'utils/common'; -import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons'; +import { IconFiles, IconRun, IconEye, IconSettings, IconSearch } from '@tabler/icons'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector'; import GlobalEnvironmentSelector from 'components/GlobalEnvironments/EnvironmentSelector'; import { addTab } from 'providers/ReduxStore/slices/tabs'; @@ -8,9 +8,23 @@ import { useDispatch } from 'react-redux'; import ToolHint from 'components/ToolHint'; import StyledWrapper from './StyledWrapper'; import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode'; +import CollectionSearch from 'components/CollectionSearch'; +import Mousetrap from 'mousetrap'; const CollectionToolBar = ({ collection }) => { const dispatch = useDispatch(); + const [searchModalOpen, setSearchModalOpen] = useState(false); + + useEffect(() => { + Mousetrap.bind(['command+k', 'ctrl+k'], (e) => { + e.preventDefault(); + handleSearch(); + }); + + return () => { + Mousetrap.unbind(['command+k', 'ctrl+k']); + }; + }, []); const handleRun = () => { dispatch( @@ -42,17 +56,26 @@ const CollectionToolBar = ({ collection }) => { ); }; + const handleSearch = () => setSearchModalOpen(true); + const handleCloseSearch = () => setSearchModalOpen(false); + return ( + {searchModalOpen && }
{collection?.name}
- + + + + + +