mirror of https://github.com/grafana/grafana
Navigation: Integrate search into topnav (#54925)
* behaviour mostly there * slight performance improvement * slightly nicer... * refactor search and add it to the store * add comments about removing old component * remove unneeded logic * small design tweak * More small tweaks * Restore top margin * add onCloseSearch/onSelectSearchItem to useSearchQuery Co-authored-by: Torkel Ödegaard <torkel@grafana.com>pull/54939/head^2
parent
ad19f018a9
commit
a861c10f1b
@ -0,0 +1,225 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { useDialog } from '@react-aria/dialog'; |
||||||
|
import { FocusScope } from '@react-aria/focus'; |
||||||
|
import { OverlayContainer, useOverlay } from '@react-aria/overlays'; |
||||||
|
import React, { useRef, useState } from 'react'; |
||||||
|
import CSSTransition from 'react-transition-group/CSSTransition'; |
||||||
|
import { useLocalStorage } from 'react-use'; |
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { config } from '@grafana/runtime'; |
||||||
|
import { IconButton, useStyles2 } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { SEARCH_PANELS_LOCAL_STORAGE_KEY } from '../constants'; |
||||||
|
import { useKeyNavigationListener } from '../hooks/useSearchKeyboardSelection'; |
||||||
|
import { useSearchQuery } from '../hooks/useSearchQuery'; |
||||||
|
import { SearchView } from '../page/components/SearchView'; |
||||||
|
|
||||||
|
const ANIMATION_DURATION = 200; |
||||||
|
|
||||||
|
export interface Props { |
||||||
|
isOpen: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export function DashboardSearchModal({ isOpen }: Props) { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const animStyles = useStyles2((theme) => getAnimStyles(theme, ANIMATION_DURATION)); |
||||||
|
const { query, onQueryChange, onCloseSearch } = useSearchQuery({}); |
||||||
|
const ref = useRef<HTMLDivElement>(null); |
||||||
|
const [animationComplete, setAnimationComplete] = useState(false); |
||||||
|
|
||||||
|
const { overlayProps, underlayProps } = useOverlay({ isOpen, onClose: onCloseSearch }, ref); |
||||||
|
|
||||||
|
const { dialogProps } = useDialog({}, ref); |
||||||
|
|
||||||
|
let [includePanels, setIncludePanels] = useLocalStorage<boolean>(SEARCH_PANELS_LOCAL_STORAGE_KEY, true); |
||||||
|
if (!config.featureToggles.panelTitleSearch) { |
||||||
|
includePanels = false; |
||||||
|
} |
||||||
|
|
||||||
|
const onSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
onQueryChange(e.currentTarget.value); |
||||||
|
}; |
||||||
|
|
||||||
|
const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<OverlayContainer> |
||||||
|
<CSSTransition appear in timeout={ANIMATION_DURATION} classNames={animStyles.underlay}> |
||||||
|
<div onClick={onCloseSearch} className={styles.underlay} {...underlayProps} /> |
||||||
|
</CSSTransition> |
||||||
|
<CSSTransition |
||||||
|
onEntered={() => setAnimationComplete(true)} |
||||||
|
appear |
||||||
|
in |
||||||
|
timeout={ANIMATION_DURATION} |
||||||
|
classNames={animStyles.overlay} |
||||||
|
> |
||||||
|
<div ref={ref} className={styles.overlay} {...overlayProps} {...dialogProps}> |
||||||
|
<FocusScope contain autoFocus> |
||||||
|
<div className={styles.searchField}> |
||||||
|
<div> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
placeholder={includePanels ? 'Search dashboards and panels by name' : 'Search dashboards by name'} |
||||||
|
value={query.query ?? ''} |
||||||
|
onChange={onSearchQueryChange} |
||||||
|
onKeyDown={onKeyDown} |
||||||
|
tabIndex={0} |
||||||
|
spellCheck={false} |
||||||
|
className={styles.input} |
||||||
|
autoFocus |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className={styles.closeBtn}> |
||||||
|
<IconButton name="times" onClick={onCloseSearch} size="xl" tooltip="Close search" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{animationComplete && ( |
||||||
|
<div className={styles.search}> |
||||||
|
<SearchView |
||||||
|
showManage={false} |
||||||
|
includePanels={includePanels!} |
||||||
|
setIncludePanels={setIncludePanels} |
||||||
|
keyboardEvents={keyboardEvents} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</FocusScope> |
||||||
|
</div> |
||||||
|
</CSSTransition> |
||||||
|
</OverlayContainer> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => { |
||||||
|
const commonTransition = { |
||||||
|
transitionDuration: `${animationDuration}ms`, |
||||||
|
transitionTimingFunction: theme.transitions.easing.easeInOut, |
||||||
|
}; |
||||||
|
|
||||||
|
const underlayTransition = { |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
...commonTransition, |
||||||
|
transitionProperty: 'opacity', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const underlayClosed = { |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
opacity: 0, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const underlayOpen = { |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
opacity: 1, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const overlayTransition = { |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
...commonTransition, |
||||||
|
transitionProperty: 'height, width', |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const overlayClosed = { |
||||||
|
height: '100%', |
||||||
|
width: '100%', |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
height: '32px', |
||||||
|
width: '50%', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const overlayOpen = { |
||||||
|
height: '100%', |
||||||
|
width: '100%', |
||||||
|
[theme.breakpoints.up('md')]: { |
||||||
|
height: '90%', |
||||||
|
width: '75%', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
return { |
||||||
|
overlay: { |
||||||
|
appear: css(overlayClosed), |
||||||
|
appearActive: css(overlayTransition, overlayOpen), |
||||||
|
appearDone: css(overlayOpen), |
||||||
|
}, |
||||||
|
underlay: { |
||||||
|
appear: css(underlayClosed), |
||||||
|
appearActive: css(underlayTransition, underlayOpen), |
||||||
|
appearDone: css(underlayOpen), |
||||||
|
}, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => { |
||||||
|
return { |
||||||
|
underlay: css` |
||||||
|
background-color: ${theme.components.overlay.background}; |
||||||
|
backdrop-filter: blur(1px); |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
padding: 0; |
||||||
|
position: fixed; |
||||||
|
right: 0; |
||||||
|
top: 0; |
||||||
|
z-index: ${theme.zIndex.modalBackdrop}; |
||||||
|
`,
|
||||||
|
overlay: css` |
||||||
|
background: ${theme.colors.background.primary}; |
||||||
|
border: 1px solid ${theme.components.panel.borderColor}; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
margin: 0 auto; |
||||||
|
padding: ${theme.spacing(1)}; |
||||||
|
position: fixed; |
||||||
|
height: 100%; |
||||||
|
z-index: ${theme.zIndex.modal}; |
||||||
|
|
||||||
|
${theme.breakpoints.up('md')} { |
||||||
|
border-radius: ${theme.shape.borderRadius(2)}; |
||||||
|
box-shadow: ${theme.shadows.z3}; |
||||||
|
left: 0; |
||||||
|
margin: ${theme.spacing(0.5, 'auto', 0)}; |
||||||
|
padding: ${theme.spacing(1)}; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
`,
|
||||||
|
closeBtn: css` |
||||||
|
right: -5px; |
||||||
|
top: 0px; |
||||||
|
z-index: 1; |
||||||
|
position: absolute; |
||||||
|
`,
|
||||||
|
searchField: css` |
||||||
|
position: relative; |
||||||
|
`,
|
||||||
|
search: css` |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
overflow: hidden; |
||||||
|
height: 100%; |
||||||
|
padding: ${theme.spacing(2, 0, 3, 0)}; |
||||||
|
`,
|
||||||
|
input: css` |
||||||
|
box-sizing: border-box; |
||||||
|
outline: none; |
||||||
|
background-color: transparent; |
||||||
|
background: transparent; |
||||||
|
border-bottom: 1px solid ${theme.colors.border.medium}; |
||||||
|
font-size: 16px; |
||||||
|
line-height: 30px; |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
&::placeholder { |
||||||
|
color: ${theme.colors.text.disabled}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}; |
||||||
@ -1,22 +1,24 @@ |
|||||||
import React, { FC, memo } from 'react'; |
import React, { FC, memo } from 'react'; |
||||||
|
|
||||||
|
import { config } from '@grafana/runtime'; |
||||||
import { useUrlParams } from 'app/core/navigation/hooks'; |
import { useUrlParams } from 'app/core/navigation/hooks'; |
||||||
|
|
||||||
import { defaultQueryParams } from '../reducers/searchQueryReducer'; |
|
||||||
|
|
||||||
import { DashboardSearch } from './DashboardSearch'; |
import { DashboardSearch } from './DashboardSearch'; |
||||||
|
import { DashboardSearchModal } from './DashboardSearchModal'; |
||||||
|
|
||||||
export const SearchWrapper: FC = memo(() => { |
export const SearchWrapper: FC = memo(() => { |
||||||
const [params, updateUrlParams] = useUrlParams(); |
const [params] = useUrlParams(); |
||||||
const isOpen = params.get('search') === 'open'; |
const isOpen = params.get('search') === 'open'; |
||||||
|
const isTopnav = config.featureToggles.topnav; |
||||||
|
|
||||||
const closeSearch = () => { |
return isOpen ? ( |
||||||
if (isOpen) { |
isTopnav ? ( |
||||||
updateUrlParams({ search: null, folder: null, ...defaultQueryParams }); |
<DashboardSearchModal isOpen={isOpen} /> |
||||||
} |
) : ( |
||||||
}; |
// TODO: remove this component when we turn on the topnav feature toggle
|
||||||
|
<DashboardSearch /> |
||||||
return isOpen ? <DashboardSearch onCloseSearch={closeSearch} /> : null; |
) |
||||||
|
) : null; |
||||||
}); |
}); |
||||||
|
|
||||||
SearchWrapper.displayName = 'SearchWrapper'; |
SearchWrapper.displayName = 'SearchWrapper'; |
||||||
|
|||||||
@ -1,11 +0,0 @@ |
|||||||
// Search Query
|
|
||||||
export const TOGGLE_STARRED = 'TOGGLE_STARRED'; |
|
||||||
export const REMOVE_STARRED = 'REMOVE_STARRED'; |
|
||||||
export const QUERY_CHANGE = 'QUERY_CHANGE'; |
|
||||||
export const DATASOURCE_CHANGE = 'DATASOURCE_CHANGE'; |
|
||||||
export const REMOVE_TAG = 'REMOVE_TAG'; |
|
||||||
export const CLEAR_FILTERS = 'CLEAR_FILTERS'; |
|
||||||
export const SET_TAGS = 'SET_TAGS'; |
|
||||||
export const ADD_TAG = 'ADD_TAG'; |
|
||||||
export const TOGGLE_SORT = 'TOGGLE_SORT'; |
|
||||||
export const LAYOUT_CHANGE = 'LAYOUT_CHANGE'; |
|
||||||
Loading…
Reference in new issue