import React, { useRef, useState, useEffect } from 'react'; import { AppDispatch, useAppDispatch, useGraphQueryResult, useQuerybuilderGraph, useRecentSearches, useSchemaGraph, useSearchResult, } from '../../data-access'; import { QueryMultiGraph } from '../../querybuilder'; import { CATEGORY_KEYS, addRecentSearch, addSearchResultData, addSearchResultQueryBuilder, addSearchResultSchema, } from '../../data-access/store/searchResultSlice'; import { filterData } from './similarity'; const SIMILARITY_THRESHOLD = 0.7; const CATEGORY_ACTIONS: { [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void; } = { data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { dispatch(addSearchResultData(payload)); }, schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { dispatch(addSearchResultSchema(payload)); }, querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { dispatch(addSearchResultQueryBuilder(payload)); }, }; const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[]; export default function Searchbar() { const inputRef = useRef<HTMLInputElement>(null); const [search, setSearch] = useState<string>(''); const searchbarRef = useRef<HTMLDivElement>(null); const dispatch = useAppDispatch(); const results = useSearchResult(); const recentSearches = useRecentSearches(); const schema = useSchemaGraph(); const graphData = useGraphQueryResult(); const querybuilderData = useQuerybuilderGraph(); const dataSources: { [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; } = { data: graphData, schema: schema, querybuilder: querybuilderData as QueryMultiGraph, }; useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { if (event.key === 'Enter') { if (search !== '') { dispatch(addRecentSearch(search)); } } }; window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [search]); useEffect(() => { handleSearch(); }, [search]); const handleSearch = () => { let query = search.toLowerCase(); const categories = search.match(/@[^ ]+/g); if (categories) { categories.map((category) => { query = query.replace(category, '').trim(); const cat = category.substring(1); if (cat in CATEGORY_ACTIONS) { const categoryAction = CATEGORY_ACTIONS[cat as CATEGORY_KEYS]; const data = dataSources[cat]; const payload = { nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), }; categoryAction(payload, dispatch); } }); } else { for (const category of SEARCH_CATEGORIES) { const categoryAction = CATEGORY_ACTIONS[category]; const data = dataSources[category]; const payload = { nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), }; categoryAction(payload, dispatch); } } }; return ( <div className="flex flex-col w-full p-2"> <div className="w-full"> <input type="text" ref={inputRef} value={search} onChange={(e) => setSearch(e.target.value)} id="input-group-search" className="block w-full p-2 ps-2 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" placeholder="Search database" ></input> </div> <div> {recentSearches.length !== 0 && ( <div className="px-3 pb-3"> <p className="text-sm">Recent searches</p> {recentSearches.slice(0, 3).map((term) => ( <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> {term} </p> ))} </div> )} {search !== '' && ( <div className="z-10 w-full overflow-y-auto scroll h-full px-2 pb-2"> {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( <div className="ml-1 text-sm"> <p className="text-secondary-500">Found no matches...</p> </div> ) : ( SEARCH_CATEGORIES.map((category, index) => { if (results[category].nodes.length > 0 || results[category].edges.length > 0) { return ( <div key={index}> <div className="flex justify-between p-2 text-lg"> <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> </div> <div className="h-[1px] w-full bg-secondary-200"></div> {Object.values(Object.values(results[category])) .flat() .map((item, index) => ( <div key={index} className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" title={JSON.stringify(item)} onClick={() => { CATEGORY_ACTIONS[category]( { nodes: results[category].nodes.includes(item) ? [item] : [], edges: results[category].edges.includes(item) ? [item] : [], }, dispatch, ); }} > <div className="font-bold text-sm"> {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} </div> <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> </div> ))} </div> ); } else return <></>; }) )} </div> )} </div> </div> ); }