diff --git a/frontend/__snapshots__/components-command-bar--search--dark.png b/frontend/__snapshots__/components-command-bar--search--dark.png index 599e3c20f7aea..0692091cc3e32 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--dark.png and b/frontend/__snapshots__/components-command-bar--search--dark.png differ diff --git a/frontend/__snapshots__/components-command-bar--search--light.png b/frontend/__snapshots__/components-command-bar--search--light.png index 75bd57cddaff1..8231897e6233f 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--light.png and b/frontend/__snapshots__/components-command-bar--search--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input--search--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input--search--dark.png index 2618d982587b5..8dd6e08c842af 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input--search--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-input--search--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input--search--light.png b/frontend/__snapshots__/lemon-ui-lemon-input--search--light.png index 171b0fb4ae3bc..42f6a48d5f2a9 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input--search--light.png and b/frontend/__snapshots__/lemon-ui-lemon-input--search--light.png differ diff --git a/frontend/src/lib/components/CommandBar/CommandBar.tsx b/frontend/src/lib/components/CommandBar/CommandBar.tsx index fe4b9c2e4555e..5d25486df6862 100644 --- a/frontend/src/lib/components/CommandBar/CommandBar.tsx +++ b/frontend/src/lib/components/CommandBar/CommandBar.tsx @@ -26,7 +26,7 @@ const CommandBarOverlay = forwardRef(fun data-attr="command-bar" className={`w-full ${ barStatus === BarStatus.SHOW_SEARCH && 'h-full' - } bg-bg-3000 rounded overflow-hidden border border-border-bold`} + } w-full bg-bg-3000 rounded overflow-hidden border border-border-bold`} ref={ref} > {children} diff --git a/frontend/src/lib/components/CommandBar/SearchBar.tsx b/frontend/src/lib/components/CommandBar/SearchBar.tsx index 3eba8e50e2aad..d9b27c7e806db 100644 --- a/frontend/src/lib/components/CommandBar/SearchBar.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBar.tsx @@ -13,9 +13,10 @@ export const SearchBar = (): JSX.Element => { const inputRef = useRef(null) return ( -
+
-
+ {/* 49px = height of search input, 40rem = height of search results */} +
diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index 354470759518e..6bb04fce0693c 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -1,6 +1,8 @@ import { LemonSkeleton } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' +import { TAILWIND_BREAKPOINTS } from 'lib/constants' +import { useWindowSize } from 'lib/hooks/useWindowSize' import { capitalizeFirstLetter } from 'lib/utils' import { useLayoutEffect, useRef } from 'react' import { useSummarizeInsight } from 'scenes/insights/summarizeInsight' @@ -22,10 +24,12 @@ type SearchResultProps = { export const SearchResult = ({ result, resultIndex, focused }: SearchResultProps): JSX.Element => { const { aggregationLabel } = useValues(searchBarLogic) - const { openResult } = useActions(searchBarLogic) + const { setActiveResultIndex, openResult } = useActions(searchBarLogic) const ref = useRef(null) + const { width } = useWindowSize() + useLayoutEffect(() => { if (focused) { // :HACKY: This uses the non-standard scrollIntoViewIfNeeded api @@ -40,27 +44,33 @@ export const SearchResult = ({ result, resultIndex, focused }: SearchResultProps }, [focused]) return ( -
{ - openResult(resultIndex) - }} - ref={ref} - > -
- - {result.type !== 'group' - ? tabToName[result.type] - : `${capitalizeFirstLetter(aggregationLabel(result.extra_fields.group_type_index).plural)}`} - - - - + <> +
{ + if (width && width <= TAILWIND_BREAKPOINTS.md) { + openResult(resultIndex) + } else { + setActiveResultIndex(resultIndex) + } + }} + ref={ref} + > +
+ + {result.type !== 'group' + ? tabToName[result.type] + : `${capitalizeFirstLetter(aggregationLabel(result.extra_fields.group_type_index).plural)}`} + + + + +
-
+ ) } diff --git a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx index 498150ebada3a..af4e70df0a08a 100644 --- a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx @@ -1,11 +1,15 @@ -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { ResultDescription, ResultName } from 'lib/components/CommandBar/SearchResult' +import { LemonButton } from 'lib/lemon-ui/LemonButton' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { tabToName } from './constants' import { searchBarLogic, urlForResult } from './searchBarLogic' export const SearchResultPreview = (): JSX.Element | null => { const { activeResultIndex, combinedSearchResults } = useValues(searchBarLogic) + const { openResult } = useActions(searchBarLogic) if (!combinedSearchResults || combinedSearchResults.length === 0) { return null @@ -14,17 +18,41 @@ export const SearchResultPreview = (): JSX.Element | null => { const result = combinedSearchResults[activeResultIndex] return ( -
-
{tabToName[result.type]}
-
- -
- - {location.host} - {urlForResult(result)} - -
- +
+
+
+
{tabToName[result.type as keyof typeof tabToName]}
+
+ +
+ + {location.host} + {urlForResult(result)} + +
+ +
+
+
+ { + openResult(activeResultIndex) + }} + tooltip={ + <> + Open + + } + aria-label="Open search result" + > + Open + +
+ Open +
+
) diff --git a/frontend/src/lib/components/CommandBar/SearchResults.tsx b/frontend/src/lib/components/CommandBar/SearchResults.tsx index 2dde6f78cbead..3e6abbc35a27d 100644 --- a/frontend/src/lib/components/CommandBar/SearchResults.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResults.tsx @@ -1,6 +1,4 @@ -import clsx from 'clsx' import { useValues } from 'kea' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { DetectiveHog } from '../hedgehogs' import { searchBarLogic } from './searchBarLogic' @@ -10,27 +8,17 @@ import { SearchResultPreview } from './SearchResultPreview' export const SearchResults = (): JSX.Element => { const { combinedSearchResults, combinedSearchLoading, activeResultIndex } = useValues(searchBarLogic) - const { ref, size } = useResizeBreakpoints({ - 0: 'small', - 550: 'normal', - }) - return ( -
+ <> {!combinedSearchLoading && combinedSearchResults?.length === 0 ? ( -
+

No results

This doesn't happen often, but we're stumped!

) : ( -
-
+
+
{combinedSearchLoading && ( <> @@ -48,13 +36,11 @@ export const SearchResults = (): JSX.Element => { /> ))}
- {size !== 'small' ? ( -
- -
- ) : null} +
+ +
)} -
+ ) } diff --git a/frontend/src/lib/components/CommandBar/SearchTabs.tsx b/frontend/src/lib/components/CommandBar/SearchTabs.tsx index 37ff41ff30a53..aa3ddb67e8496 100644 --- a/frontend/src/lib/components/CommandBar/SearchTabs.tsx +++ b/frontend/src/lib/components/CommandBar/SearchTabs.tsx @@ -12,11 +12,13 @@ type SearchTabsProps = { export const SearchTabs = ({ inputRef }: SearchTabsProps): JSX.Element | null => { const { tabsGrouped } = useValues(searchBarLogic) return ( -
+
{Object.entries(tabsGrouped).map(([group, tabs]) => (
{group !== 'all' && ( - {groupToName[group]} + + {groupToName[group as keyof typeof groupToName]} + )} {tabs.map((tab) => ( diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss index 02aa24cb7a11d..3150d46ed5ada 100644 --- a/frontend/src/lib/components/CommandBar/index.scss +++ b/frontend/src/lib/components/CommandBar/index.scss @@ -16,11 +16,6 @@ } } -.SearchResults { - // offset container height by input - height: calc(100% - 2.875rem); -} - .CommandBar__overlay { position: fixed; top: 0; diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts index b3576ac482d10..1b96c64c34b81 100644 --- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts @@ -61,6 +61,7 @@ export const searchBarLogic = kea([ onArrowUp: (activeIndex: number, maxIndex: number) => ({ activeIndex, maxIndex }), onArrowDown: (activeIndex: number, maxIndex: number) => ({ activeIndex, maxIndex }), openResult: (index: number) => ({ index }), + setActiveResultIndex: (index: number) => ({ index }), }), loaders(({ values, actions }) => ({ rawSearchResponse: [ @@ -208,6 +209,7 @@ export const searchBarLogic = kea([ openResult: () => 0, onArrowUp: (_, { activeIndex, maxIndex }) => (activeIndex > 0 ? activeIndex - 1 : maxIndex), onArrowDown: (_, { activeIndex, maxIndex }) => (activeIndex < maxIndex ? activeIndex + 1 : 0), + setActiveResultIndex: (_, { index }) => index, }, ], activeTab: [ diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index d41a232518b18..ae93860525488 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -314,3 +314,11 @@ export const SESSION_REPLAY_MINIMUM_DURATION_OPTIONS: LemonSelectOptions { _debugTag?: string _runFn: () => Promise @@ -8,7 +11,7 @@ class ConcurrencyControllerItem { constructor( concurrencyController: ConcurrencyController, userFn: () => Promise, - abortController: AbortController, + abortController: AbortController | undefined, priority: number = Infinity, debugTag: string | undefined ) { @@ -17,7 +20,7 @@ class ConcurrencyControllerItem { const { promise, resolve, reject } = promiseResolveReject() this._promise = promise this._runFn = async () => { - if (abortController.signal.aborted) { + if (abortController?.signal.aborted) { reject(new FakeAbortError(abortController.signal.reason || 'AbortError')) return } @@ -32,7 +35,7 @@ class ConcurrencyControllerItem { reject(error) } } - abortController.signal.addEventListener('abort', () => { + abortController?.signal.addEventListener('abort', () => { reject(new FakeAbortError(abortController.signal.reason || 'AbortError')) }) promise @@ -76,7 +79,7 @@ export class ConcurrencyController { }: { fn: () => Promise priority?: number - abortController: AbortController + abortController?: AbortController debugTag?: string }): Promise => { const item = new ConcurrencyControllerItem(this, fn, abortController, priority, debugTag) diff --git a/package.json b/package.json index 6944c5b335e23..7d55acb0ca23e 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.202.2", + "posthog-js": "1.202.4", "posthog-js-lite": "3.0.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/plugin-server/package.json b/plugin-server/package.json index 11df155e0757c..9014d19be548b 100644 --- a/plugin-server/package.json +++ b/plugin-server/package.json @@ -69,16 +69,17 @@ "express": "^4.18.2", "faker": "^5.5.3", "fast-deep-equal": "^3.1.3", + "fastpriorityqueue": "^0.7.5", "fernet-nodejs": "^1.0.6", "generic-pool": "^3.7.1", "graphile-worker": "0.13.0", "ioredis": "^4.27.6", "ipaddr.js": "^2.1.0", "kafkajs": "^2.2.0", - "lz4-kafkajs": "1.0.0", "kafkajs-snappy": "^1.1.0", "lru-cache": "^6.0.0", "luxon": "^3.4.4", + "lz4-kafkajs": "1.0.0", "node-fetch": "^2.6.1", "node-rdkafka": "^2.17.0", "node-schedule": "^2.1.0", diff --git a/plugin-server/pnpm-lock.yaml b/plugin-server/pnpm-lock.yaml index c297462845d8e..f187191553102 100644 --- a/plugin-server/pnpm-lock.yaml +++ b/plugin-server/pnpm-lock.yaml @@ -91,6 +91,9 @@ dependencies: fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 + fastpriorityqueue: + specifier: ^0.7.5 + version: 0.7.5 fernet-nodejs: specifier: ^1.0.6 version: 1.0.6 @@ -6276,6 +6279,10 @@ packages: strnum: 1.0.5 dev: false + /fastpriorityqueue@0.7.5: + resolution: {integrity: sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA==} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: diff --git a/plugin-server/src/utils/concurrencyController.ts b/plugin-server/src/utils/concurrencyController.ts new file mode 100644 index 0000000000000..ac84d439fd507 --- /dev/null +++ b/plugin-server/src/utils/concurrencyController.ts @@ -0,0 +1,133 @@ +import FastPriorityQueue from 'fastpriorityqueue' + +export function promiseResolveReject(): { + resolve: (value: T) => void + reject: (reason?: any) => void + promise: Promise +} { + let resolve: (value: T) => void + let reject: (reason?: any) => void + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve + reject = innerReject + }) + return { resolve: resolve!, reject: reject!, promise } +} + +// Note that this file also exists in the frontend code, please keep them in sync as the tests only exist in the other version +class ConcurrencyControllerItem { + _debugTag?: string + _runFn: () => Promise + _priority: number = Infinity + _promise: Promise + constructor( + concurrencyController: ConcurrencyController, + userFn: () => Promise, + abortController: AbortController | undefined, + priority: number = Infinity, + debugTag: string | undefined + ) { + this._debugTag = debugTag + this._priority = priority + const { promise, resolve, reject } = promiseResolveReject() + this._promise = promise + this._runFn = async () => { + if (abortController?.signal.aborted) { + reject(new FakeAbortError(abortController.signal.reason || 'AbortError')) + return + } + if (concurrencyController._current.length >= concurrencyController._concurrencyLimit) { + throw new Error('Developer Error: ConcurrencyControllerItem: _runFn called while already running') + } + try { + concurrencyController._current.push(this) + const result = await userFn() + resolve(result) + } catch (error) { + reject(error) + } + } + abortController?.signal.addEventListener('abort', () => { + reject(new FakeAbortError(abortController.signal.reason || 'AbortError')) + }) + promise + .catch(() => { + // ignore + }) + .finally(() => { + if (concurrencyController._current.includes(this)) { + concurrencyController._current = concurrencyController._current.filter((item) => item !== this) + concurrencyController._runNext() + } + }) + } +} + +export class ConcurrencyController { + _concurrencyLimit: number + + _current: ConcurrencyControllerItem[] = [] + private _queue: FastPriorityQueue> = new FastPriorityQueue( + (a, b) => a._priority < b._priority + ) + + constructor(concurrencyLimit: number) { + this._concurrencyLimit = concurrencyLimit + } + + /** + * Run a function with a mutex. If the mutex is already running, the function will be queued and run when the mutex + * is available. + * @param fn The function to run + * @param priority The priority of the function. Lower numbers will be run first. Defaults to Infinity. + * @param abortController An AbortController that, if aborted, will reject the promise and immediately start the next item in the queue. + * @param debugTag + */ + run = ({ + fn, + priority, + abortController, + debugTag, + }: { + fn: () => Promise + priority?: number + abortController?: AbortController + debugTag?: string + }): Promise => { + const item = new ConcurrencyControllerItem(this, fn, abortController, priority, debugTag) + + this._queue.add(item) + + this._tryRunNext() + + return item._promise + } + + _runNext(): void { + const next = this._queue.poll() + if (next) { + next._runFn() + .catch(() => { + // ignore + }) + .finally(() => { + this._tryRunNext() + }) + } + } + + _tryRunNext(): void { + if (this._current.length < this._concurrencyLimit) { + this._runNext() + } + } + + setConcurrencyLimit = (limit: number): void => { + this._concurrencyLimit = limit + } +} + +// Create a fake AbortError that allows us to use e.name === 'AbortError' to check if an error is an AbortError +class FakeAbortError extends Error { + name = 'AbortError' +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f383926dd1807..902f297bc841d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -308,8 +308,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.202.2 - version: 1.202.2 + specifier: 1.202.4 + version: 1.202.4 posthog-js-lite: specifier: 3.0.0 version: 3.0.0 @@ -13319,7 +13319,7 @@ packages: gopd: 1.2.0 has-symbols: 1.1.0 hasown: 2.0.2 - math-intrinsics: 1.0.0 + math-intrinsics: 1.1.0 dev: true /get-nonce@1.0.1: @@ -14076,7 +14076,7 @@ packages: hogan.js: 3.0.2 htm: 3.1.1 instantsearch-ui-components: 0.3.0 - preact: 10.25.2 + preact: 10.25.3 qs: 6.9.7 search-insights: 2.13.0 dev: false @@ -16003,8 +16003,8 @@ packages: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false - /math-intrinsics@1.0.0: - resolution: {integrity: sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==} + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} dev: true @@ -17909,12 +17909,12 @@ packages: resolution: {integrity: sha512-dyajjnfzZD1tht4N7p7iwf7nBnR1MjVaVu+MKr+7gBgA39bn28wizCIJZztZPtHy4PY0YwtSGgwfBCuG/hnHgA==} dev: false - /posthog-js@1.202.2: - resolution: {integrity: sha512-9p7dAWuCfoM0WrasubGwtC8i38HU3iMqK3gd0mhyAoTrEVMVozTQq64Toc2VEv8H69NGNn6ikk5t2LclHT9XFA==} + /posthog-js@1.202.4: + resolution: {integrity: sha512-YLu6f1ibAkiopGivGQnLBaCKegT+0GHP3DfP72z3KVby2UXLBB7dj+GIa54zfjburE7ehasysRzeK5nW39QOfA==} dependencies: core-js: 3.39.0 fflate: 0.4.8 - preact: 10.25.2 + preact: 10.25.3 web-vitals: 4.2.4 dev: false @@ -17922,8 +17922,8 @@ packages: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} dev: false - /preact@10.25.2: - resolution: {integrity: sha512-GEts1EH3oMnqdOIeXhlbBSddZ9nrINd070WBOiPO2ous1orrKGUM4SMDbwyjSWD1iMS2dBvaDjAa5qUhz3TXqw==} + /preact@10.25.3: + resolution: {integrity: sha512-dzQmIFtM970z+fP9ziQ3yG4e3ULIbwZzJ734vaMVUTaKQ2+Ru1Ou/gjshOYVHCcd1rpAelC6ngjvjDXph98unQ==} dev: false /prelude-ls@1.2.1: