Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(command-bar): add search to command bar #17864

Merged
merged 59 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
2e07cfc
add feature flag and scaffold
thmsobrmlr Oct 9, 2023
71442c7
style container
thmsobrmlr Oct 9, 2023
d9281af
add search input
thmsobrmlr Oct 9, 2023
5445cf9
move search bar to separate component
thmsobrmlr Oct 9, 2023
8e6ed8b
hide command bar on escape
thmsobrmlr Oct 9, 2023
6bc8b84
Merge branch 'master' into command-bar
thmsobrmlr Oct 10, 2023
6c5d264
add search bar logic
thmsobrmlr Oct 10, 2023
2ea24fd
scaffold search endpoint
thmsobrmlr Oct 10, 2023
7bf7d67
return ranked dashboards
thmsobrmlr Oct 10, 2023
43248ec
basic multi-model search
thmsobrmlr Oct 10, 2023
daf0779
move rank order out
thmsobrmlr Oct 10, 2023
95671e3
filter by rank and take q input
thmsobrmlr Oct 10, 2023
27bc138
dummy search
thmsobrmlr Oct 10, 2023
616920a
implement tabs and adjust styles
thmsobrmlr Oct 10, 2023
e6229e1
styling changes
thmsobrmlr Oct 10, 2023
65a68d8
naive prefix search
thmsobrmlr Oct 10, 2023
7935f16
handle empty query
thmsobrmlr Oct 10, 2023
e24f114
refactor search endpoint
thmsobrmlr Oct 11, 2023
93c6f21
re-add counts
thmsobrmlr Oct 11, 2023
fc42e47
add other postgres based entities
thmsobrmlr Oct 11, 2023
51dd9a1
handle other entities frontend side
thmsobrmlr Oct 11, 2023
0adaf51
fix typo
thmsobrmlr Oct 11, 2023
f6ca7ec
refactor frontend
thmsobrmlr Oct 11, 2023
c568f54
more refactoring
thmsobrmlr Oct 11, 2023
1a5dd7a
use plural for tabs
thmsobrmlr Oct 11, 2023
e43c55a
fix kea issue by re-ordering
thmsobrmlr Oct 11, 2023
9e89e51
keyboard select
thmsobrmlr Oct 11, 2023
d30485a
implement navigation
thmsobrmlr Oct 11, 2023
8fe359b
add tests
thmsobrmlr Oct 12, 2023
1a6a1e0
work on keyboard navigation
thmsobrmlr Oct 12, 2023
9e37dbf
more reliable opening of results
thmsobrmlr Oct 12, 2023
cd9a302
Merge branch 'master' into command-bar
thmsobrmlr Oct 31, 2023
c46bdd8
search on mount
thmsobrmlr Oct 31, 2023
dcffc23
make tabs work
thmsobrmlr Oct 31, 2023
e7ae614
add empty state
thmsobrmlr Oct 31, 2023
9e9edf8
add loading state
thmsobrmlr Oct 31, 2023
44bec6a
replace with lemon modal
thmsobrmlr Oct 31, 2023
bcd7718
shadow
thmsobrmlr Oct 31, 2023
8c25873
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 31, 2023
c26d8c8
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 31, 2023
a356130
improve scroll behaviour
thmsobrmlr Oct 31, 2023
364435b
keyboard select fixes
thmsobrmlr Oct 31, 2023
8c38182
fix lint issues
thmsobrmlr Oct 31, 2023
23b9434
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 31, 2023
d154332
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 31, 2023
6bfae8e
fix search with all invalid charaters
thmsobrmlr Nov 2, 2023
64d78d8
fix types
thmsobrmlr Nov 2, 2023
56f2d97
fix mypy
thmsobrmlr Nov 2, 2023
71be2cc
Update frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.tsx
thmsobrmlr Nov 8, 2023
565bd8e
bem class names
thmsobrmlr Nov 8, 2023
45e763e
add debounce
thmsobrmlr Nov 8, 2023
7c3b88c
Update frontend/src/lib/components/CommandBar/CommandBar.tsx
thmsobrmlr Nov 8, 2023
5537b38
remove leftover shadow style
thmsobrmlr Nov 8, 2023
ed0d69b
use css for conditional styling
thmsobrmlr Nov 8, 2023
31066e5
rename to isAutoScrolling
thmsobrmlr Nov 8, 2023
46ccdbf
format
thmsobrmlr Nov 8, 2023
eb443aa
Merge branch 'master' into command-bar
thmsobrmlr Nov 8, 2023
7657de5
feat(command-bar): add commands from existing palette (#18355)
thmsobrmlr Nov 14, 2023
a006476
Merge branch 'master' into command-bar
thmsobrmlr Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions frontend/src/lib/components/CommandBar/CommandBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useRef, forwardRef } from 'react'
import { useActions, useValues } from 'kea'

import { useEventListener } from 'lib/hooks/useEventListener'
import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler'

import { commandBarLogic } from './commandBarLogic'
import { BarStatus } from './types'

import './index.scss'
import SearchBar from './SearchBar'
import { LemonModal } from '@posthog/lemon-ui'

const CommandBarContainer = forwardRef<HTMLDivElement, { children?: React.ReactNode }>(function CommandBarContainer(
{ children },
ref
): JSX.Element {
return (
<div className="w-full h-160 max-w-lg bg-bg-3000 rounded overflow-hidden flex flex-col" ref={ref}>
{children}
</div>
)
})

function CommandBar(): JSX.Element | null {
const containerRef = useRef<HTMLDivElement | null>(null)
const { barStatus } = useValues(commandBarLogic)
const { toggleSearchBar, toggleActionsBar, hideCommandBar } = useActions(commandBarLogic)

useEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
if (event.shiftKey) {
toggleActionsBar()
} else {
toggleSearchBar()
}
} else if (event.key === 'Escape') {
hideCommandBar()
}
})

useOutsideClickHandler(containerRef, hideCommandBar, [])

return (
<LemonModal isOpen={barStatus !== BarStatus.HIDDEN} simple closable={false} width={800}>
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
<CommandBarContainer ref={containerRef}>
<SearchBar />
</CommandBarContainer>
</LemonModal>
)
}

export default CommandBar
21 changes: 21 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMountedLogic } from 'kea'

import { searchBarLogic } from './searchBarLogic'

import SearchInput from './SearchInput'
import SearchResults from './SearchResults'
import SearchTabs from './SearchTabs'

const SearchBar = (): JSX.Element => {
useMountedLogic(searchBarLogic)
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className="flex flex-col h-full">
<SearchInput />
<SearchResults />
<SearchTabs />
</div>
)
}

export default SearchBar
23 changes: 23 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchBarTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useActions } from 'kea'

import { resultTypeToName } from './constants'
import { searchBarLogic } from './searchBarLogic'
import { ResultTypeWithAll } from './types'

type SearchBarTabProps = {
type: ResultTypeWithAll
active: boolean
count?: number | null
}

const SearchBarTab = ({ type, active, count }: SearchBarTabProps): JSX.Element => {
const { setActiveTab } = useActions(searchBarLogic)
return (
<div className={`px-3 py-2 cursor-pointer text-xs ${active && 'font-bold'}`} onClick={() => setActiveTab(type)}>
{resultTypeToName[type]}
{count != null && <span className="ml-1 text-xxs text-muted-3000">{count}</span>}
</div>
)
}

export default SearchBarTab
28 changes: 28 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useActions, useValues } from 'kea'

import { LemonInput } from '@posthog/lemon-ui'
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'

import { searchBarLogic } from './searchBarLogic'

const SearchInput = (): JSX.Element => {
const { searchQuery } = useValues(searchBarLogic)
const { setSearchQuery } = useActions(searchBarLogic)

return (
<div className="border-b">
<LemonInput
type="search"
size="small"
className="CommandBar__search-input"
fullWidth
suffix={<KeyboardShortcut escape muted />}
autoFocus
value={searchQuery}
onChange={setSearchQuery}
/>
</div>
)
}

export default SearchInput
86 changes: 86 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useLayoutEffect, useRef } from 'react'
import { useActions, useValues } from 'kea'

import { resultTypeToName } from './constants'
import { searchBarLogic, urlForResult } from './searchBarLogic'
import { SearchResult as SearchResultType } from './types'
import { LemonSkeleton } from '@posthog/lemon-ui'

type SearchResultProps = {
result: SearchResultType
resultIndex: number
focused: boolean
keyboardFocused: boolean
}

const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchResultProps): JSX.Element => {
const { isAutoScrolling } = useValues(searchBarLogic)
const { onMouseEnterResult, onMouseLeaveResult, openResult, setIsAutoScrolling } = useActions(searchBarLogic)

const ref = useRef<HTMLDivElement | null>(null)

useLayoutEffect(() => {
if (keyboardFocused) {
// :HACKY: This uses the non-standard scrollIntoViewIfNeeded api
// to improve scroll behaviour. Change to scrollIntoView({ scrollMode: 'if-needed' })
// once available.
if ((ref.current as any)?.scrollIntoViewIfNeeded) {
;(ref.current as any).scrollIntoViewIfNeeded(false)
} else {
ref.current?.scrollIntoView()
}

// set scrolling state to prevent mouse enter/leave events during
// keyboard navigation
setIsAutoScrolling(true)
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this trigger setTimeout multiple times? We should store the value and override / cancel it if triggered again

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I'm following here. Do you mean a change of keyboardFocused from false -> true -> false -> true would trigger this twice?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was what I meant originally. Being honest I didn't consider how infrequently keyboardFocused probably changes so in practice you're probably not going to rack up a large number of concurrent setTimeouts

setIsAutoScrolling(false)
}, 50)
}
}, [keyboardFocused])

return (
<div
className={`w-full pl-3 pr-2 ${
focused ? 'bg-secondary-3000-hover' : 'bg-secondary-3000'
} border-b cursor-pointer`}
onMouseEnter={() => {
if (isAutoScrolling) {
return
}
onMouseEnterResult(resultIndex)
}}
onMouseLeave={() => {
if (isAutoScrolling) {
return
}
onMouseLeaveResult()
}}
onClick={() => {
openResult(resultIndex)
}}
ref={ref}
>
<div className="px-2 py-3 w-full space-y-0.5 flex flex-col items-start">
<span className="text-muted-3000 text-xs">{resultTypeToName[result.type]}</span>
<span className="text-text-3000">{result.name}</span>
<span className="text-trace-3000 text-xs">
{location.host}
<span className="text-muted-3000">{urlForResult(result)}</span>
</span>
</div>
</div>
)
}

export const SearchResultSkeleton = (): JSX.Element => (
<div className="w-full pl-3 pr-2 bg-secondary-3000 border-b">
<div className="px-2 py-3 w-full space-y-0.5 flex flex-col items-start">
<LemonSkeleton className="w-32 opacity-75" height={3} />
<LemonSkeleton className="w-80" />
<LemonSkeleton className="w-100 opacity-75" height={3} />
</div>
</div>
)

export default SearchResult
60 changes: 60 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useActions, useValues } from 'kea'
import { useEventListener } from 'lib/hooks/useEventListener'
import { DetectiveHog } from '../hedgehogs'

import { searchBarLogic } from './searchBarLogic'
import SearchResult, { SearchResultSkeleton } from './SearchResult'

const SearchResults = (): JSX.Element => {
const { filterSearchResults, searchResponseLoading, activeResultIndex, keyboardResultIndex, maxIndex } =
useValues(searchBarLogic)
const { onArrowUp, onArrowDown, openResult } = useActions(searchBarLogic)

useEventListener('keydown', (event) => {
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
if (!filterSearchResults) {
return
}

if (event.key === 'Enter') {
event.preventDefault()
openResult(activeResultIndex)
} else if (event.key === 'ArrowDown') {
event.preventDefault()
onArrowDown(activeResultIndex, maxIndex)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
onArrowUp(activeResultIndex, maxIndex)
}
})

return (
<div className="grow overscroll-none overflow-y-auto">
{searchResponseLoading && (
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
<>
<SearchResultSkeleton />
<SearchResultSkeleton />
<SearchResultSkeleton />
</>
)}
{!searchResponseLoading && filterSearchResults?.length === 0 && (
<div className="h-full flex flex-col items-center justify-center p-3">
<h3 className="mb-0 text-xl">No results</h3>
<p className="opacity-75 mb-0">This doesn't happen often, but we're stumped!</p>
<DetectiveHog height={150} width={150} />
</div>
)}
{!searchResponseLoading &&
filterSearchResults?.map((result, index) => (
<SearchResult
key={`${result.type}_${result.result_id}`}
result={result}
resultIndex={index}
focused={index === activeResultIndex}
keyboardFocused={index === keyboardResultIndex}
/>
))}
</div>
)
}

export default SearchResults
23 changes: 23 additions & 0 deletions frontend/src/lib/components/CommandBar/SearchTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useValues } from 'kea'

import { searchBarLogic } from './searchBarLogic'
import SearchBarTab from './SearchBarTab'
import { ResultType } from './types'

const SearchTabs = (): JSX.Element | null => {
const { searchResponse, activeTab } = useValues(searchBarLogic)

if (!searchResponse) {
return null
}

return (
<div className="flex items-center border-t space-x-3 px-2">
<SearchBarTab type="all" active={activeTab === 'all'} />
{Object.entries(searchResponse.counts).map(([type, count]) => (
<SearchBarTab key={type} type={type as ResultType} count={count} active={activeTab === type} />
))}
</div>
)
}
export default SearchTabs
25 changes: 25 additions & 0 deletions frontend/src/lib/components/CommandBar/commandBarLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { kea, path, actions, reducers } from 'kea'
import { BarStatus } from './types'

import type { commandBarLogicType } from './commandBarLogicType'

export const commandBarLogic = kea<commandBarLogicType>([
path(['lib', 'components', 'CommandBar', 'commandBarLogic']),
actions({
hideCommandBar: true,
toggleSearchBar: true,
toggleActionsBar: true,
}),
reducers({
barStatus: [
BarStatus.HIDDEN as BarStatus,
{
hideCommandBar: () => BarStatus.HIDDEN,
toggleSearchBar: (previousState) =>
previousState === BarStatus.HIDDEN ? BarStatus.SHOW_SEARCH : BarStatus.HIDDEN,
toggleActionsBar: (previousState) =>
previousState === BarStatus.HIDDEN ? BarStatus.SHOW_ACTIONS : BarStatus.HIDDEN,
},
],
}),
])
11 changes: 11 additions & 0 deletions frontend/src/lib/components/CommandBar/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ResultTypeWithAll } from './types'

export const resultTypeToName: Record<ResultTypeWithAll, string> = {
all: 'All',
action: 'Actions',
cohort: 'Cohorts',
dashboard: 'Dashboards',
experiment: 'Experiments',
feature_flag: 'Feature Flags',
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
insight: 'Insights',
}
4 changes: 4 additions & 0 deletions frontend/src/lib/components/CommandBar/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.CommandBar__search-input {
border-color: transparent !important;
border-radius: 0;
}
Loading
Loading