diff --git a/dashboard/src/components/Dialog/CustomDialog.tsx b/dashboard/src/components/Dialog/CustomDialog.tsx index f7d984017..3f2199204 100644 --- a/dashboard/src/components/Dialog/CustomDialog.tsx +++ b/dashboard/src/components/Dialog/CustomDialog.tsx @@ -36,8 +36,12 @@ export type ActionLink = ActionLinkWithIntl | ActionLinkWithLabel; export interface CustomDialogProps { trigger: ReactNode; - titleIntlId: MessagesKey; - descriptionIntlId: MessagesKey; + titleIntlId?: MessagesKey; + descriptionIntlId?: MessagesKey; + content?: ReactNode; + contentClassName?: string; + showOverlay?: boolean; + showCloseButton?: boolean; footerClassName?: string; actionLinks?: ActionLink[]; showCancel?: boolean; @@ -47,47 +51,67 @@ export const CustomDialog = ({ trigger, titleIntlId, descriptionIntlId, + content, + contentClassName, + showOverlay = true, + showCloseButton = true, footerClassName, actionLinks = [], showCancel = true, }: CustomDialogProps): JSX.Element => { + const hasHeader = Boolean(titleIntlId || descriptionIntlId); + const hasFooter = actionLinks.length > 0 || showCancel; + return ( {trigger} - - - - - - - - - - - {actionLinks.map((actionLink, index) => ( - - ))} - {showCancel && ( - - - - )} - + ))} + {showCancel && ( + + + + )} + + )} ); diff --git a/dashboard/src/components/SearchBoxNavigate/SearchBoxNavigate.tsx b/dashboard/src/components/SearchBoxNavigate/SearchBoxNavigate.tsx new file mode 100644 index 000000000..97ca8ed31 --- /dev/null +++ b/dashboard/src/components/SearchBoxNavigate/SearchBoxNavigate.tsx @@ -0,0 +1,143 @@ +import { useMatches, useNavigate, useSearch } from '@tanstack/react-router'; +import type { ChangeEvent, JSX } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { HiSearch } from 'react-icons/hi'; + +import DebounceInput from '@/components/DebounceInput/DebounceInput'; +import { CustomDialog } from '@/components/Dialog/CustomDialog'; + +// Relates the type of listing to the corresponding search key +const forwardFields: Record = { + tree: 'treeSearch', + hardware: 'hardwareSearch', + issue: 'issueSearch', +}; + +interface ISearchData { + currentSearch?: string; + searchPlaceholder: string; + navigateTarget: string; +} + +export const SearchBoxNavigate = (): JSX.Element => { + const matches = useMatches(); + const routeInfo = useMemo(() => { + const lastMatch = matches[matches.length - 1]; + const cleanFullPath = lastMatch?.fullPath.replace(/\//g, '') ?? ''; + + if (['tree', 'treev1', 'treev2'].includes(cleanFullPath)) { + return 'tree'; + } + + if (['hardware', 'hardwarev1'].includes(cleanFullPath)) { + return 'hardware'; + } + + if (cleanFullPath === 'issues') { + return 'issue'; + } + + return 'unknown'; + }, [matches]); + const { formatMessage } = useIntl(); + + const { treeSearch, hardwareSearch, issueSearch } = useSearch({ + strict: false, + }); + const searchData = useMemo((): ISearchData => { + switch (routeInfo) { + case 'tree': + return { + currentSearch: treeSearch, + searchPlaceholder: formatMessage({ id: 'tree.searchPlaceholder' }), + navigateTarget: 'treeSearch', + }; + case 'hardware': + return { + currentSearch: hardwareSearch, + searchPlaceholder: formatMessage({ + id: 'hardware.searchPlaceholder', + }), + navigateTarget: 'hardwareSearch', + }; + case 'issue': + return { + currentSearch: issueSearch, + searchPlaceholder: formatMessage({ + id: 'issue.searchPlaceholder', + }), + navigateTarget: 'issueSearch', + }; + default: + return { + currentSearch: '', + searchPlaceholder: '', + navigateTarget: '', + }; + } + }, [routeInfo, treeSearch, formatMessage, hardwareSearch, issueSearch]); + + const navigate = useNavigate(); + + const onInputSearchTextChange = useCallback( + (e: ChangeEvent) => { + const value = e.target.value; + // using routeInfo as a dependency instead of searchData so that we don't depend on useSearch + const forwardSearch = { [forwardFields[routeInfo]]: value }; + + navigate({ + to: '.', + search: previousSearch => ({ + ...previousSearch, + ...forwardSearch, + }), + }); + }, + [navigate, routeInfo], + ); + + const sharedInput = useMemo(() => { + return ( + + ); + }, [ + onInputSearchTextChange, + routeInfo, + searchData.currentSearch, + searchData.searchPlaceholder, + ]); + + if (routeInfo === 'unknown') { + console.error('SearchBoxNavigate shown on an invalid route.'); + console.error('Route is ', matches); + return <>; + } + + return ( +
+ {/* Mobile: icon button */} + + + + } + content={sharedInput} + contentClassName="w-9/10 top-10 border-0 bg-transparent p-0 shadow-none min-[475px]:hidden" + showCloseButton={false} + showCancel={false} + /> + + {/* Desktop: inline input */} +
{sharedInput}
+
+ ); +}; diff --git a/dashboard/src/components/SearchBoxNavigate/index.tsx b/dashboard/src/components/SearchBoxNavigate/index.tsx new file mode 100644 index 000000000..a77c4b1ba --- /dev/null +++ b/dashboard/src/components/SearchBoxNavigate/index.tsx @@ -0,0 +1,3 @@ +import { SearchBoxNavigate } from './SearchBoxNavigate'; + +export { SearchBoxNavigate }; diff --git a/dashboard/src/components/TopBar/TopBar.tsx b/dashboard/src/components/TopBar/TopBar.tsx index 36c6940d8..b8b6dc8b6 100644 --- a/dashboard/src/components/TopBar/TopBar.tsx +++ b/dashboard/src/components/TopBar/TopBar.tsx @@ -17,6 +17,8 @@ import { useOrigins } from '@/api/origin'; import { Button } from '@/components/ui/button'; import MobileSideMenu from '@/components/SideMenu/MobileSideMenu'; +import { SearchBoxNavigate } from '@/components/SearchBoxNavigate'; + const OriginSelect = ({ isHardwarePath, }: { @@ -75,7 +77,7 @@ const OriginSelect = ({ return (
- +