From d5dfc86641597738a060459928a6463092a27b4f Mon Sep 17 00:00:00 2001 From: Tim Glaser Date: Wed, 30 Mar 2022 11:46:47 +0000 Subject: [PATCH 01/18] Universal search WIP --- ee/clickhouse/views/groups.py | 10 ++++++---- posthog/api/annotation.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 59025a6eb369c..42f48e20adc58 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -1,7 +1,8 @@ from collections import defaultdict from typing import Dict, List, cast -from rest_framework import mixins, request, response, serializers, viewsets +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, mixins, request, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.pagination import CursorPagination @@ -60,9 +61,10 @@ class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewse queryset = Group.objects.all() pagination_class = GroupCursorPagination permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] - - def get_queryset(self): - return super().get_queryset().filter(group_type_index=self.request.GET["group_type_index"]) + filter_backends = [DjangoFilterBackend] + filterset_fields = ["group_type_index"] + filter_backends = [filters.SearchFilter] + search_fields = ["group_key"] @action(methods=["GET"], detail=False) def find(self, request: request.Request, **kw) -> response.Response: diff --git a/posthog/api/annotation.py b/posthog/api/annotation.py index 5e50dd5b80ad1..bf16d537889f9 100644 --- a/posthog/api/annotation.py +++ b/posthog/api/annotation.py @@ -3,7 +3,7 @@ from django.db.models import QuerySet from django.db.models.signals import post_save from django.dispatch import receiver -from rest_framework import request, serializers, viewsets +from rest_framework import filters, request, serializers, viewsets from rest_framework.permissions import IsAuthenticated from rest_hooks.signals import raw_hook_event @@ -58,6 +58,8 @@ class AnnotationsViewSet(StructuredViewSetMixin, AnalyticsDestroyModelMixin, vie queryset = Annotation.objects.all() serializer_class = AnnotationSerializer permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] + filter_backends = [filters.SearchFilter] + search_fields = ["content"] def get_queryset(self) -> QuerySet: queryset = super().get_queryset() From 1f59138d7d6e99fedf93c47a96198c421ff71a52 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 30 Mar 2022 15:38:15 +0000 Subject: [PATCH 02/18] wip frontend universal search --- .../src/layout/navigation/TopBar/TopBar.tsx | 56 +++ .../UniversalSearch/InfiniteList.scss | 84 ++++ .../UniversalSearch/InfiniteSelectResults.tsx | 103 ++++ .../UniversalSearch/UniversalSearch.scss | 64 +++ .../UniversalSearch/UniversalSearch.tsx | 141 ++++++ .../UniversalSearch/UniversalSearchPopup.tsx | 74 +++ .../components/UniversalSearch/searchList.tsx | 459 ++++++++++++++++++ .../UniversalSearch/searchListLogic.ts | 328 +++++++++++++ .../lib/components/UniversalSearch/types.ts | 65 +++ .../UniversalSearch/universalSearchLogic.tsx | 368 ++++++++++++++ 10 files changed, 1742 insertions(+) create mode 100644 frontend/src/lib/components/UniversalSearch/InfiniteList.scss create mode 100644 frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx create mode 100644 frontend/src/lib/components/UniversalSearch/UniversalSearch.scss create mode 100644 frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx create mode 100644 frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx create mode 100644 frontend/src/lib/components/UniversalSearch/searchList.tsx create mode 100644 frontend/src/lib/components/UniversalSearch/searchListLogic.ts create mode 100644 frontend/src/lib/components/UniversalSearch/types.ts create mode 100644 frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index a3e1918cfcf28..f2aa4ac43a8a3 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -14,6 +14,11 @@ import { IconMenu, IconMenuOpen } from '../../../lib/components/icons' import { CreateProjectModal } from '../../../scenes/project/CreateProjectModal' import './TopBar.scss' import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { UniversalSearchPopup } from 'lib/components/UniversalSearch/UniversalSearchPopup' +import { UniversalSearchGroupType } from 'lib/components/UniversalSearch/types' +import { urls } from 'scenes/urls' +import { combineUrl, router } from 'kea-router' +import { ChartDisplayType, InsightType } from '~/types' export function TopBar(): JSX.Element { const { isSideBarShown, bareNav, mobileLayout, isCreateOrganizationModalShown, isCreateProjectModalShown } = @@ -39,6 +44,57 @@ export function TopBar(): JSX.Element { +
+ { + console.log('new values:::', value, groupType, item) + if (groupType === UniversalSearchGroupType.Events) { + // Go to Insights instead? + router.actions.push(combineUrl(urls.events(), { eventFilter: value }).url) + router.actions.push( + combineUrl( + urls.insightNew({ + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + events: [{ id: value, name: value, type: 'events', math: 'dau' }], + }) + ).url + ) + } else if (groupType === UniversalSearchGroupType.Actions) { + router.actions.push( + combineUrl( + urls.insightNew({ + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [ + { + id: item.id, + name: item.name, + type: 'actions', + order: 0, + }, + ], + }) + ).url + ) + } else if (groupType === UniversalSearchGroupType.Cohorts) { + router.actions.push(urls.cohort(value)) + } else if (groupType === UniversalSearchGroupType.Persons) { + router.actions.push(urls.person(value)) + } + }} + /> +
diff --git a/frontend/src/lib/components/UniversalSearch/InfiniteList.scss b/frontend/src/lib/components/UniversalSearch/InfiniteList.scss new file mode 100644 index 0000000000000..62e1109f87c43 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/InfiniteList.scss @@ -0,0 +1,84 @@ +.taxonomic-infinite-list { + min-height: 200px; + + &.empty-infinite-list { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + .no-infinite-results { + color: #666; + } + } + + .taxonomic-list-row { + display: flex; + align-items: center; + justify-content: space-between; + color: #2d2d2d; + padding: 4px 12px; + cursor: pointer; + border: none; + + .taxonomic-list-row-contents { + display: flex; + align-items: center; + + .taxonomic-list-row-contents-icon { + width: 30px; + + svg.taxonomy-icon { + vertical-align: middle; + flex-shrink: 0; + height: 18.5px; + + &.taxonomy-icon-muted { + path { + fill: var(--taxonomy-icon-muted); + } + } + + &.taxonomy-icon-verified { + height: 24px; + path { + fill: var(--success); + } + + &:not(.taxonomy-icon-ph) { + margin-bottom: -4px; // Slightly larger height requires offset to look vertically centered + } + } + } + } + } + + & > div { + max-width: 100%; + + & > span { + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &.hover { + background: rgba(0, 0, 0, 0.1); + } + + &.selected { + font-weight: bold; + } + + &.skeleton-row { + // center-align this antd skeleton + .ant-skeleton-paragraph { + margin-bottom: 0; + } + } + &.expand-row { + color: var(--primary); + } + } +} diff --git a/frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx b/frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx new file mode 100644 index 0000000000000..7d7afaf7a333d --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import { Tag } from 'antd' +import { BindLogic, useActions, useValues } from 'kea' +import { universalSearchLogic } from './universalSearchLogic' +import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' +import { SearchList } from 'lib/components/UniversalSearch/searchList' +import { UniversalSearchGroupType, UniversalSearchLogicProps } from './types' +import clsx from 'clsx' + +export interface InfiniteSelectResultsProps { + focusInput: () => void + universalSearchLogicProps: UniversalSearchLogicProps +} + +function CategoryPill({ + isActive, + groupType, + universalSearchLogicProps, + onClick, +}: { + isActive: boolean + groupType: UniversalSearchGroupType + universalSearchLogicProps: UniversalSearchLogicProps + onClick: () => void +}): JSX.Element { + const logic = searchListLogic({ ...universalSearchLogicProps, listGroupType: groupType }) + const { taxonomicGroups } = useValues(universalSearchLogic) + const { totalResultCount, totalListCount } = useValues(logic) + + const group = taxonomicGroups.find((g) => g.type === groupType) + + // :TRICKY: use `totalListCount` (results + extra) to toggle interactivity, while showing `totalResultCount` + const canInteract = totalListCount > 0 + + return ( + + {group?.name} + {': '} + {totalResultCount ?? '...'} + + ) +} + +export function InfiniteSelectResults({ + focusInput, + universalSearchLogicProps, +}: InfiniteSelectResultsProps): JSX.Element { + const { activeTab, taxonomicGroups, taxonomicGroupTypes } = useValues(universalSearchLogic) + const { setActiveTab } = useActions(universalSearchLogic) + + if (taxonomicGroupTypes.length === 1) { + return ( + + + + ) + } + + const openTab = activeTab || taxonomicGroups[0].type + return ( + <> +
Categories
+
+ {taxonomicGroupTypes.map((groupType) => { + return ( + { + setActiveTab(groupType) + focusInput() + }} + /> + ) + })} +
+
+ {taxonomicGroups.find((g) => g.type === openTab)?.name || openTab} +
+ {taxonomicGroupTypes.map((groupType) => { + return ( +
+ + + +
+ ) + })} + + ) +} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss new file mode 100644 index 0000000000000..1abc035689a02 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss @@ -0,0 +1,64 @@ +.universal-search { + width: 550px; + max-width: calc(100vw - 40px); + background: white; + display: flex; + flex-direction: column; + + &.force-minimum-width { + min-width: 300px; + } + + &.one-taxonomic-tab { + .taxonomic-infinite-list { + margin-top: 10px; + } + } + + .taxonomic-group-title { + display: flex; + width: 100%; + align-items: stretch; + color: var(--text-muted); + text-transform: uppercase; + font-size: 12px; + line-height: 12px; + padding-top: 16px; + margin-bottom: 12px; + font-weight: 600; + &.with-border { + border-top: 1px solid var(--border-light); + } + } + + .taxonomic-pills { + margin-top: 8px; + margin-bottom: 8px; + .ant-tag { + transition: none; + margin-right: 2px; + margin-bottom: 2px; + cursor: pointer; + color: var(--primary); + background: var(--bg-side); + border-color: var(--bg-side); + &.taxonomic-count-zero { + color: var(--text-muted); + cursor: not-allowed; + } + &.taxonomic-pill-active { + color: var(--text-light); + background: var(--primary); + border-color: var(--primary); + } + } + } + + .magnifier-icon { + font-size: 18px; + color: var(--text-muted); + &.magnifier-icon-active { + color: var(--primary); + } + } +} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx new file mode 100644 index 0000000000000..540592f303046 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx @@ -0,0 +1,141 @@ +import './UniversalSearch.scss' +import React, { useEffect, useMemo, useRef } from 'react' +import { Input } from 'antd' +import { useValues, useActions, BindLogic } from 'kea' +import { InfiniteSelectResults } from './InfiniteSelectResults' +import { IconKeyboard, IconMagnifier } from '../icons' +import { Tooltip } from '../Tooltip' +import clsx from 'clsx' +import { universalSearchLogic } from './universalSearchLogic' +import { UniversalSearchLogicProps, UniversalSearchProps } from './types' + +let uniqueMemoizedIndex = 0 + +export function UniversalSearch({ + taxonomicFilterLogicKey: taxonomicFilterLogicKeyInput, + groupType, + value, + onChange, + onClose, + taxonomicGroupTypes, + optionsFromProp, + eventNames, + height, + width, + popoverEnabled = true, + selectFirstItem = true, +}: UniversalSearchProps): JSX.Element { + // Generate a unique key for each unique UniversalSearch that's rendered + const universalSearchLogicKey = useMemo( + () => taxonomicFilterLogicKeyInput || `universal-search-${uniqueMemoizedIndex++}`, + [taxonomicFilterLogicKeyInput] + ) + + const searchInputRef = useRef(null) + const focusInput = (): void => searchInputRef.current?.focus() + + const universalSearchLogicProps: UniversalSearchLogicProps = { + universalSearchLogicKey, + groupType, + value, + onChange, + taxonomicGroupTypes, + optionsFromProp, + eventNames, + popoverEnabled, + selectFirstItem, + } + + const logic = universalSearchLogic(universalSearchLogicProps) + const { searchQuery, searchPlaceholder } = useValues(logic) + const { setSearchQuery, moveUp, moveDown, tabLeft, tabRight, selectSelected } = useActions(logic) + + useEffect(() => { + window.setTimeout(() => focusInput(), 1) + }, []) + + const style = { + ...(width ? { width } : {}), + ...(height ? { height } : {}), + } + + return ( + +
+
+ + } + value={searchQuery} + ref={(ref) => (searchInputRef.current = ref)} + onChange={(e) => setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'ArrowUp') { + e.preventDefault() + moveUp() + } + if (e.key === 'ArrowDown') { + e.preventDefault() + moveDown() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + tabLeft() + } + if (e.key === 'ArrowRight') { + e.preventDefault() + tabRight() + } + if (e.key === 'Tab') { + e.preventDefault() + if (e.shiftKey) { + tabLeft() + } else { + tabRight() + } + } + + if (e.key === 'Enter') { + e.preventDefault() + selectSelected() + } + if (e.key === 'Escape') { + e.preventDefault() + onClose?.() + } + }} + suffix={ + + You can easily navigate between tabs with your keyboard.{' '} +
+ Use tab or right arrow to move to the next tab. +
+
+ Use shift + tab or left arrow to move to the previous tab. +
+ + } + > + +
+ } + /> +
+ +
+
+ ) +} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx new file mode 100644 index 0000000000000..b39f3920400bd --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react' +import { LemonButtonWithPopupProps } from '../LemonButton' +import { TaxonomicFilterValue } from '../TaxonomicFilter/types' +import { UniversalSearchGroupType } from './types' +import { Popup } from 'lib/components/Popup/Popup' +import { UniversalSearch } from './UniversalSearch' +import { Button } from 'antd' +import { DownOutlined } from '@ant-design/icons' +import clsx from 'clsx' + +export interface UniversalSearchPopupProps + extends Omit { + groupType: UniversalSearchGroupType + value?: ValueType + onChange: (value: ValueType, groupType: UniversalSearchGroupType) => void + + groupTypes?: UniversalSearchGroupType[] + renderValue?: (value: ValueType) => JSX.Element + dataAttr?: string + eventNames?: string[] + placeholder?: React.ReactNode + dropdownMatchSelectWidth?: boolean + allowClear?: boolean +} + +export function UniversalSearchPopup({ + groupType, + value, + onChange, + renderValue, + groupTypes, + dataAttr, + eventNames = [], + placeholder = 'Please select', + style, + fullWidth = true, +}: UniversalSearchPopupProps): JSX.Element { + const [visible, setVisible] = useState(false) + + return ( + { + onChange?.(payload, type, item) + setVisible(false) + }} + taxonomicGroupTypes={groupTypes ?? [groupType]} + eventNames={eventNames} + /> + } + visible={visible} + onClickOutside={() => setVisible(false)} + > + {({ setRef }) => ( + + )} + + ) +} diff --git a/frontend/src/lib/components/UniversalSearch/searchList.tsx b/frontend/src/lib/components/UniversalSearch/searchList.tsx new file mode 100644 index 0000000000000..d33b5690c1195 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/searchList.tsx @@ -0,0 +1,459 @@ +import './InfiniteList.scss' +import '../Popup/Popup.scss' +import React, { useState } from 'react' +import { Empty, Skeleton, Tag } from 'antd' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' +import { + getKeyMapping, + PropertyKeyDescription, + PropertyKeyInfo, + PropertyKeyTitle, +} from 'lib/components/PropertyKeyInfo' +import { BindLogic, Provider, useActions, useValues } from 'kea' +import { searchListLogic, NO_ITEM_SELECTED } from './searchListLogic' +import { universalSearchLogic } from './universalSearchLogic' +import { + TaxonomicDefinitionTypes, + // UniversalSearchGroup, + // UniversalSearchGroupType, +} from 'lib/components/TaxonomicFilter/types' +import ReactDOM from 'react-dom' +import { usePopper } from 'react-popper' +import { ActionType, CohortType, EventDefinition, KeyMapping, PropertyDefinition } from '~/types' +import { AimOutlined } from '@ant-design/icons' +import { Link } from 'lib/components/Link' +import { ActionSelectInfo } from 'scenes/insights/ActionSelectInfo' +import { urls } from 'scenes/urls' +import { dayjs } from 'lib/dayjs' +import { FEATURE_FLAGS, STALE_EVENT_SECONDS } from 'lib/constants' +import { Tooltip } from '../Tooltip' +import clsx from 'clsx' +import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' +import { definitionPopupLogic } from 'lib/components/DefinitionPopup/definitionPopupLogic' +import { ControlledDefinitionPopupContents } from 'lib/components/DefinitionPopup/DefinitionPopupContents' +import { pluralize } from 'lib/utils' +import { UniversalSearchGroup, UniversalSearchGroupType } from './types' + +enum ListTooltip { + None = 0, + Left = 1, + Right = 2, +} + +export function tooltipDesiredState(element?: Element | null): ListTooltip { + let desiredState: ListTooltip = ListTooltip.None + const rect = element?.getBoundingClientRect() + if (rect) { + if (window.innerWidth - rect.right > 300) { + desiredState = ListTooltip.Right + } else if (rect.left > 300) { + desiredState = ListTooltip.Left + } + } + return desiredState +} + +const staleIndicator = (parsedLastSeen: dayjs.Dayjs | null): JSX.Element => { + return ( + + This event was last seen {parsedLastSeen ? parsedLastSeen.fromNow() : 'a while ago'}. + + } + > + Stale + + ) +} + +const unusedIndicator = (eventNames: string[]): JSX.Element => { + return ( + + This property has not been seen on{' '} + {eventNames ? ( + <> + the event{eventNames.length > 1 ? 's' : ''}{' '} + {eventNames.map((e, index) => ( + <> + {index === 0 ? '' : index === eventNames.length - 1 ? ' and ' : ', '} + "{e}" + + ))} + + ) : ( + 'this event' + )} + , but has been seen on other events. + + } + > + Not seen + + ) +} + +const renderItemContents = ({ + item, + listGroupType, + group, + featureFlags, + eventNames, +}: { + item: TaxonomicDefinitionTypes + listGroupType: UniversalSearchGroupType + group: UniversalSearchGroup + featureFlags: FeatureFlagsSet + eventNames: string[] +}): JSX.Element | string => { + const parsedLastSeen = (item as EventDefinition).last_seen_at ? dayjs((item as EventDefinition).last_seen_at) : null + const isStale = + (listGroupType === UniversalSearchGroupType.Events && !parsedLastSeen) || + dayjs().diff(parsedLastSeen, 'seconds') > STALE_EVENT_SECONDS + + const isUnusedEventProperty = + (listGroupType === UniversalSearchGroupType.NumericalEventProperties || + listGroupType === UniversalSearchGroupType.EventProperties) && + (item as PropertyDefinition).is_event_property !== null && + !(item as PropertyDefinition).is_event_property + + const icon =
{group.getIcon?.(item)}
+ + return listGroupType === UniversalSearchGroupType.EventProperties || + listGroupType === UniversalSearchGroupType.NumericalEventProperties || + listGroupType === UniversalSearchGroupType.PersonProperties || + listGroupType === UniversalSearchGroupType.Events || + // listGroupType === UniversalSearchGroupType.CustomEvents || + listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix) ? ( + <> +
+ {featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon} + +
+ {isStale && staleIndicator(parsedLastSeen)} + {isUnusedEventProperty && unusedIndicator(eventNames)} + + ) : ( +
+ {listGroupType === UniversalSearchGroupType.Elements ? ( + + ) : ( + <> + {featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon} + {item.name ?? ''} + + )} +
+ ) +} + +const renderItemPopupWithoutTaxonomy = ( + item: PropertyDefinition | CohortType | ActionType, + listGroupType: UniversalSearchGroupType, + group: UniversalSearchGroup +): JSX.Element | string => { + const width = 265 + let data: KeyMapping | null = null + const value = group.getValue(item) + + if (value) { + if (listGroupType === UniversalSearchGroupType.Actions && 'id' in item) { + return ( +
+ Actions + + edit + +
+

+ +

+ {item && } +
+ ) + } + + if ( + // NB: also update "selectedItemHasPopup" below + listGroupType === UniversalSearchGroupType.Events || + listGroupType === UniversalSearchGroupType.EventProperties || + listGroupType === UniversalSearchGroupType.NumericalEventProperties || + listGroupType === UniversalSearchGroupType.PersonProperties + ) { + data = getKeyMapping(value.toString(), 'event') + } else if (listGroupType === UniversalSearchGroupType.Elements) { + data = getKeyMapping(value.toString(), 'element') + } + + if (data) { + return ( +
+ + {data.description ?
: null} + + {'volume_30_day' in item && (item.volume_30_day || 0) > 0 ? ( +

+ Seen {item.volume_30_day} times.{' '} +

+ ) : null} + {'query_usage_30_day' in item && (item.query_usage_30_day || 0) > 0 ? ( +

+ Used in {item.query_usage_30_day} queries. +

+ ) : null} +
+ ) + } + } + + return item.name ?? '' +} + +const selectedItemHasPopup = ( + item?: TaxonomicDefinitionTypes, + listGroupType?: UniversalSearchGroupType, + group?: UniversalSearchGroup, + showNewPopups: boolean = false +): boolean => { + if (showNewPopups) { + return ( + // NB: also update "renderItemPopup" above + !!item && + !!group?.getValue(item) && + !!listGroupType && + ([ + UniversalSearchGroupType.Actions, + // UniversalSearchGroupType.Elements, + UniversalSearchGroupType.Events, + // UniversalSearchGroupType.CustomEvents, + UniversalSearchGroupType.EventProperties, + UniversalSearchGroupType.NumericalEventProperties, + UniversalSearchGroupType.PersonProperties, + UniversalSearchGroupType.Cohorts, + // UniversalSearchGroupType.CohortsWithAllUsers, + ].includes(listGroupType) || + listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) + ) + } + + return ( + // NB: also update "renderItemPopup" above + !!item && + !!group?.getValue(item) && + (listGroupType === UniversalSearchGroupType.Actions || + ((listGroupType === UniversalSearchGroupType.Events || + // listGroupType === UniversalSearchGroupType.Elements || + listGroupType === UniversalSearchGroupType.EventProperties || + listGroupType === UniversalSearchGroupType.NumericalEventProperties || + listGroupType === UniversalSearchGroupType.PersonProperties) && + !!getKeyMapping(group?.getValue(item), 'event'))) + ) +} + +export function SearchList(): JSX.Element { + const { mouseInteractionsEnabled, activeTab, searchQuery, value, groupType, eventNames } = + useValues(universalSearchLogic) + const { selectItem } = useActions(universalSearchLogic) + const { featureFlags } = useValues(featureFlagLogic) + + const { + isLoading, + results, + index, + listGroupType, + group, + selectedItem, + selectedItemInView, + isExpandable, + totalResultCount, + totalListCount, + expandedCount, + showPopover, + } = useValues(searchListLogic) + const { onRowsRendered, setIndex, expand, updateRemoteItem } = useActions(searchListLogic) + + const isActiveTab = listGroupType === activeTab + const showEmptyState = totalListCount === 0 && !isLoading + const showNewPopups = !!featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] + + const [referenceElement, setReferenceElement] = useState(null) + const [popperElement, setPopperElement] = useState(null) + + const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, { + placement: 'right', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 10], + }, + }, + { + name: 'preventOverflow', + options: { + padding: 10, + }, + }, + ], + }) + + const renderItem: ListRowRenderer = ({ index: rowIndex, style }: ListRowProps): JSX.Element | null => { + const item = results[rowIndex] + const itemValue = item ? group?.getValue?.(item) : null + const isSelected = listGroupType === groupType && itemValue === value + const isHighlighted = rowIndex === index && isActiveTab + + const commonDivProps: React.HTMLProps = { + key: `item_${rowIndex}`, + className: clsx( + 'taxonomic-list-row', + rowIndex === index && mouseInteractionsEnabled && 'hover', + isSelected && 'selected' + ), + onMouseOver: () => (mouseInteractionsEnabled ? setIndex(rowIndex) : setIndex(NO_ITEM_SELECTED)), + // if the popover is not enabled then don't leave the row selected when the mouse leaves it + onMouseLeave: () => (mouseInteractionsEnabled && !showPopover ? setIndex(NO_ITEM_SELECTED) : null), + style: style, + ref: isHighlighted ? setReferenceElement : null, + } + + return item && group ? ( +
selectItem(group, itemValue ?? null, item)} + > + {renderItemContents({ + item, + listGroupType, + group, + featureFlags, + eventNames, + })} +
+ ) : !item && rowIndex === totalListCount - 1 && isExpandable && !isLoading ? ( +
+ {group.expandLabel?.({ count: totalResultCount, expandedCount }) ?? + `Click here to see ${expandedCount - totalResultCount} more ${pluralize( + expandedCount - totalResultCount, + 'row', + 'rows', + false + )}`} +
+ ) : ( +
+ +
+ ) + } + + return ( +
+ {showEmptyState ? ( +
+ + {searchQuery ? ( + <> + No results for "{searchQuery}" + + ) : ( + 'No results' + )} + + } + /> +
+ ) : ( + + {({ height, width }) => ( + + )} + + )} + {isActiveTab && + selectedItemInView && + selectedItemHasPopup(selectedItem, listGroupType, group, showNewPopups) && + tooltipDesiredState(referenceElement) !== ListTooltip.None && + showPopover ? ( + + {ReactDOM.createPortal( + selectedItem && group ? ( + showNewPopups ? ( + + + + ) : ( +
+ {renderItemPopupWithoutTaxonomy( + selectedItem as PropertyDefinition | CohortType | ActionType, + listGroupType, + group + )} +
+ ) + ) : null, + document.querySelector('body') as HTMLElement + )} +
+ ) : null} +
+ ) +} diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts new file mode 100644 index 0000000000000..581f7e35e82a8 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts @@ -0,0 +1,328 @@ +import { kea } from 'kea' +import { combineUrl } from 'kea-router' +import api from 'lib/api' +import { RenderedRows } from 'react-virtualized/dist/es/List' +import { CohortType, EventDefinition } from '~/types' +import Fuse from 'fuse.js' + +import { ListFuse, ListStorage, LoaderOptions, TaxonomicDefinitionTypes } from 'lib/components/TaxonomicFilter/types' +import { SearchListLogicProps, UniversalSearchGroup } from 'lib/components/UniversalSearch/types' +import { universalSearchLogic } from './universalSearchLogic' + +import { searchListLogicType } from './searchListLogicType' +/* + by default the pop-up starts open for the first item in the list + this can be used with actions.setIndex to allow a caller to override that + */ +export const NO_ITEM_SELECTED = -1 + +function appendAtIndex(array: T[], items: any[], startIndex?: number): T[] { + if (startIndex === undefined) { + return [...array, ...items] + } + const arrayCopy = [...array] + items.forEach((item, i) => { + arrayCopy[startIndex + i] = item + }) + return arrayCopy +} + +const createEmptyListStorage = (searchQuery = '', first = false): ListStorage => ({ + results: [], + searchQuery, + count: 0, + first, +}) + +// simple cache with a setTimeout expiry +const API_CACHE_TIMEOUT = 60000 +let apiCache: Record = {} +let apiCacheTimers: Record = {} + +async function fetchCachedListResponse(path: string, searchParams: Record): Promise { + const url = combineUrl(path, searchParams).url + let response + if (apiCache[url]) { + response = apiCache[url] + } else { + response = await api.get(url) + apiCache[url] = response + apiCacheTimers[url] = window.setTimeout(() => { + delete apiCache[url] + delete apiCacheTimers[url] + }, API_CACHE_TIMEOUT) + } + return response +} + +export const searchListLogic = kea({ + path: (key) => ['lib', 'components', 'UniversalSearch', 'searchListLogic', key], + props: {} as SearchListLogicProps, + + key: (props) => `${props.universalSearchLogicKey}-${props.listGroupType}`, + + connect: (props: SearchListLogicProps) => ({ + values: [universalSearchLogic(props), ['searchQuery', 'value', 'groupType', 'taxonomicGroups']], + actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], + }), + + actions: { + selectSelected: true, + moveUp: true, + moveDown: true, + setIndex: (index: number) => ({ index }), + setLimit: (limit: number) => ({ limit }), + onRowsRendered: (rowInfo: RenderedRows) => ({ rowInfo }), + loadRemoteItems: (options: LoaderOptions) => options, + updateRemoteItem: (item: TaxonomicDefinitionTypes) => ({ item }), + expand: true, + }, + + reducers: ({ props }) => ({ + index: [ + (props.selectFirstItem === false ? NO_ITEM_SELECTED : 0) as number, + { + setIndex: (_, { index }) => index, + loadRemoteItemsSuccess: (state, { remoteItems }) => (remoteItems.queryChanged ? 0 : state), + }, + ], + showPopover: [props.popoverEnabled !== false, {}], + limit: [ + 100, + { + setLimit: (_, { limit }) => limit, + }, + ], + startIndex: [0, { onRowsRendered: (_, { rowInfo: { startIndex } }) => startIndex }], + stopIndex: [0, { onRowsRendered: (_, { rowInfo: { stopIndex } }) => stopIndex }], + isExpanded: [false, { expand: () => true }], + }), + + loaders: ({ values }) => ({ + remoteItems: [ + createEmptyListStorage('', true), + { + loadRemoteItems: async ({ offset, limit }, breakpoint) => { + // avoid the 150ms delay on first load + if (!values.remoteItems.first) { + await breakpoint(150) + } else { + // These connected values below might be read before they are available due to circular logic mounting. + // Adding a slight delay (breakpoint) fixes this. + await breakpoint(1) + } + + const { isExpanded, remoteEndpoint, scopedRemoteEndpoint, searchQuery } = values + + if (!remoteEndpoint) { + // should not have been here in the first place! + return createEmptyListStorage(searchQuery) + } + + const searchParams = { + [`${values.group?.searchAlias || 'search'}`]: searchQuery, + limit, + offset, + } + + const [response, expandedCountResponse] = await Promise.all([ + // get the list of results + fetchCachedListResponse( + scopedRemoteEndpoint && !isExpanded ? scopedRemoteEndpoint : remoteEndpoint, + searchParams + ), + // if this is an unexpanded scoped list, get the count for the normafull list + scopedRemoteEndpoint && !isExpanded + ? fetchCachedListResponse(remoteEndpoint, { + ...searchParams, + limit: 1, + offset: 0, + }) + : null, + ]) + breakpoint() + + const queryChanged = values.items.searchQuery !== values.searchQuery + + return { + results: appendAtIndex( + queryChanged ? [] : values.items.results, + response.results || response, + offset + ), + searchQuery: values.searchQuery, + queryChanged, + count: response.count || (response.results || []).length, + expandedCount: expandedCountResponse?.count, + } + }, + updateRemoteItem: ({ item }) => { + // On updating item, invalidate cache + apiCache = {} + apiCacheTimers = {} + return { + ...values.remoteItems, + results: values.remoteItems.results.map((i) => (i.name === item.name ? item : i)), + } + }, + }, + ], + }), + + listeners: ({ values, actions, props }) => ({ + onRowsRendered: ({ rowInfo: { startIndex, stopIndex, overscanStopIndex } }) => { + if (values.isRemoteDataSource) { + let loadFrom: number | null = null + for (let i = startIndex; i < (stopIndex + overscanStopIndex) / 2; i++) { + if (!values.results[i]) { + loadFrom = i + break + } + } + if (loadFrom !== null) { + actions.loadRemoteItems({ offset: loadFrom || startIndex, limit: values.limit }) + } + } + }, + setSearchQuery: () => { + if (values.isRemoteDataSource) { + actions.loadRemoteItems({ offset: 0, limit: values.limit }) + } else { + actions.setIndex(0) + } + }, + moveUp: () => { + const { index, totalListCount } = values + actions.setIndex((index - 1 + totalListCount) % totalListCount) + }, + moveDown: () => { + const { index, totalListCount } = values + actions.setIndex((index + 1) % totalListCount) + }, + selectSelected: () => { + if (values.isExpandableButtonSelected) { + actions.expand() + } else { + actions.selectItem(values.group, values.selectedItemValue, values.selectedItem) + } + }, + loadRemoteItemsSuccess: ({ remoteItems }) => { + actions.infiniteListResultsReceived(props.listGroupType, remoteItems) + }, + expand: () => { + actions.loadRemoteItems({ offset: values.index, limit: values.limit }) + }, + }), + + selectors: { + listGroupType: [() => [(_, props) => props.listGroupType], (listGroupType) => listGroupType], + isLoading: [(s) => [s.remoteItemsLoading], (remoteItemsLoading) => remoteItemsLoading], + group: [ + (s) => [s.listGroupType, s.taxonomicGroups], + (listGroupType, taxonomicGroups): UniversalSearchGroup => + taxonomicGroups.find((g) => g.type === listGroupType) as UniversalSearchGroup, + ], + remoteEndpoint: [(s) => [s.group], (group) => group?.endpoint || null], + scopedRemoteEndpoint: [(s) => [s.group], (group) => group?.scopedEndpoint || null], + isExpandable: [ + (s) => [s.remoteEndpoint, s.scopedRemoteEndpoint, s.remoteItems], + (remoteEndpoint, scopedRemoteEndpoint, remoteItems) => + !!( + remoteEndpoint && + scopedRemoteEndpoint && + remoteItems.expandedCount && + remoteItems.expandedCount > remoteItems.count + ), + ], + isExpandableButtonSelected: [ + (s) => [s.isExpandable, s.index, s.totalListCount], + (isExpandable, index, totalListCount) => isExpandable && index === totalListCount - 1, + ], + isRemoteDataSource: [(s) => [s.remoteEndpoint], (remoteEndpoint) => !!remoteEndpoint], + rawLocalItems: [ + (selectors) => [ + (state, props) => { + const taxonomicGroups = selectors.taxonomicGroups(state) + const group = taxonomicGroups.find((g) => g.type === props.listGroupType) + if (group?.logic && group?.value) { + return group.logic.selectors[group.value]?.(state) || null + } + if (group?.options) { + return group.options + } + if (props.optionsFromProp && Object.keys(props.optionsFromProp).includes(props.listGroupType)) { + return props.optionsFromProp[props.listGroupType] + } + return null + }, + ], + (rawLocalItems: (EventDefinition | CohortType)[]) => rawLocalItems, + ], + fuse: [ + (s) => [s.rawLocalItems, s.group], + (rawLocalItems, group): ListFuse => + new Fuse( + (rawLocalItems || []).map((item) => ({ + name: group?.getName?.(item) || '', + item: item, + })), + { + keys: ['name'], + threshold: 0.3, + } + ), + ], + localItems: [ + (s) => [s.rawLocalItems, s.searchQuery, s.fuse], + (rawLocalItems, searchQuery, fuse): ListStorage => { + if (rawLocalItems) { + const filteredItems = searchQuery + ? fuse.search(searchQuery).map((result) => result.item.item) + : rawLocalItems + + return { + results: filteredItems, + count: filteredItems.length, + searchQuery, + } + } + return createEmptyListStorage() + }, + ], + items: [ + (s) => [s.isRemoteDataSource, s.remoteItems, s.localItems], + (isRemoteDataSource, remoteItems, localItems) => (isRemoteDataSource ? remoteItems : localItems), + ], + totalResultCount: [(s) => [s.items], (items) => items.count || 0], + totalExtraCount: [(s) => [s.isExpandable], (isExpandable) => (isExpandable ? 1 : 0)], + totalListCount: [ + (s) => [s.totalResultCount, s.totalExtraCount], + (totalResultCount, totalExtraCount) => totalResultCount + totalExtraCount, + ], + expandedCount: [(s) => [s.items], (items) => items.expandedCount || 0], + results: [(s) => [s.items], (items) => items.results], + selectedItem: [ + (s) => [s.index, s.items], + (index, items): TaxonomicDefinitionTypes | undefined => (index >= 0 ? items.results[index] : undefined), + ], + selectedItemValue: [ + (s) => [s.selectedItem, s.group], + (selectedItem, group) => (selectedItem ? group?.getValue?.(selectedItem) || null : null), + ], + selectedItemInView: [ + (s) => [s.index, s.startIndex, s.stopIndex], + (index, startIndex, stopIndex) => typeof index === 'number' && index >= startIndex && index <= stopIndex, + ], + }, + + events: ({ actions, values, props }) => ({ + afterMount: () => { + if (values.isRemoteDataSource) { + actions.loadRemoteItems({ offset: 0, limit: values.limit }) + } else if (values.groupType === props.listGroupType) { + const { value, group, results } = values + actions.setIndex(results.findIndex((r) => group?.getValue?.(r) === value)) + } + }, + }), +}) diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts new file mode 100644 index 0000000000000..aafb53546c4a5 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -0,0 +1,65 @@ +import { LogicWrapper } from 'kea' +import { SimpleOption, TaxonomicFilterValue } from '../TaxonomicFilter/types' + +export interface UniversalSearchLogicProps extends UniversalSearchProps { + universalSearchLogicKey: string +} + +export interface SearchListLogicProps extends UniversalSearchLogicProps { + listGroupType: UniversalSearchGroupType +} + +export interface UniversalSearchProps { + groupType?: UniversalSearchGroupType + value?: TaxonomicFilterValue + onChange?: (group: UniversalSearchGroup, value: TaxonomicFilterValue, item: any) => void + onClose?: () => void + taxonomicGroupTypes: UniversalSearchGroupType[] + taxonomicFilterLogicKey?: string + optionsFromProp?: Partial> + eventNames?: string[] + height?: number + width?: number + popoverEnabled?: boolean + selectFirstItem?: boolean +} + +export enum UniversalSearchGroupType { + Actions = 'actions', + Cohorts = 'cohorts', + // CohortsWithAllUsers = 'cohorts_with_all', + // Elements = 'elements', + Events = 'events', + EventProperties = 'event_properties', + NumericalEventProperties = 'numerical_event_properties', + Persons = 'persons', + // PersonProperties = 'person_properties', + // PageviewUrls = 'pageview_urls', + // Screens = 'screens', + // CustomEvents = 'custom_events', + // Wildcards = 'wildcard', + GroupsPrefix = 'groups', + FeatureFlags = 'feature_flags', + Experiments = 'experiments', +} + +export interface UniversalSearchGroup { + name: string + searchPlaceholder: string + type: UniversalSearchGroupType + endpoint?: string + /** If present, will be used instead of "endpoint" until the user presses "expand results". */ + scopedEndpoint?: string + expandLabel?: (props: { count: number; expandedCount: number }) => React.ReactNode + options?: Record[] + logic?: LogicWrapper + value?: string + searchAlias?: string + valuesEndpoint?: (key: string) => string + getName: (instance: any) => string + getValue: (instance: any) => TaxonomicFilterValue + getPopupHeader: (instance: any) => string + getIcon?: (instance: any) => JSX.Element + groupTypeIndex?: number + getFullDetailUrl?: (instance: any) => string +} diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx new file mode 100644 index 0000000000000..30bc335b215d7 --- /dev/null +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -0,0 +1,368 @@ +import React from 'react' +import { kea } from 'kea' +import { TaxonomicFilterValue, ListStorage } from 'lib/components/TaxonomicFilter/types' +import { UniversalSearchGroup, UniversalSearchGroupType, UniversalSearchLogicProps } from './types' +import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' +import { ActionType, CohortType, EventDefinition, PersonType, PropertyDefinition } from '~/types' +import { cohortsModel } from '~/models/cohortsModel' +import { actionsModel } from '~/models/actionsModel' +import { teamLogic } from 'scenes/teamLogic' +import { groupsModel } from '~/models/groupsModel' +import { groupPropertiesModel } from '~/models/groupPropertiesModel' +import { capitalizeFirstLetter, pluralize, toParams } from 'lib/utils' +import { combineUrl } from 'kea-router' +import { ActionStack, CohortIcon } from 'lib/components/icons' +import { keyMapping } from 'lib/components/PropertyKeyInfo' +import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' + +import { universalSearchLogicType } from './universalSearchLogicType' +const eventTaxonomicGroupProps: Pick = { + getPopupHeader: (eventDefinition: EventDefinition): string => { + if (!!keyMapping.event[eventDefinition.name]) { + return 'Verified Event' + } + return `${eventDefinition.verified ? 'Verified' : 'Unverified'} Event` + }, + getIcon: getEventDefinitionIcon, +} + +const propertyTaxonomicGroupProps = ( + verified: boolean = false +): Pick => ({ + getPopupHeader: (propertyDefinition: PropertyDefinition): string => { + if (verified || !!keyMapping.event[propertyDefinition.name]) { + return 'Verified Property' + } + return 'Property' + }, + getIcon: getPropertyDefinitionIcon, +}) + +export const universalSearchLogic = kea({ + path: (key) => ['lib', 'components', 'UniversalSearch', 'universalSearchLogic', key], + props: {} as UniversalSearchLogicProps, + key: (props) => `${props.universalSearchLogicKey}`, + connect: { + values: [ + teamLogic, + ['currentTeamId'], + groupsModel, + ['groupTypes', 'aggregationLabel'], + groupPropertiesModel, + ['allGroupProperties'], + ], + }, + actions: () => ({ + moveUp: true, + moveDown: true, + selectSelected: (onComplete?: () => void) => ({ onComplete }), + enableMouseInteractions: true, + tabLeft: true, + tabRight: true, + setSearchQuery: (searchQuery: string) => ({ searchQuery }), + setActiveTab: (activeTab: UniversalSearchGroupType) => ({ activeTab }), + selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: any) => ({ + group, + value, + item, + }), + infiniteListResultsReceived: (groupType: UniversalSearchGroupType, results: ListStorage) => ({ + groupType, + results, + }), + }), + + reducers: ({ selectors }) => ({ + searchQuery: [ + '', + { + setSearchQuery: (_, { searchQuery }) => searchQuery, + }, + ], + activeTab: [ + (state: any): UniversalSearchGroupType => { + return selectors.groupType(state) || selectors.taxonomicGroupTypes(state)[0] + }, + { + setActiveTab: (_, { activeTab }) => activeTab, + }, + ], + mouseInteractionsEnabled: [ + // This fixes a bug with keyboard up/down scrolling when the mouse is over the list. + // Otherwise shifting list elements cause the "hover" action to be triggered randomly. + true, + { + moveUp: () => false, + moveDown: () => false, + setActiveTab: () => true, + enableMouseInteractions: () => true, + }, + ], + }), + + // NB, don't change to the async "selectors: (logic) => {}", as this causes a white screen when infiniteListLogic-s + // connect to taxonomicFilterLogic to select their initial values. They won't be built yet and will be unknown. + selectors: { + universalSerchLogicKey: [ + () => [(_, props) => props.universalSerchLogicKey], + (universalSerchLogicKey) => universalSerchLogicKey, + ], + eventNames: [() => [(_, props) => props.eventNames], (eventNames) => eventNames ?? []], + taxonomicGroups: [ + (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups, selectors.eventNames], + (teamId, groupAnalyticsTaxonomicGroups, eventNames): UniversalSearchGroup[] => [ + { + name: 'Events', + searchPlaceholder: 'events', + type: UniversalSearchGroupType.Events, + endpoint: `api/projects/${teamId}/event_definitions`, + getName: (eventDefinition: EventDefinition) => eventDefinition.name, + getValue: (eventDefinition: EventDefinition) => eventDefinition.name, + ...eventTaxonomicGroupProps, + }, + { + name: 'Actions', + searchPlaceholder: 'actions', + type: UniversalSearchGroupType.Actions, + logic: actionsModel, + value: 'actions', + getName: (action: ActionType) => action.name || '', + getValue: (action: ActionType) => action.id, + getPopupHeader: () => 'Action', + getIcon: function _getIcon(): JSX.Element { + return + }, + }, + { + name: 'Event properties', + searchPlaceholder: 'event properties', + type: UniversalSearchGroupType.EventProperties, + endpoint: combineUrl( + `api/projects/${teamId}/property_definitions`, + eventNames.length > 0 ? { event_names: eventNames } : {} + ).url, + scopedEndpoint: + eventNames.length > 0 + ? combineUrl(`api/projects/${teamId}/property_definitions`, { + event_names: eventNames, + is_event_property: true, + }).url + : undefined, + expandLabel: ({ count, expandedCount }) => + `Show ${pluralize(expandedCount - count, 'property', 'properties')} that ${pluralize( + eventNames.length, + 'has', + 'have', + false + )}n't been seen with ${pluralize(eventNames.length, 'this event', 'these events', false)}`, + getName: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, + getValue: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, + ...propertyTaxonomicGroupProps(), + }, + { + name: 'Numerical event properties', + searchPlaceholder: 'numerical event properties', + type: UniversalSearchGroupType.NumericalEventProperties, + endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { + is_numerical: true, + event_names: eventNames, + }).url, + getName: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, + getValue: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, + ...propertyTaxonomicGroupProps(), + }, + { + name: 'Persons', + searchPlaceholder: 'persons', + type: UniversalSearchGroupType.Persons, + // endpoint: `api/person` + endpoint: `api/projects/${teamId}/persons/`, + getName: (person: PersonType) => person.name || 'Anon user?', + getValue: (person: PersonType) => person.distinct_ids[0], + ...propertyTaxonomicGroupProps, + }, + { + name: 'Cohorts', + searchPlaceholder: 'cohorts', + type: UniversalSearchGroupType.Cohorts, + logic: cohortsModel, + value: 'cohorts', + getName: (cohort: CohortType) => cohort.name || `Cohort ${cohort.id}`, + getValue: (cohort: CohortType) => cohort.id, + getPopupHeader: (cohort: CohortType) => `${cohort.is_static ? 'Static' : 'Dynamic'} Cohort`, + getIcon: function _getIcon(): JSX.Element { + return + }, + }, + ...groupAnalyticsTaxonomicGroups, + ], + ], + activeTaxonomicGroup: [ + (s) => [s.activeTab, s.taxonomicGroups], + (activeTab, taxonomicGroups) => taxonomicGroups.find((g) => g.type === activeTab), + ], + taxonomicGroupTypes: [ + (selectors) => [(_, props) => props.taxonomicGroupTypes, selectors.taxonomicGroups], + (groupTypes, taxonomicGroups): UniversalSearchGroupType[] => + groupTypes || taxonomicGroups.map((g) => g.type), + ], + groupAnalyticsTaxonomicGroups: [ + (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], + (groupTypes, teamId, aggregationLabel): UniversalSearchGroup[] => + groupTypes.map((type) => ({ + name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular)} properties`, + searchPlaceholder: `${aggregationLabel(type.group_type_index).singular} properties`, + type: `${UniversalSearchGroupType.GroupsPrefix}_${type.group_type_index}` as UniversalSearchGroupType, + logic: groupPropertiesModel, + value: `groupProperties_${type.group_type_index}`, + valuesEndpoint: (key) => + `api/projects/${teamId}/groups/property_values/?${toParams({ + key, + group_type_index: type.group_type_index, + })}`, + getName: () => capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular), + getValue: (group) => group.name, + getPopupHeader: () => `Property`, + getIcon: getPropertyDefinitionIcon, + groupTypeIndex: type.group_type_index, + })), + ], + infiniteListLogics: [ + (s) => [s.taxonomicGroupTypes, (_, props) => props], + (taxonomicGroupTypes, props): Record> => + Object.fromEntries( + taxonomicGroupTypes.map((groupType) => [ + groupType, + searchListLogic.build({ + ...props, + listGroupType: groupType, + }), + ]) + ), + ], + infiniteListCounts: [ + (s) => [ + (state, props) => + Object.fromEntries( + Object.entries(s.infiniteListLogics(state, props)).map(([groupType, logic]) => [ + groupType, + logic.isMounted() ? logic.selectors.totalListCount(state, logic.props) : 0, + ]) + ), + ], + (infiniteListCounts) => infiniteListCounts, + ], + value: [() => [(_, props) => props.value], (value) => value], + groupType: [() => [(_, props) => props.groupType], (groupType) => groupType], + currentTabIndex: [ + (s) => [s.taxonomicGroupTypes, s.activeTab], + (groupTypes, activeTab) => Math.max(groupTypes.indexOf(activeTab || ''), 0), + ], + searchPlaceholder: [ + (s) => [s.taxonomicGroups, s.taxonomicGroupTypes], + (allTaxonomicGroups, searchGroupTypes) => { + if (searchGroupTypes.length > 1) { + searchGroupTypes = searchGroupTypes.filter( + (type) => !type.startsWith(UniversalSearchGroupType.GroupsPrefix) + ) + } + const names = searchGroupTypes.map((type) => { + const taxonomicGroup = allTaxonomicGroups.find( + (tGroup) => tGroup.type == type + ) as UniversalSearchGroup + return taxonomicGroup.searchPlaceholder + }) + return names + .map( + (name, index) => + `${index !== 0 ? (index === searchGroupTypes.length - 1 ? ' or ' : ', ') : ''}${name}` + ) + .join('') + }, + ], + }, + listeners: ({ actions, values, props }) => ({ + selectItem: ({ group, value, item }) => { + if (item && value) { + props.onChange?.(group, value, item) + } + actions.setSearchQuery('') + }, + + moveUp: async (_, breakpoint) => { + if (values.activeTab) { + searchListLogic({ + ...props, + listGroupType: values.activeTab, + }).actions.moveUp() + } + await breakpoint(100) + actions.enableMouseInteractions() + }, + + moveDown: async (_, breakpoint) => { + if (values.activeTab) { + searchListLogic({ + ...props, + listGroupType: values.activeTab, + }).actions.moveDown() + } + await breakpoint(100) + actions.enableMouseInteractions() + }, + + selectSelected: async (_, breakpoint) => { + if (values.activeTab) { + searchListLogic({ + ...props, + listGroupType: values.activeTab, + }).actions.selectSelected() + } + await breakpoint(100) + actions.enableMouseInteractions() + }, + + tabLeft: () => { + const { currentTabIndex, taxonomicGroupTypes, infiniteListCounts } = values + for (let i = 1; i < taxonomicGroupTypes.length; i++) { + const newIndex = (currentTabIndex - i + taxonomicGroupTypes.length) % taxonomicGroupTypes.length + if (infiniteListCounts[taxonomicGroupTypes[newIndex]] > 0) { + actions.setActiveTab(taxonomicGroupTypes[newIndex]) + return + } + } + }, + + tabRight: () => { + const { currentTabIndex, taxonomicGroupTypes, infiniteListCounts } = values + for (let i = 1; i < taxonomicGroupTypes.length; i++) { + const newIndex = (currentTabIndex + i) % taxonomicGroupTypes.length + if (infiniteListCounts[taxonomicGroupTypes[newIndex]] > 0) { + actions.setActiveTab(taxonomicGroupTypes[newIndex]) + return + } + } + }, + + setSearchQuery: () => { + const { activeTaxonomicGroup, infiniteListCounts } = values + + // Taxonomic group with a local data source, zero results after searching. + // Open the next tab. + if ( + activeTaxonomicGroup && + !activeTaxonomicGroup.endpoint && + infiniteListCounts[activeTaxonomicGroup.type] === 0 + ) { + actions.tabRight() + } + }, + + infiniteListResultsReceived: ({ groupType, results }) => { + // Open the next tab if no results on an active tab. + if (groupType === values.activeTab && !results.count && !results.expandedCount) { + actions.tabRight() + } + }, + }), +}) From b599564bfa4a1d85b12cd2cb6e913f5438d8e530 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 30 Mar 2022 17:07:02 +0000 Subject: [PATCH 03/18] add more things to search --- .../src/layout/navigation/TopBar/TopBar.tsx | 13 +++- .../components/UniversalSearch/searchList.tsx | 8 +-- .../UniversalSearch/searchListLogic.ts | 9 ++- .../lib/components/UniversalSearch/types.ts | 1 + .../UniversalSearch/universalSearchLogic.tsx | 65 ++++++++++++++----- 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index f2aa4ac43a8a3..b9c583722b6cf 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -49,10 +49,15 @@ export function TopBar(): JSX.Element { groupType={UniversalSearchGroupType.Events} groupTypes={[ UniversalSearchGroupType.Events, - UniversalSearchGroupType.EventProperties, + // UniversalSearchGroupType.EventProperties, UniversalSearchGroupType.Persons, UniversalSearchGroupType.Actions, UniversalSearchGroupType.Cohorts, + UniversalSearchGroupType.Insights, + UniversalSearchGroupType.FeatureFlags, + // 'groups_0', + 'groups_1', + // 'groups_2', ]} onChange={(value, groupType, item) => { console.log('new values:::', value, groupType, item) @@ -91,6 +96,12 @@ export function TopBar(): JSX.Element { router.actions.push(urls.cohort(value)) } else if (groupType === UniversalSearchGroupType.Persons) { router.actions.push(urls.person(value)) + } else if (groupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) { + router.actions.push(urls.group(item.groupTypeIndex, value)) + } else if (groupType === UniversalSearchGroupType.Insights) { + router.actions.push(urls.insightView(value)) + } else if (groupType === UniversalSearchGroupType.FeatureFlags) { + router.actions.push(urls.featureFlag(value)) } }} /> diff --git a/frontend/src/lib/components/UniversalSearch/searchList.tsx b/frontend/src/lib/components/UniversalSearch/searchList.tsx index d33b5690c1195..3404554042154 100644 --- a/frontend/src/lib/components/UniversalSearch/searchList.tsx +++ b/frontend/src/lib/components/UniversalSearch/searchList.tsx @@ -124,10 +124,10 @@ const renderItemContents = ({ return listGroupType === UniversalSearchGroupType.EventProperties || listGroupType === UniversalSearchGroupType.NumericalEventProperties || - listGroupType === UniversalSearchGroupType.PersonProperties || - listGroupType === UniversalSearchGroupType.Events || + listGroupType === UniversalSearchGroupType.Persons || // listGroupType === UniversalSearchGroupType.CustomEvents || - listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix) ? ( + // listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix) || + listGroupType === UniversalSearchGroupType.Events ? ( <>
{featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon} @@ -148,7 +148,7 @@ const renderItemContents = ({ ) : ( <> {featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon} - {item.name ?? ''} + {(item.name || group.getName(item)) ?? ''} )}
diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts index 581f7e35e82a8..d264482b90490 100644 --- a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts +++ b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts @@ -10,6 +10,7 @@ import { SearchListLogicProps, UniversalSearchGroup } from 'lib/components/Unive import { universalSearchLogic } from './universalSearchLogic' import { searchListLogicType } from './searchListLogicType' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' /* by default the pop-up starts open for the first item in the list this can be used with actions.setIndex to allow a caller to override that @@ -62,7 +63,13 @@ export const searchListLogic = kea({ key: (props) => `${props.universalSearchLogicKey}-${props.listGroupType}`, connect: (props: SearchListLogicProps) => ({ - values: [universalSearchLogic(props), ['searchQuery', 'value', 'groupType', 'taxonomicGroups']], + // TODO: had to connect FF to get the model loaded for filtering + values: [ + universalSearchLogic(props), + ['searchQuery', 'value', 'groupType', 'taxonomicGroups'], + featureFlagsLogic, + ['featureFlags'], + ], actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], }), diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index aafb53546c4a5..4ee609624bc88 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -40,6 +40,7 @@ export enum UniversalSearchGroupType { // Wildcards = 'wildcard', GroupsPrefix = 'groups', FeatureFlags = 'feature_flags', + Insights = 'insights', Experiments = 'experiments', } diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index 30bc335b215d7..00534edf0ca2d 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -3,19 +3,30 @@ import { kea } from 'kea' import { TaxonomicFilterValue, ListStorage } from 'lib/components/TaxonomicFilter/types' import { UniversalSearchGroup, UniversalSearchGroupType, UniversalSearchLogicProps } from './types' import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' -import { ActionType, CohortType, EventDefinition, PersonType, PropertyDefinition } from '~/types' +import { + ActionType, + CohortType, + EventDefinition, + FeatureFlagType, + Group, + InsightModel, + PersonType, + PropertyDefinition, +} from '~/types' import { cohortsModel } from '~/models/cohortsModel' import { actionsModel } from '~/models/actionsModel' import { teamLogic } from 'scenes/teamLogic' import { groupsModel } from '~/models/groupsModel' import { groupPropertiesModel } from '~/models/groupPropertiesModel' -import { capitalizeFirstLetter, pluralize, toParams } from 'lib/utils' +import { capitalizeFirstLetter, pluralize } from 'lib/utils' import { combineUrl } from 'kea-router' import { ActionStack, CohortIcon } from 'lib/components/icons' import { keyMapping } from 'lib/components/PropertyKeyInfo' import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' import { universalSearchLogicType } from './universalSearchLogicType' +import { groupDisplayId } from 'scenes/persons/GroupActorHeader' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' const eventTaxonomicGroupProps: Pick = { getPopupHeader: (eventDefinition: EventDefinition): string => { if (!!keyMapping.event[eventDefinition.name]) { @@ -175,11 +186,23 @@ export const universalSearchLogic = kea({ name: 'Persons', searchPlaceholder: 'persons', type: UniversalSearchGroupType.Persons, - // endpoint: `api/person` endpoint: `api/projects/${teamId}/persons/`, getName: (person: PersonType) => person.name || 'Anon user?', getValue: (person: PersonType) => person.distinct_ids[0], - ...propertyTaxonomicGroupProps, + //TODO: Fix! + getPopupHeader: (person: PersonType) => `${person.is_static ? 'Static' : 'Dynamic'} Cohort`, + }, + { + name: 'Insights', + searchPlaceholder: 'insights', + type: UniversalSearchGroupType.Insights, + endpoint: combineUrl(`api/projects/${teamId}/insights/`, { + saved: true, + }).url, + getName: (insight: InsightModel) => insight.name, + getValue: (insight: InsightModel) => insight.short_id, + //TODO: Fix! + getPopupHeader: (person: PersonType) => `${person.is_static ? 'Static' : 'Dynamic'} Cohort`, }, { name: 'Cohorts', @@ -194,6 +217,19 @@ export const universalSearchLogic = kea({ return }, }, + { + name: 'Feature Flags', + searchPlaceholder: 'feature flags', + type: UniversalSearchGroupType.FeatureFlags, + logic: featureFlagsLogic, + value: 'featureFlags', + getName: (featureFlag: FeatureFlagType) => featureFlag.name || featureFlag.key, + getValue: (featureFlag: FeatureFlagType) => featureFlag.id || '', + getPopupHeader: () => 'Feature Flag', + // getIcon: function _getIcon(): JSX.Element { + // return + // }, + }, ...groupAnalyticsTaxonomicGroups, ], ], @@ -210,18 +246,15 @@ export const universalSearchLogic = kea({ (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], (groupTypes, teamId, aggregationLabel): UniversalSearchGroup[] => groupTypes.map((type) => ({ - name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular)} properties`, - searchPlaceholder: `${aggregationLabel(type.group_type_index).singular} properties`, - type: `${UniversalSearchGroupType.GroupsPrefix}_${type.group_type_index}` as UniversalSearchGroupType, - logic: groupPropertiesModel, - value: `groupProperties_${type.group_type_index}`, - valuesEndpoint: (key) => - `api/projects/${teamId}/groups/property_values/?${toParams({ - key, - group_type_index: type.group_type_index, - })}`, - getName: () => capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular), - getValue: (group) => group.name, + name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).plural)}`, + searchPlaceholder: `${aggregationLabel(type.group_type_index).plural}`, + type: `${UniversalSearchGroupType.GroupsPrefix}_${type.group_type_index}` as unknown as UniversalSearchGroupType, + endpoint: combineUrl(`api/projects/${teamId}/groups/`, { + group_type_index: type.group_type_index, + }).url, + searchAlias: 'group_key', + getName: (group: Group) => groupDisplayId(group.group_key, group.group_properties), + getValue: (group: Group) => group.group_key, getPopupHeader: () => `Property`, getIcon: getPropertyDefinitionIcon, groupTypeIndex: type.group_type_index, From fc96aaa2ad090a0ebdabdfe5d6b99614e1397267 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 30 Mar 2022 18:21:46 +0000 Subject: [PATCH 04/18] make plugins smooth --- .../src/layout/navigation/TopBar/TopBar.tsx | 8 +++++ .../UniversalSearch/searchListLogic.ts | 6 ++++ .../lib/components/UniversalSearch/types.ts | 1 + .../UniversalSearch/universalSearchLogic.tsx | 24 +++++++++++++++ frontend/src/scenes/plugins/pluginsLogic.ts | 29 +++++++++++++++++++ 5 files changed, 68 insertions(+) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index b9c583722b6cf..93dc65acb4b4f 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -55,6 +55,8 @@ export function TopBar(): JSX.Element { UniversalSearchGroupType.Cohorts, UniversalSearchGroupType.Insights, UniversalSearchGroupType.FeatureFlags, + UniversalSearchGroupType.Plugins, + UniversalSearchGroupType.Experiments, // 'groups_0', 'groups_1', // 'groups_2', @@ -102,6 +104,12 @@ export function TopBar(): JSX.Element { router.actions.push(urls.insightView(value)) } else if (groupType === UniversalSearchGroupType.FeatureFlags) { router.actions.push(urls.featureFlag(value)) + } else if (groupType === UniversalSearchGroupType.Experiments) { + router.actions.push(urls.experiment(value)) + } else if (groupType === UniversalSearchGroupType.Plugins) { + router.actions.push( + combineUrl(urls.plugins(), { tab: item.tab, name: item.name }).url + ) } }} /> diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts index d264482b90490..0a6c725f7d205 100644 --- a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts +++ b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts @@ -11,6 +11,8 @@ import { universalSearchLogic } from './universalSearchLogic' import { searchListLogicType } from './searchListLogicType' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' /* by default the pop-up starts open for the first item in the list this can be used with actions.setIndex to allow a caller to override that @@ -69,6 +71,10 @@ export const searchListLogic = kea({ ['searchQuery', 'value', 'groupType', 'taxonomicGroups'], featureFlagsLogic, ['featureFlags'], + experimentsLogic, + ['experiments'], + pluginsLogic, + ['plugins'], ], actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], }), diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index 4ee609624bc88..b604f40b5322f 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -42,6 +42,7 @@ export enum UniversalSearchGroupType { FeatureFlags = 'feature_flags', Insights = 'insights', Experiments = 'experiments', + Plugins = 'plugins', } export interface UniversalSearchGroup { diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index 00534edf0ca2d..e0beec3cf36d9 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -7,10 +7,12 @@ import { ActionType, CohortType, EventDefinition, + Experiment, FeatureFlagType, Group, InsightModel, PersonType, + PluginType, PropertyDefinition, } from '~/types' import { cohortsModel } from '~/models/cohortsModel' @@ -27,6 +29,8 @@ import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-m import { universalSearchLogicType } from './universalSearchLogicType' import { groupDisplayId } from 'scenes/persons/GroupActorHeader' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' const eventTaxonomicGroupProps: Pick = { getPopupHeader: (eventDefinition: EventDefinition): string => { if (!!keyMapping.event[eventDefinition.name]) { @@ -230,6 +234,26 @@ export const universalSearchLogic = kea({ // return // }, }, + { + name: 'Experiments', + searchPlaceholder: 'experiments', + type: UniversalSearchGroupType.Experiments, + logic: experimentsLogic, + value: 'experiments', + getName: (experiment: Experiment) => experiment.name, + getValue: (experiment: Experiment) => experiment.id, + getPopupHeader: () => 'Experiment', + }, + { + name: 'Plugins', + searchPlaceholder: 'plugins', + type: UniversalSearchGroupType.Plugins, + logic: pluginsLogic, + value: 'allPossiblePlugins', + getName: (plugin: Pick) => plugin.name, + getValue: (plugin: Pick) => plugin.name, + getPopupHeader: () => 'Plugin', + }, ...groupAnalyticsTaxonomicGroups, ], ], diff --git a/frontend/src/scenes/plugins/pluginsLogic.ts b/frontend/src/scenes/plugins/pluginsLogic.ts index 60275247919b9..47c36fea344e0 100644 --- a/frontend/src/scenes/plugins/pluginsLogic.ts +++ b/frontend/src/scenes/plugins/pluginsLogic.ts @@ -639,6 +639,24 @@ export const pluginsLogic = kea>({ return pluginNameToMaintainerMap }, ], + allPossiblePlugins: [ + (s) => [s.repository, s.plugins], + (repository, plugins) => { + const allPossiblePlugins: { name: string; url?: string; tab: PluginTab }[] = [] + for (const plugin of Object.values(plugins) as PluginType[]) { + allPossiblePlugins.push({ name: plugin.name, url: plugin.url, tab: PluginTab.Installed }) + } + + const installedUrls = new Set(Object.values(plugins).map((plugin) => plugin.url)) + + for (const plugin of Object.values(repository) as PluginRepositoryEntry[]) { + if (!installedUrls.has(plugin.url)) { + allPossiblePlugins.push({ name: plugin.name, url: plugin.url, tab: PluginTab.Repository }) + } + } + return allPossiblePlugins + }, + ], }, listeners: ({ actions, values }) => ({ @@ -707,6 +725,17 @@ export const pluginsLogic = kea>({ } }, }), + + urlToAction: ({ actions }) => ({ + '/project/plugins': (url, { tab, name }) => { + console.log('got values: ', tab, name, url) + if (tab && name) { + actions.setSearchTerm(name) + actions.setPluginTab(tab as PluginTab) + } + }, + }), + events: ({ actions }) => ({ afterMount: () => { actions.loadPlugins() From 9d5ab67c5911c11343cc9b48bc8999cb7ff0f5b3 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 30 Mar 2022 20:27:15 +0000 Subject: [PATCH 05/18] add dashboards --- frontend/src/layout/navigation/TopBar/TopBar.tsx | 3 +++ frontend/src/lib/components/UniversalSearch/types.ts | 1 + .../UniversalSearch/universalSearchLogic.tsx | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 93dc65acb4b4f..20858745729f3 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -57,6 +57,7 @@ export function TopBar(): JSX.Element { UniversalSearchGroupType.FeatureFlags, UniversalSearchGroupType.Plugins, UniversalSearchGroupType.Experiments, + UniversalSearchGroupType.Dashboards, // 'groups_0', 'groups_1', // 'groups_2', @@ -110,6 +111,8 @@ export function TopBar(): JSX.Element { router.actions.push( combineUrl(urls.plugins(), { tab: item.tab, name: item.name }).url ) + } else if (groupType === UniversalSearchGroupType.Dashboards) { + router.actions.push(urls.dashboard(value)) } }} /> diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index b604f40b5322f..c6599b78e5703 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -43,6 +43,7 @@ export enum UniversalSearchGroupType { Insights = 'insights', Experiments = 'experiments', Plugins = 'plugins', + Dashboards = 'dashboards', } export interface UniversalSearchGroup { diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index e0beec3cf36d9..a8e4b0d510d0c 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -6,6 +6,7 @@ import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' import { ActionType, CohortType, + DashboardType, EventDefinition, Experiment, FeatureFlagType, @@ -31,6 +32,7 @@ import { groupDisplayId } from 'scenes/persons/GroupActorHeader' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { experimentsLogic } from 'scenes/experiments/experimentsLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { dashboardsModel } from '~/models/dashboardsModel' const eventTaxonomicGroupProps: Pick = { getPopupHeader: (eventDefinition: EventDefinition): string => { if (!!keyMapping.event[eventDefinition.name]) { @@ -254,6 +256,16 @@ export const universalSearchLogic = kea({ getValue: (plugin: Pick) => plugin.name, getPopupHeader: () => 'Plugin', }, + { + name: 'Dashboards', + searchPlaceholder: 'dashboards', + type: UniversalSearchGroupType.Dashboards, + logic: dashboardsModel, + value: 'nameSortedDashboards', + getName: (dashboard: DashboardType) => dashboard.name, + getValue: (dashboard: DashboardType) => dashboard.id, + getPopupHeader: () => 'Dashboard', + }, ...groupAnalyticsTaxonomicGroups, ], ], From 07baa43447f120dade5fc122d963363935b559d9 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 31 Mar 2022 12:11:50 +0000 Subject: [PATCH 06/18] clean internals up --- .../{InfiniteList.scss => SearchList.scss} | 0 ...iteSelectResults.tsx => SearchResults.tsx} | 13 +- .../UniversalSearch/UniversalSearch.tsx | 4 +- .../UniversalSearch/UniversalSearchPopup.tsx | 4 +- .../components/UniversalSearch/searchList.tsx | 333 +----------------- .../UniversalSearch/searchListLogic.ts | 88 +---- .../lib/components/UniversalSearch/types.ts | 35 +- .../UniversalSearch/universalSearchLogic.tsx | 126 ++----- 8 files changed, 103 insertions(+), 500 deletions(-) rename frontend/src/lib/components/UniversalSearch/{InfiniteList.scss => SearchList.scss} (100%) rename frontend/src/lib/components/UniversalSearch/{InfiniteSelectResults.tsx => SearchResults.tsx} (89%) diff --git a/frontend/src/lib/components/UniversalSearch/InfiniteList.scss b/frontend/src/lib/components/UniversalSearch/SearchList.scss similarity index 100% rename from frontend/src/lib/components/UniversalSearch/InfiniteList.scss rename to frontend/src/lib/components/UniversalSearch/SearchList.scss diff --git a/frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx similarity index 89% rename from frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx rename to frontend/src/lib/components/UniversalSearch/SearchResults.tsx index 7d7afaf7a333d..3e13351cb9c30 100644 --- a/frontend/src/lib/components/UniversalSearch/InfiniteSelectResults.tsx +++ b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx @@ -7,7 +7,7 @@ import { SearchList } from 'lib/components/UniversalSearch/searchList' import { UniversalSearchGroupType, UniversalSearchLogicProps } from './types' import clsx from 'clsx' -export interface InfiniteSelectResultsProps { +export interface SearchResultsProps { focusInput: () => void universalSearchLogicProps: UniversalSearchLogicProps } @@ -25,12 +25,10 @@ function CategoryPill({ }): JSX.Element { const logic = searchListLogic({ ...universalSearchLogicProps, listGroupType: groupType }) const { taxonomicGroups } = useValues(universalSearchLogic) - const { totalResultCount, totalListCount } = useValues(logic) + const { totalResultCount } = useValues(logic) const group = taxonomicGroups.find((g) => g.type === groupType) - - // :TRICKY: use `totalListCount` (results + extra) to toggle interactivity, while showing `totalResultCount` - const canInteract = totalListCount > 0 + const canInteract = totalResultCount > 0 return (
- + ) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index b39f3920400bd..fc1d5dca808e8 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { LemonButtonWithPopupProps } from '../LemonButton' import { TaxonomicFilterValue } from '../TaxonomicFilter/types' -import { UniversalSearchGroupType } from './types' +import { SearchDefinitionTypes, UniversalSearchGroupType } from './types' import { Popup } from 'lib/components/Popup/Popup' import { UniversalSearch } from './UniversalSearch' import { Button } from 'antd' @@ -12,7 +12,7 @@ export interface UniversalSearchPopupProps extends Omit { groupType: UniversalSearchGroupType value?: ValueType - onChange: (value: ValueType, groupType: UniversalSearchGroupType) => void + onChange: (value: ValueType, groupType: UniversalSearchGroupType, item: SearchDefinitionTypes) => void groupTypes?: UniversalSearchGroupType[] renderValue?: (value: ValueType) => JSX.Element diff --git a/frontend/src/lib/components/UniversalSearch/searchList.tsx b/frontend/src/lib/components/UniversalSearch/searchList.tsx index 3404554042154..c059e9c02d431 100644 --- a/frontend/src/lib/components/UniversalSearch/searchList.tsx +++ b/frontend/src/lib/components/UniversalSearch/searchList.tsx @@ -1,58 +1,20 @@ -import './InfiniteList.scss' +import './SearchList.scss' import '../Popup/Popup.scss' -import React, { useState } from 'react' +import React from 'react' import { Empty, Skeleton, Tag } from 'antd' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' -import { - getKeyMapping, - PropertyKeyDescription, - PropertyKeyInfo, - PropertyKeyTitle, -} from 'lib/components/PropertyKeyInfo' -import { BindLogic, Provider, useActions, useValues } from 'kea' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { useActions, useValues } from 'kea' import { searchListLogic, NO_ITEM_SELECTED } from './searchListLogic' import { universalSearchLogic } from './universalSearchLogic' -import { - TaxonomicDefinitionTypes, - // UniversalSearchGroup, - // UniversalSearchGroupType, -} from 'lib/components/TaxonomicFilter/types' -import ReactDOM from 'react-dom' -import { usePopper } from 'react-popper' -import { ActionType, CohortType, EventDefinition, KeyMapping, PropertyDefinition } from '~/types' -import { AimOutlined } from '@ant-design/icons' -import { Link } from 'lib/components/Link' -import { ActionSelectInfo } from 'scenes/insights/ActionSelectInfo' -import { urls } from 'scenes/urls' +import { EventDefinition, PersonType } from '~/types' import { dayjs } from 'lib/dayjs' import { FEATURE_FLAGS, STALE_EVENT_SECONDS } from 'lib/constants' import { Tooltip } from '../Tooltip' import clsx from 'clsx' import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' -import { definitionPopupLogic } from 'lib/components/DefinitionPopup/definitionPopupLogic' -import { ControlledDefinitionPopupContents } from 'lib/components/DefinitionPopup/DefinitionPopupContents' -import { pluralize } from 'lib/utils' -import { UniversalSearchGroup, UniversalSearchGroupType } from './types' - -enum ListTooltip { - None = 0, - Left = 1, - Right = 2, -} - -export function tooltipDesiredState(element?: Element | null): ListTooltip { - let desiredState: ListTooltip = ListTooltip.None - const rect = element?.getBoundingClientRect() - if (rect) { - if (window.innerWidth - rect.right > 300) { - desiredState = ListTooltip.Right - } else if (rect.left > 300) { - desiredState = ListTooltip.Left - } - } - return desiredState -} +import { SearchDefinitionTypes, UniversalSearchGroup, UniversalSearchGroupType } from './types' const staleIndicator = (parsedLastSeen: dayjs.Dayjs | null): JSX.Element => { return ( @@ -68,252 +30,54 @@ const staleIndicator = (parsedLastSeen: dayjs.Dayjs | null): JSX.Element => { ) } -const unusedIndicator = (eventNames: string[]): JSX.Element => { - return ( - - This property has not been seen on{' '} - {eventNames ? ( - <> - the event{eventNames.length > 1 ? 's' : ''}{' '} - {eventNames.map((e, index) => ( - <> - {index === 0 ? '' : index === eventNames.length - 1 ? ' and ' : ', '} - "{e}" - - ))} - - ) : ( - 'this event' - )} - , but has been seen on other events. - - } - > - Not seen - - ) -} - const renderItemContents = ({ item, listGroupType, group, featureFlags, - eventNames, }: { - item: TaxonomicDefinitionTypes + item: SearchDefinitionTypes listGroupType: UniversalSearchGroupType group: UniversalSearchGroup featureFlags: FeatureFlagsSet - eventNames: string[] }): JSX.Element | string => { const parsedLastSeen = (item as EventDefinition).last_seen_at ? dayjs((item as EventDefinition).last_seen_at) : null const isStale = (listGroupType === UniversalSearchGroupType.Events && !parsedLastSeen) || dayjs().diff(parsedLastSeen, 'seconds') > STALE_EVENT_SECONDS - const isUnusedEventProperty = - (listGroupType === UniversalSearchGroupType.NumericalEventProperties || - listGroupType === UniversalSearchGroupType.EventProperties) && - (item as PropertyDefinition).is_event_property !== null && - !(item as PropertyDefinition).is_event_property - - const icon =
{group.getIcon?.(item)}
- - return listGroupType === UniversalSearchGroupType.EventProperties || - listGroupType === UniversalSearchGroupType.NumericalEventProperties || - listGroupType === UniversalSearchGroupType.Persons || - // listGroupType === UniversalSearchGroupType.CustomEvents || - // listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix) || - listGroupType === UniversalSearchGroupType.Events ? ( + return listGroupType === UniversalSearchGroupType.Persons || listGroupType === UniversalSearchGroupType.Events ? ( <>
- {featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon}
{isStale && staleIndicator(parsedLastSeen)} - {isUnusedEventProperty && unusedIndicator(eventNames)} ) : ( -
- {listGroupType === UniversalSearchGroupType.Elements ? ( - - ) : ( - <> - {featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] && icon} - {(item.name || group.getName(item)) ?? ''} - - )} -
- ) -} - -const renderItemPopupWithoutTaxonomy = ( - item: PropertyDefinition | CohortType | ActionType, - listGroupType: UniversalSearchGroupType, - group: UniversalSearchGroup -): JSX.Element | string => { - const width = 265 - let data: KeyMapping | null = null - const value = group.getValue(item) - - if (value) { - if (listGroupType === UniversalSearchGroupType.Actions && 'id' in item) { - return ( -
- Actions - - edit - -
-

- -

- {item && } -
- ) - } - - if ( - // NB: also update "selectedItemHasPopup" below - listGroupType === UniversalSearchGroupType.Events || - listGroupType === UniversalSearchGroupType.EventProperties || - listGroupType === UniversalSearchGroupType.NumericalEventProperties || - listGroupType === UniversalSearchGroupType.PersonProperties - ) { - data = getKeyMapping(value.toString(), 'event') - } else if (listGroupType === UniversalSearchGroupType.Elements) { - data = getKeyMapping(value.toString(), 'element') - } - - if (data) { - return ( -
- - {data.description ?
: null} - - {'volume_30_day' in item && (item.volume_30_day || 0) > 0 ? ( -

- Seen {item.volume_30_day} times.{' '} -

- ) : null} - {'query_usage_30_day' in item && (item.query_usage_30_day || 0) > 0 ? ( -

- Used in {item.query_usage_30_day} queries. -

- ) : null} -
- ) - } - } - - return item.name ?? '' -} - -const selectedItemHasPopup = ( - item?: TaxonomicDefinitionTypes, - listGroupType?: UniversalSearchGroupType, - group?: UniversalSearchGroup, - showNewPopups: boolean = false -): boolean => { - if (showNewPopups) { - return ( - // NB: also update "renderItemPopup" above - !!item && - !!group?.getValue(item) && - !!listGroupType && - ([ - UniversalSearchGroupType.Actions, - // UniversalSearchGroupType.Elements, - UniversalSearchGroupType.Events, - // UniversalSearchGroupType.CustomEvents, - UniversalSearchGroupType.EventProperties, - UniversalSearchGroupType.NumericalEventProperties, - UniversalSearchGroupType.PersonProperties, - UniversalSearchGroupType.Cohorts, - // UniversalSearchGroupType.CohortsWithAllUsers, - ].includes(listGroupType) || - listGroupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) - ) - } - - return ( - // NB: also update "renderItemPopup" above - !!item && - !!group?.getValue(item) && - (listGroupType === UniversalSearchGroupType.Actions || - ((listGroupType === UniversalSearchGroupType.Events || - // listGroupType === UniversalSearchGroupType.Elements || - listGroupType === UniversalSearchGroupType.EventProperties || - listGroupType === UniversalSearchGroupType.NumericalEventProperties || - listGroupType === UniversalSearchGroupType.PersonProperties) && - !!getKeyMapping(group?.getValue(item), 'event'))) + <>{(item.name || group.getName(item)) ?? ''} ) } export function SearchList(): JSX.Element { - const { mouseInteractionsEnabled, activeTab, searchQuery, value, groupType, eventNames } = - useValues(universalSearchLogic) + const { mouseInteractionsEnabled, searchQuery, value, groupType } = useValues(universalSearchLogic) const { selectItem } = useActions(universalSearchLogic) const { featureFlags } = useValues(featureFlagLogic) - const { - isLoading, - results, - index, - listGroupType, - group, - selectedItem, - selectedItemInView, - isExpandable, - totalResultCount, - totalListCount, - expandedCount, - showPopover, - } = useValues(searchListLogic) - const { onRowsRendered, setIndex, expand, updateRemoteItem } = useActions(searchListLogic) - - const isActiveTab = listGroupType === activeTab - const showEmptyState = totalListCount === 0 && !isLoading - const showNewPopups = !!featureFlags[FEATURE_FLAGS.DATA_MANAGEMENT] + const { isLoading, results, index, listGroupType, group, totalResultCount, showPopover } = + useValues(searchListLogic) + const { onRowsRendered, setIndex } = useActions(searchListLogic) - const [referenceElement, setReferenceElement] = useState(null) - const [popperElement, setPopperElement] = useState(null) - - const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, { - placement: 'right', - modifiers: [ - { - name: 'offset', - options: { - offset: [0, 10], - }, - }, - { - name: 'preventOverflow', - options: { - padding: 10, - }, - }, - ], - }) + const showEmptyState = totalResultCount === 0 && !isLoading const renderItem: ListRowRenderer = ({ index: rowIndex, style }: ListRowProps): JSX.Element | null => { const item = results[rowIndex] const itemValue = item ? group?.getValue?.(item) : null const isSelected = listGroupType === groupType && itemValue === value - const isHighlighted = rowIndex === index && isActiveTab const commonDivProps: React.HTMLProps = { key: `item_${rowIndex}`, @@ -326,7 +90,6 @@ export function SearchList(): JSX.Element { // if the popover is not enabled then don't leave the row selected when the mouse leaves it onMouseLeave: () => (mouseInteractionsEnabled && !showPopover ? setIndex(NO_ITEM_SELECTED) : null), style: style, - ref: isHighlighted ? setReferenceElement : null, } return item && group ? ( @@ -340,24 +103,8 @@ export function SearchList(): JSX.Element { listGroupType, group, featureFlags, - eventNames, })} - ) : !item && rowIndex === totalListCount - 1 && isExpandable && !isLoading ? ( -
- {group.expandLabel?.({ count: totalResultCount, expandedCount }) ?? - `Click here to see ${expandedCount - totalResultCount} more ${pluralize( - expandedCount - totalResultCount, - 'row', - 'rows', - false - )}`} -
) : (
)} - {isActiveTab && - selectedItemInView && - selectedItemHasPopup(selectedItem, listGroupType, group, showNewPopups) && - tooltipDesiredState(referenceElement) !== ListTooltip.None && - showPopover ? ( - - {ReactDOM.createPortal( - selectedItem && group ? ( - showNewPopups ? ( - - - - ) : ( -
- {renderItemPopupWithoutTaxonomy( - selectedItem as PropertyDefinition | CohortType | ActionType, - listGroupType, - group - )} -
- ) - ) : null, - document.querySelector('body') as HTMLElement - )} -
- ) : null}
) } diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts index 0a6c725f7d205..2ed77d9e0cecc 100644 --- a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts +++ b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts @@ -5,8 +5,13 @@ import { RenderedRows } from 'react-virtualized/dist/es/List' import { CohortType, EventDefinition } from '~/types' import Fuse from 'fuse.js' -import { ListFuse, ListStorage, LoaderOptions, TaxonomicDefinitionTypes } from 'lib/components/TaxonomicFilter/types' -import { SearchListLogicProps, UniversalSearchGroup } from 'lib/components/UniversalSearch/types' +import { ListFuse, LoaderOptions } from 'lib/components/TaxonomicFilter/types' +import { + SearchDefinitionTypes, + SearchListLogicProps, + UniversalSearchGroup, + ListStorage, +} from 'lib/components/UniversalSearch/types' import { universalSearchLogic } from './universalSearchLogic' import { searchListLogicType } from './searchListLogicType' @@ -39,8 +44,8 @@ const createEmptyListStorage = (searchQuery = '', first = false): ListStorage => // simple cache with a setTimeout expiry const API_CACHE_TIMEOUT = 60000 -let apiCache: Record = {} -let apiCacheTimers: Record = {} +const apiCache: Record = {} +const apiCacheTimers: Record = {} async function fetchCachedListResponse(path: string, searchParams: Record): Promise { const url = combineUrl(path, searchParams).url @@ -76,7 +81,7 @@ export const searchListLogic = kea({ pluginsLogic, ['plugins'], ], - actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], + actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'searchListResultsReceived']], }), actions: { @@ -87,8 +92,6 @@ export const searchListLogic = kea({ setLimit: (limit: number) => ({ limit }), onRowsRendered: (rowInfo: RenderedRows) => ({ rowInfo }), loadRemoteItems: (options: LoaderOptions) => options, - updateRemoteItem: (item: TaxonomicDefinitionTypes) => ({ item }), - expand: true, }, reducers: ({ props }) => ({ @@ -108,7 +111,6 @@ export const searchListLogic = kea({ ], startIndex: [0, { onRowsRendered: (_, { rowInfo: { startIndex } }) => startIndex }], stopIndex: [0, { onRowsRendered: (_, { rowInfo: { stopIndex } }) => stopIndex }], - isExpanded: [false, { expand: () => true }], }), loaders: ({ values }) => ({ @@ -125,7 +127,7 @@ export const searchListLogic = kea({ await breakpoint(1) } - const { isExpanded, remoteEndpoint, scopedRemoteEndpoint, searchQuery } = values + const { remoteEndpoint, searchQuery } = values if (!remoteEndpoint) { // should not have been here in the first place! @@ -138,21 +140,7 @@ export const searchListLogic = kea({ offset, } - const [response, expandedCountResponse] = await Promise.all([ - // get the list of results - fetchCachedListResponse( - scopedRemoteEndpoint && !isExpanded ? scopedRemoteEndpoint : remoteEndpoint, - searchParams - ), - // if this is an unexpanded scoped list, get the count for the normafull list - scopedRemoteEndpoint && !isExpanded - ? fetchCachedListResponse(remoteEndpoint, { - ...searchParams, - limit: 1, - offset: 0, - }) - : null, - ]) + const response = await fetchCachedListResponse(remoteEndpoint, searchParams) breakpoint() const queryChanged = values.items.searchQuery !== values.searchQuery @@ -166,16 +154,6 @@ export const searchListLogic = kea({ searchQuery: values.searchQuery, queryChanged, count: response.count || (response.results || []).length, - expandedCount: expandedCountResponse?.count, - } - }, - updateRemoteItem: ({ item }) => { - // On updating item, invalidate cache - apiCache = {} - apiCacheTimers = {} - return { - ...values.remoteItems, - results: values.remoteItems.results.map((i) => (i.name === item.name ? item : i)), } }, }, @@ -205,25 +183,18 @@ export const searchListLogic = kea({ } }, moveUp: () => { - const { index, totalListCount } = values - actions.setIndex((index - 1 + totalListCount) % totalListCount) + const { index, totalResultCount } = values + actions.setIndex((index - 1 + totalResultCount) % totalResultCount) }, moveDown: () => { - const { index, totalListCount } = values - actions.setIndex((index + 1) % totalListCount) + const { index, totalResultCount } = values + actions.setIndex((index + 1) % totalResultCount) }, selectSelected: () => { - if (values.isExpandableButtonSelected) { - actions.expand() - } else { - actions.selectItem(values.group, values.selectedItemValue, values.selectedItem) - } + actions.selectItem(values.group, values.selectedItemValue, values.selectedItem) }, loadRemoteItemsSuccess: ({ remoteItems }) => { - actions.infiniteListResultsReceived(props.listGroupType, remoteItems) - }, - expand: () => { - actions.loadRemoteItems({ offset: values.index, limit: values.limit }) + actions.searchListResultsReceived(props.listGroupType, remoteItems) }, }), @@ -236,21 +207,6 @@ export const searchListLogic = kea({ taxonomicGroups.find((g) => g.type === listGroupType) as UniversalSearchGroup, ], remoteEndpoint: [(s) => [s.group], (group) => group?.endpoint || null], - scopedRemoteEndpoint: [(s) => [s.group], (group) => group?.scopedEndpoint || null], - isExpandable: [ - (s) => [s.remoteEndpoint, s.scopedRemoteEndpoint, s.remoteItems], - (remoteEndpoint, scopedRemoteEndpoint, remoteItems) => - !!( - remoteEndpoint && - scopedRemoteEndpoint && - remoteItems.expandedCount && - remoteItems.expandedCount > remoteItems.count - ), - ], - isExpandableButtonSelected: [ - (s) => [s.isExpandable, s.index, s.totalListCount], - (isExpandable, index, totalListCount) => isExpandable && index === totalListCount - 1, - ], isRemoteDataSource: [(s) => [s.remoteEndpoint], (remoteEndpoint) => !!remoteEndpoint], rawLocalItems: [ (selectors) => [ @@ -307,16 +263,10 @@ export const searchListLogic = kea({ (isRemoteDataSource, remoteItems, localItems) => (isRemoteDataSource ? remoteItems : localItems), ], totalResultCount: [(s) => [s.items], (items) => items.count || 0], - totalExtraCount: [(s) => [s.isExpandable], (isExpandable) => (isExpandable ? 1 : 0)], - totalListCount: [ - (s) => [s.totalResultCount, s.totalExtraCount], - (totalResultCount, totalExtraCount) => totalResultCount + totalExtraCount, - ], - expandedCount: [(s) => [s.items], (items) => items.expandedCount || 0], results: [(s) => [s.items], (items) => items.results], selectedItem: [ (s) => [s.index, s.items], - (index, items): TaxonomicDefinitionTypes | undefined => (index >= 0 ? items.results[index] : undefined), + (index, items): SearchDefinitionTypes | undefined => (index >= 0 ? items.results[index] : undefined), ], selectedItemValue: [ (s) => [s.selectedItem, s.group], diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index c6599b78e5703..0cca0e2af0340 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -1,4 +1,15 @@ import { LogicWrapper } from 'kea' +import { + ActionType, + CohortType, + EventDefinition, + Experiment, + FeatureFlagType, + GroupType, + InsightType, + PersonType, + PluginType, +} from '~/types' import { SimpleOption, TaxonomicFilterValue } from '../TaxonomicFilter/types' export interface UniversalSearchLogicProps extends UniversalSearchProps { @@ -9,6 +20,26 @@ export interface SearchListLogicProps extends UniversalSearchLogicProps { listGroupType: UniversalSearchGroupType } +export type SearchDefinitionTypes = + | EventDefinition + | CohortType + | ActionType + | Experiment + | PersonType + | GroupType + | FeatureFlagType + | InsightType + | PluginType + +export interface ListStorage { + results: SearchDefinitionTypes[] + searchQuery?: string // Query used for the results currently in state + count: number + expandedCount?: number + queryChanged?: boolean + first?: boolean +} + export interface UniversalSearchProps { groupType?: UniversalSearchGroupType value?: TaxonomicFilterValue @@ -61,8 +92,8 @@ export interface UniversalSearchGroup { valuesEndpoint?: (key: string) => string getName: (instance: any) => string getValue: (instance: any) => TaxonomicFilterValue - getPopupHeader: (instance: any) => string - getIcon?: (instance: any) => JSX.Element + // getPopupHeader: (instance: any) => string + // getIcon?: (instance: any) => JSX.Element groupTypeIndex?: number getFullDetailUrl?: (instance: any) => string } diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index a8e4b0d510d0c..b12672c9dd6f4 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -1,7 +1,12 @@ -import React from 'react' import { kea } from 'kea' -import { TaxonomicFilterValue, ListStorage } from 'lib/components/TaxonomicFilter/types' -import { UniversalSearchGroup, UniversalSearchGroupType, UniversalSearchLogicProps } from './types' +import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { + SearchDefinitionTypes, + UniversalSearchGroup, + UniversalSearchGroupType, + UniversalSearchLogicProps, + ListStorage, +} from './types' import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' import { ActionType, @@ -14,18 +19,14 @@ import { InsightModel, PersonType, PluginType, - PropertyDefinition, } from '~/types' import { cohortsModel } from '~/models/cohortsModel' import { actionsModel } from '~/models/actionsModel' import { teamLogic } from 'scenes/teamLogic' import { groupsModel } from '~/models/groupsModel' import { groupPropertiesModel } from '~/models/groupPropertiesModel' -import { capitalizeFirstLetter, pluralize } from 'lib/utils' +import { capitalizeFirstLetter } from 'lib/utils' import { combineUrl } from 'kea-router' -import { ActionStack, CohortIcon } from 'lib/components/icons' -import { keyMapping } from 'lib/components/PropertyKeyInfo' -import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' import { universalSearchLogicType } from './universalSearchLogicType' import { groupDisplayId } from 'scenes/persons/GroupActorHeader' @@ -33,27 +34,6 @@ import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { experimentsLogic } from 'scenes/experiments/experimentsLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { dashboardsModel } from '~/models/dashboardsModel' -const eventTaxonomicGroupProps: Pick = { - getPopupHeader: (eventDefinition: EventDefinition): string => { - if (!!keyMapping.event[eventDefinition.name]) { - return 'Verified Event' - } - return `${eventDefinition.verified ? 'Verified' : 'Unverified'} Event` - }, - getIcon: getEventDefinitionIcon, -} - -const propertyTaxonomicGroupProps = ( - verified: boolean = false -): Pick => ({ - getPopupHeader: (propertyDefinition: PropertyDefinition): string => { - if (verified || !!keyMapping.event[propertyDefinition.name]) { - return 'Verified Property' - } - return 'Property' - }, - getIcon: getPropertyDefinitionIcon, -}) export const universalSearchLogic = kea({ path: (key) => ['lib', 'components', 'UniversalSearch', 'universalSearchLogic', key], @@ -78,12 +58,12 @@ export const universalSearchLogic = kea({ tabRight: true, setSearchQuery: (searchQuery: string) => ({ searchQuery }), setActiveTab: (activeTab: UniversalSearchGroupType) => ({ activeTab }), - selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: any) => ({ + selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: SearchDefinitionTypes) => ({ group, value, item, }), - infiniteListResultsReceived: (groupType: UniversalSearchGroupType, results: ListStorage) => ({ + searchListResultsReceived: (groupType: UniversalSearchGroupType, results: ListStorage) => ({ groupType, results, }), @@ -124,10 +104,9 @@ export const universalSearchLogic = kea({ () => [(_, props) => props.universalSerchLogicKey], (universalSerchLogicKey) => universalSerchLogicKey, ], - eventNames: [() => [(_, props) => props.eventNames], (eventNames) => eventNames ?? []], taxonomicGroups: [ - (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups, selectors.eventNames], - (teamId, groupAnalyticsTaxonomicGroups, eventNames): UniversalSearchGroup[] => [ + (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups], + (teamId, groupAnalyticsTaxonomicGroups): UniversalSearchGroup[] => [ { name: 'Events', searchPlaceholder: 'events', @@ -135,7 +114,6 @@ export const universalSearchLogic = kea({ endpoint: `api/projects/${teamId}/event_definitions`, getName: (eventDefinition: EventDefinition) => eventDefinition.name, getValue: (eventDefinition: EventDefinition) => eventDefinition.name, - ...eventTaxonomicGroupProps, }, { name: 'Actions', @@ -145,48 +123,6 @@ export const universalSearchLogic = kea({ value: 'actions', getName: (action: ActionType) => action.name || '', getValue: (action: ActionType) => action.id, - getPopupHeader: () => 'Action', - getIcon: function _getIcon(): JSX.Element { - return - }, - }, - { - name: 'Event properties', - searchPlaceholder: 'event properties', - type: UniversalSearchGroupType.EventProperties, - endpoint: combineUrl( - `api/projects/${teamId}/property_definitions`, - eventNames.length > 0 ? { event_names: eventNames } : {} - ).url, - scopedEndpoint: - eventNames.length > 0 - ? combineUrl(`api/projects/${teamId}/property_definitions`, { - event_names: eventNames, - is_event_property: true, - }).url - : undefined, - expandLabel: ({ count, expandedCount }) => - `Show ${pluralize(expandedCount - count, 'property', 'properties')} that ${pluralize( - eventNames.length, - 'has', - 'have', - false - )}n't been seen with ${pluralize(eventNames.length, 'this event', 'these events', false)}`, - getName: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, - getValue: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, - ...propertyTaxonomicGroupProps(), - }, - { - name: 'Numerical event properties', - searchPlaceholder: 'numerical event properties', - type: UniversalSearchGroupType.NumericalEventProperties, - endpoint: combineUrl(`api/projects/${teamId}/property_definitions`, { - is_numerical: true, - event_names: eventNames, - }).url, - getName: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, - getValue: (propertyDefinition: PropertyDefinition) => propertyDefinition.name, - ...propertyTaxonomicGroupProps(), }, { name: 'Persons', @@ -195,8 +131,6 @@ export const universalSearchLogic = kea({ endpoint: `api/projects/${teamId}/persons/`, getName: (person: PersonType) => person.name || 'Anon user?', getValue: (person: PersonType) => person.distinct_ids[0], - //TODO: Fix! - getPopupHeader: (person: PersonType) => `${person.is_static ? 'Static' : 'Dynamic'} Cohort`, }, { name: 'Insights', @@ -207,8 +141,6 @@ export const universalSearchLogic = kea({ }).url, getName: (insight: InsightModel) => insight.name, getValue: (insight: InsightModel) => insight.short_id, - //TODO: Fix! - getPopupHeader: (person: PersonType) => `${person.is_static ? 'Static' : 'Dynamic'} Cohort`, }, { name: 'Cohorts', @@ -218,10 +150,6 @@ export const universalSearchLogic = kea({ value: 'cohorts', getName: (cohort: CohortType) => cohort.name || `Cohort ${cohort.id}`, getValue: (cohort: CohortType) => cohort.id, - getPopupHeader: (cohort: CohortType) => `${cohort.is_static ? 'Static' : 'Dynamic'} Cohort`, - getIcon: function _getIcon(): JSX.Element { - return - }, }, { name: 'Feature Flags', @@ -231,10 +159,6 @@ export const universalSearchLogic = kea({ value: 'featureFlags', getName: (featureFlag: FeatureFlagType) => featureFlag.name || featureFlag.key, getValue: (featureFlag: FeatureFlagType) => featureFlag.id || '', - getPopupHeader: () => 'Feature Flag', - // getIcon: function _getIcon(): JSX.Element { - // return - // }, }, { name: 'Experiments', @@ -244,7 +168,6 @@ export const universalSearchLogic = kea({ value: 'experiments', getName: (experiment: Experiment) => experiment.name, getValue: (experiment: Experiment) => experiment.id, - getPopupHeader: () => 'Experiment', }, { name: 'Plugins', @@ -254,7 +177,6 @@ export const universalSearchLogic = kea({ value: 'allPossiblePlugins', getName: (plugin: Pick) => plugin.name, getValue: (plugin: Pick) => plugin.name, - getPopupHeader: () => 'Plugin', }, { name: 'Dashboards', @@ -264,7 +186,15 @@ export const universalSearchLogic = kea({ value: 'nameSortedDashboards', getName: (dashboard: DashboardType) => dashboard.name, getValue: (dashboard: DashboardType) => dashboard.id, - getPopupHeader: () => 'Dashboard', + }, + { + name: 'Dashboards', + searchPlaceholder: 'dashboards', + type: UniversalSearchGroupType.Dashboards, + logic: dashboardsModel, + value: 'nameSortedDashboards', + getName: (dashboard: DashboardType) => dashboard.name, + getValue: (dashboard: DashboardType) => dashboard.id, }, ...groupAnalyticsTaxonomicGroups, ], @@ -291,12 +221,10 @@ export const universalSearchLogic = kea({ searchAlias: 'group_key', getName: (group: Group) => groupDisplayId(group.group_key, group.group_properties), getValue: (group: Group) => group.group_key, - getPopupHeader: () => `Property`, - getIcon: getPropertyDefinitionIcon, groupTypeIndex: type.group_type_index, })), ], - infiniteListLogics: [ + searchListLogics: [ (s) => [s.taxonomicGroupTypes, (_, props) => props], (taxonomicGroupTypes, props): Record> => Object.fromEntries( @@ -313,9 +241,9 @@ export const universalSearchLogic = kea({ (s) => [ (state, props) => Object.fromEntries( - Object.entries(s.infiniteListLogics(state, props)).map(([groupType, logic]) => [ + Object.entries(s.searchListLogics(state, props)).map(([groupType, logic]) => [ groupType, - logic.isMounted() ? logic.selectors.totalListCount(state, logic.props) : 0, + logic.isMounted() ? logic.selectors.totalResultCount(state, logic.props) : 0, ]) ), ], @@ -427,9 +355,9 @@ export const universalSearchLogic = kea({ } }, - infiniteListResultsReceived: ({ groupType, results }) => { + searchListResultsReceived: ({ groupType, results }) => { // Open the next tab if no results on an active tab. - if (groupType === values.activeTab && !results.count && !results.expandedCount) { + if (groupType === values.activeTab && !results.count) { actions.tabRight() } }, From bbc01b75c9adbf89e7054c507ea749710a225e61 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 31 Mar 2022 12:32:41 +0000 Subject: [PATCH 07/18] search group types --- .../src/layout/navigation/TopBar/TopBar.tsx | 3 +- .../UniversalSearch/SearchResults.tsx | 18 +++--- .../UniversalSearch/UniversalSearch.tsx | 14 ++--- .../UniversalSearch/UniversalSearchPopup.tsx | 4 +- .../UniversalSearch/searchListLogic.ts | 12 ++-- .../lib/components/UniversalSearch/types.ts | 5 +- .../UniversalSearch/universalSearchLogic.tsx | 55 ++++++++----------- 7 files changed, 49 insertions(+), 62 deletions(-) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 20858745729f3..bc83664dad7bd 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -63,10 +63,9 @@ export function TopBar(): JSX.Element { // 'groups_2', ]} onChange={(value, groupType, item) => { - console.log('new values:::', value, groupType, item) if (groupType === UniversalSearchGroupType.Events) { // Go to Insights instead? - router.actions.push(combineUrl(urls.events(), { eventFilter: value }).url) + // router.actions.push(combineUrl(urls.events(), { eventFilter: value }).url) router.actions.push( combineUrl( urls.insightNew({ diff --git a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx index 3e13351cb9c30..c9b0d4d0afd9b 100644 --- a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx +++ b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx @@ -24,10 +24,10 @@ function CategoryPill({ onClick: () => void }): JSX.Element { const logic = searchListLogic({ ...universalSearchLogicProps, listGroupType: groupType }) - const { taxonomicGroups } = useValues(universalSearchLogic) + const { searchGroups } = useValues(universalSearchLogic) const { totalResultCount } = useValues(logic) - const group = taxonomicGroups.find((g) => g.type === groupType) + const group = searchGroups.find((g) => g.type === groupType) const canInteract = totalResultCount > 0 return ( @@ -44,26 +44,26 @@ function CategoryPill({ } export function SearchResults({ focusInput, universalSearchLogicProps }: SearchResultsProps): JSX.Element { - const { activeTab, taxonomicGroups, taxonomicGroupTypes } = useValues(universalSearchLogic) + const { activeTab, searchGroups, searchGroupTypes } = useValues(universalSearchLogic) const { setActiveTab } = useActions(universalSearchLogic) - if (taxonomicGroupTypes.length === 1) { + if (searchGroupTypes.length === 1) { return ( ) } - const openTab = activeTab || taxonomicGroups[0].type + const openTab = activeTab || searchGroups[0].type return ( <>
Categories
- {taxonomicGroupTypes.map((groupType) => { + {searchGroupTypes.map((groupType) => { return (
- {taxonomicGroups.find((g) => g.type === openTab)?.name || openTab} + {searchGroups.find((g) => g.type === openTab)?.name || openTab}
- {taxonomicGroupTypes.map((groupType) => { + {searchGroupTypes.map((groupType) => { return (
taxonomicFilterLogicKeyInput || `universal-search-${uniqueMemoizedIndex++}`, - [taxonomicFilterLogicKeyInput] + () => universalSearchFilterLogicKey || `universal-search-${uniqueMemoizedIndex++}`, + [universalSearchFilterLogicKey] ) const searchInputRef = useRef(null) @@ -39,9 +38,8 @@ export function UniversalSearch({ groupType, value, onChange, - taxonomicGroupTypes, + searchGroupTypes, optionsFromProp, - eventNames, popoverEnabled, selectFirstItem, } @@ -64,7 +62,7 @@ export function UniversalSearch({
} visible={visible} diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts index 2ed77d9e0cecc..8829ad4157410 100644 --- a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts +++ b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts @@ -73,7 +73,7 @@ export const searchListLogic = kea({ // TODO: had to connect FF to get the model loaded for filtering values: [ universalSearchLogic(props), - ['searchQuery', 'value', 'groupType', 'taxonomicGroups'], + ['searchQuery', 'value', 'groupType', 'searchGroups'], featureFlagsLogic, ['featureFlags'], experimentsLogic, @@ -202,17 +202,17 @@ export const searchListLogic = kea({ listGroupType: [() => [(_, props) => props.listGroupType], (listGroupType) => listGroupType], isLoading: [(s) => [s.remoteItemsLoading], (remoteItemsLoading) => remoteItemsLoading], group: [ - (s) => [s.listGroupType, s.taxonomicGroups], - (listGroupType, taxonomicGroups): UniversalSearchGroup => - taxonomicGroups.find((g) => g.type === listGroupType) as UniversalSearchGroup, + (s) => [s.listGroupType, s.searchGroups], + (listGroupType, searchGroups): UniversalSearchGroup => + searchGroups.find((g) => g.type === listGroupType) as UniversalSearchGroup, ], remoteEndpoint: [(s) => [s.group], (group) => group?.endpoint || null], isRemoteDataSource: [(s) => [s.remoteEndpoint], (remoteEndpoint) => !!remoteEndpoint], rawLocalItems: [ (selectors) => [ (state, props) => { - const taxonomicGroups = selectors.taxonomicGroups(state) - const group = taxonomicGroups.find((g) => g.type === props.listGroupType) + const searchGroups = selectors.searchGroups(state) + const group = searchGroups.find((g) => g.type === props.listGroupType) if (group?.logic && group?.value) { return group.logic.selectors[group.value]?.(state) || null } diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index 0cca0e2af0340..afef9f95fb081 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -45,10 +45,9 @@ export interface UniversalSearchProps { value?: TaxonomicFilterValue onChange?: (group: UniversalSearchGroup, value: TaxonomicFilterValue, item: any) => void onClose?: () => void - taxonomicGroupTypes: UniversalSearchGroupType[] - taxonomicFilterLogicKey?: string + searchGroupTypes: UniversalSearchGroupType[] + // taxonomicFilterLogicKey?: string optionsFromProp?: Partial> - eventNames?: string[] height?: number width?: number popoverEnabled?: boolean diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index b12672c9dd6f4..deb88cca8befa 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -1,12 +1,6 @@ import { kea } from 'kea' import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' -import { - SearchDefinitionTypes, - UniversalSearchGroup, - UniversalSearchGroupType, - UniversalSearchLogicProps, - ListStorage, -} from './types' +import { UniversalSearchGroup, UniversalSearchGroupType, UniversalSearchLogicProps, ListStorage } from './types' import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' import { ActionType, @@ -58,7 +52,7 @@ export const universalSearchLogic = kea({ tabRight: true, setSearchQuery: (searchQuery: string) => ({ searchQuery }), setActiveTab: (activeTab: UniversalSearchGroupType) => ({ activeTab }), - selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: SearchDefinitionTypes) => ({ + selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: any) => ({ group, value, item, @@ -78,7 +72,7 @@ export const universalSearchLogic = kea({ ], activeTab: [ (state: any): UniversalSearchGroupType => { - return selectors.groupType(state) || selectors.taxonomicGroupTypes(state)[0] + return selectors.groupType(state) || selectors.searchGroupTypes(state)[0] }, { setActiveTab: (_, { activeTab }) => activeTab, @@ -104,7 +98,7 @@ export const universalSearchLogic = kea({ () => [(_, props) => props.universalSerchLogicKey], (universalSerchLogicKey) => universalSerchLogicKey, ], - taxonomicGroups: [ + searchGroups: [ (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups], (teamId, groupAnalyticsTaxonomicGroups): UniversalSearchGroup[] => [ { @@ -200,13 +194,12 @@ export const universalSearchLogic = kea({ ], ], activeTaxonomicGroup: [ - (s) => [s.activeTab, s.taxonomicGroups], - (activeTab, taxonomicGroups) => taxonomicGroups.find((g) => g.type === activeTab), + (s) => [s.activeTab, s.searchGroups], + (activeTab, searchGroups) => searchGroups.find((g) => g.type === activeTab), ], - taxonomicGroupTypes: [ - (selectors) => [(_, props) => props.taxonomicGroupTypes, selectors.taxonomicGroups], - (groupTypes, taxonomicGroups): UniversalSearchGroupType[] => - groupTypes || taxonomicGroups.map((g) => g.type), + searchGroupTypes: [ + (selectors) => [(_, props) => props.searchGroupTypes, selectors.searchGroups], + (groupTypes, searchGroups): UniversalSearchGroupType[] => groupTypes || searchGroups.map((g) => g.type), ], groupAnalyticsTaxonomicGroups: [ (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], @@ -225,10 +218,10 @@ export const universalSearchLogic = kea({ })), ], searchListLogics: [ - (s) => [s.taxonomicGroupTypes, (_, props) => props], - (taxonomicGroupTypes, props): Record> => + (s) => [s.searchGroupTypes, (_, props) => props], + (searchGroupTypes, props): Record> => Object.fromEntries( - taxonomicGroupTypes.map((groupType) => [ + searchGroupTypes.map((groupType) => [ groupType, searchListLogic.build({ ...props, @@ -252,11 +245,11 @@ export const universalSearchLogic = kea({ value: [() => [(_, props) => props.value], (value) => value], groupType: [() => [(_, props) => props.groupType], (groupType) => groupType], currentTabIndex: [ - (s) => [s.taxonomicGroupTypes, s.activeTab], + (s) => [s.searchGroupTypes, s.activeTab], (groupTypes, activeTab) => Math.max(groupTypes.indexOf(activeTab || ''), 0), ], searchPlaceholder: [ - (s) => [s.taxonomicGroups, s.taxonomicGroupTypes], + (s) => [s.searchGroups, s.searchGroupTypes], (allTaxonomicGroups, searchGroupTypes) => { if (searchGroupTypes.length > 1) { searchGroupTypes = searchGroupTypes.filter( @@ -320,22 +313,22 @@ export const universalSearchLogic = kea({ }, tabLeft: () => { - const { currentTabIndex, taxonomicGroupTypes, infiniteListCounts } = values - for (let i = 1; i < taxonomicGroupTypes.length; i++) { - const newIndex = (currentTabIndex - i + taxonomicGroupTypes.length) % taxonomicGroupTypes.length - if (infiniteListCounts[taxonomicGroupTypes[newIndex]] > 0) { - actions.setActiveTab(taxonomicGroupTypes[newIndex]) + const { currentTabIndex, searchGroupTypes, infiniteListCounts } = values + for (let i = 1; i < searchGroupTypes.length; i++) { + const newIndex = (currentTabIndex - i + searchGroupTypes.length) % searchGroupTypes.length + if (infiniteListCounts[searchGroupTypes[newIndex]] > 0) { + actions.setActiveTab(searchGroupTypes[newIndex]) return } } }, tabRight: () => { - const { currentTabIndex, taxonomicGroupTypes, infiniteListCounts } = values - for (let i = 1; i < taxonomicGroupTypes.length; i++) { - const newIndex = (currentTabIndex + i) % taxonomicGroupTypes.length - if (infiniteListCounts[taxonomicGroupTypes[newIndex]] > 0) { - actions.setActiveTab(taxonomicGroupTypes[newIndex]) + const { currentTabIndex, searchGroupTypes, infiniteListCounts } = values + for (let i = 1; i < searchGroupTypes.length; i++) { + const newIndex = (currentTabIndex + i) % searchGroupTypes.length + if (infiniteListCounts[searchGroupTypes[newIndex]] > 0) { + actions.setActiveTab(searchGroupTypes[newIndex]) return } } From 202c14b8c8e0e35d33314bb95d5cfe27d8e900d4 Mon Sep 17 00:00:00 2001 From: Tim Glaser Date: Thu, 31 Mar 2022 14:10:37 +0000 Subject: [PATCH 08/18] PUSH --- .../src/layout/navigation/TopBar/TopBar.scss | 42 +------ .../src/layout/navigation/TopBar/TopBar.tsx | 4 +- frontend/src/lib/components/Popup/Popup.tsx | 5 +- .../UniversalSearch/UniversalSearch.scss | 49 +++++++++ .../UniversalSearch/UniversalSearch.tsx | 18 +-- .../UniversalSearch/UniversalSearchPopup.tsx | 103 ++++++++++++------ .../lib/components/UniversalSearch/types.ts | 6 +- .../UniversalSearch/universalSearchLogic.tsx | 2 +- 8 files changed, 132 insertions(+), 97 deletions(-) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.scss b/frontend/src/layout/navigation/TopBar/TopBar.scss index 1c8f7bc2676a6..33be943823fb3 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.scss +++ b/frontend/src/layout/navigation/TopBar/TopBar.scss @@ -24,6 +24,9 @@ display: flex; align-items: center; height: 100%; + &--left { + flex-grow: 1; + } &--left > * { margin-right: 1rem; } @@ -92,45 +95,6 @@ } } -.SearchBox { - transition: background-color 200ms ease, border-color 200ms ease; - display: none; - align-items: center; - width: 15rem; - height: 100%; - padding: 0 0.625rem; - border-radius: var(--radius); - border: 1px solid var(--border-dark); - color: var(--text-muted); - user-select: none; - cursor: pointer; - &:hover { - background-color: var(--bg-shaded); - } - &:active { - border-color: var(--border-active); - } - @media screen and (min-width: $sm) { - display: flex; - } -} - -.SearchBox__primary-area { - flex-grow: 1; - display: flex; - align-items: center; -} - -.SearchBox__magnifier { - color: var(--text-default); - font-size: 1rem; - padding-right: 0.5rem; -} - -.SearchBox__keyboard-shortcut { - font-weight: 600; -} - .SitePopover { max-width: 22rem; } diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index bc83664dad7bd..4e36066e957f8 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -3,7 +3,6 @@ import React from 'react' import { FriendlyLogo } from '../../../toolbar/assets/FriendlyLogo' import { SitePopover } from './SitePopover' import { Announcement } from './Announcement' -import { SearchBox } from './SearchBox' import { navigationLogic } from '../navigationLogic' import { HelpButton } from '../../../lib/components/HelpButton/HelpButton' import { CommandPalette } from '../../../lib/components/CommandPalette' @@ -44,7 +43,7 @@ export function TopBar(): JSX.Element { -
+
-
diff --git a/frontend/src/lib/components/Popup/Popup.tsx b/frontend/src/lib/components/Popup/Popup.tsx index e0ab5e43fcac7..d099785900acd 100644 --- a/frontend/src/lib/components/Popup/Popup.tsx +++ b/frontend/src/lib/components/Popup/Popup.tsx @@ -25,6 +25,7 @@ export interface PopupProps { /** Whether the popover's width should be synced with the children's width. */ sameWidth?: boolean className?: string + modifier?: Record } /** 0 means no parent. */ @@ -44,6 +45,7 @@ export function Popup({ className, actionable = false, sameWidth = false, + modifier = {}, }: PopupProps): JSX.Element { const [referenceElement, setReferenceElement] = useState(null) const [popperElement, setPopperElement] = useState(null) @@ -58,7 +60,7 @@ export function Popup({ { name: 'offset', options: { - offset: [0, 4], + offset: [0, -250], }, }, fallbackPlacements @@ -80,6 +82,7 @@ export function Popup({ requires: ['computeStyles'], } : {}, + modifier, ], [] ) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss index 1abc035689a02..a58a909d4ce7d 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss @@ -1,4 +1,53 @@ +@import '~/vars'; + +.SearchBox { + // transition: background-color 200ms ease, border-color 200ms ease; + // display: none; + // align-items: center; + max-width: 483px; + // height: 100%; + // padding: 0 0.625rem; + // border-radius: var(--radius); + // border: 1px solid var(--border-dark); + // color: var(--text-muted); + // user-select: none; + cursor: pointer; + // &:hover { + // background-color: var(--bg-shaded); + // } + // &:active { + // border-color: var(--border-active); + // } + + transition: 200ms ease margin; + .ant-input-affix-wrapper, + input { + background: var(--bg-bridge); + } + + &.SearchBox--sidebar-shown { + margin-left: 55px; + } + @media screen and (min-width: $sm) { + display: flex; + } +} + .universal-search { + .ant-input { + cursor: pointer; + } + + .magnifier-icon { + font-size: 18px; + color: var(--text-muted); + &.magnifier-icon-active { + color: var(--primary); + } + } +} + +.universal-search-popup { width: 550px; max-width: calc(100vw - 40px); background: white; diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx index feac493b027ab..5b8bd489ce568 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx @@ -1,5 +1,5 @@ import './UniversalSearch.scss' -import React, { useEffect, useMemo, useRef } from 'react' +import React, { useEffect, useRef } from 'react' import { Input } from 'antd' import { useValues, useActions, BindLogic } from 'kea' import { SearchResults } from './SearchResults' @@ -7,12 +7,9 @@ import { IconKeyboard, IconMagnifier } from '../icons' import { Tooltip } from '../Tooltip' import clsx from 'clsx' import { universalSearchLogic } from './universalSearchLogic' -import { UniversalSearchLogicProps, UniversalSearchProps } from './types' - -let uniqueMemoizedIndex = 0 +import { UniversalSearchProps } from './types' export function UniversalSearch({ - universalSearchFilterLogicKey, groupType, value, onChange, @@ -25,16 +22,11 @@ export function UniversalSearch({ selectFirstItem = true, }: UniversalSearchProps): JSX.Element { // Generate a unique key for each unique UniversalSearch that's rendered - const universalSearchLogicKey = useMemo( - () => universalSearchFilterLogicKey || `universal-search-${uniqueMemoizedIndex++}`, - [universalSearchFilterLogicKey] - ) const searchInputRef = useRef(null) const focusInput = (): void => searchInputRef.current?.focus() - const universalSearchLogicProps: UniversalSearchLogicProps = { - universalSearchLogicKey, + const universalSearchLogicProps: UniversalSearchProps = { groupType, value, onChange, @@ -61,7 +53,7 @@ export function UniversalSearch({
diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 777e66d1caa7a..c3537d82eb22f 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -4,9 +4,12 @@ import { TaxonomicFilterValue } from '../TaxonomicFilter/types' import { SearchDefinitionTypes, UniversalSearchGroupType } from './types' import { Popup } from 'lib/components/Popup/Popup' import { UniversalSearch } from './UniversalSearch' -import { Button } from 'antd' -import { DownOutlined } from '@ant-design/icons' +import { Input } from 'antd' import clsx from 'clsx' +import { IconMagnifier } from '../icons' +import { navigationLogic } from '~/layout/navigation/navigationLogic' +import { useValues } from 'kea' +import { universalSearchLogic } from './universalSearchLogic' export interface UniversalSearchPopupProps extends Omit { @@ -27,46 +30,76 @@ export function UniversalSearchPopup({ groupType, value, onChange, - renderValue, groupTypes, dataAttr, - placeholder = 'Please select', style, fullWidth = true, }: UniversalSearchPopupProps): JSX.Element { const [visible, setVisible] = useState(false) + const { isSideBarShown } = useValues(navigationLogic) + + const logic = universalSearchLogic() + const { searchQuery, searchPlaceholder } = useValues(logic) + return ( - { - onChange?.(payload, type, item) - setVisible(false) - }} - searchGroupTypes={groupTypes ?? [groupType]} - /> - } - visible={visible} - onClickOutside={() => setVisible(false)} - > - {({ setRef }) => ( - - )} - +
+ { + onChange?.(payload, type, item) + setVisible(false) + }} + searchGroupTypes={groupTypes ?? [groupType]} + /> + } + visible={visible} + placement="right-start" + fallbackPlacements={['bottom']} + onClickOutside={() => setVisible(false)} + modifier={{ + name: 'offset', + options: { + offset: ({ placement }) => { + if (placement === 'right-start') { + return [-10, -249 - 243] + } else { + return [] + } + }, + }, + }} + > + {({ setRef }) => ( +
{ + e.preventDefault() + e.stopPropagation() + setVisible(!visible) + }} + ref={setRef} + className={clsx( + { 'full-width': fullWidth }, + '', + 'SearchBox', + isSideBarShown && 'SearchBox--sidebar-shown' + )} + style={style} + > + } + /> +
+ )} +
+
) } diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index afef9f95fb081..1071dc0032638 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -12,11 +12,7 @@ import { } from '~/types' import { SimpleOption, TaxonomicFilterValue } from '../TaxonomicFilter/types' -export interface UniversalSearchLogicProps extends UniversalSearchProps { - universalSearchLogicKey: string -} - -export interface SearchListLogicProps extends UniversalSearchLogicProps { +export interface SearchListLogicProps extends UniversalSearchProps { listGroupType: UniversalSearchGroupType } diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index deb88cca8befa..b83b5d7918aa5 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -32,7 +32,7 @@ import { dashboardsModel } from '~/models/dashboardsModel' export const universalSearchLogic = kea({ path: (key) => ['lib', 'components', 'UniversalSearch', 'universalSearchLogic', key], props: {} as UniversalSearchLogicProps, - key: (props) => `${props.universalSearchLogicKey}`, + key: () => `universal-search`, connect: { values: [ teamLogic, From 2e39265c6f1acf1fc9010586fa3bffd28e04a053 Mon Sep 17 00:00:00 2001 From: Tim Glaser Date: Thu, 31 Mar 2022 14:24:50 +0000 Subject: [PATCH 09/18] fix props --- .../UniversalSearch/UniversalSearchPopup.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index c3537d82eb22f..62719b3fea161 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { LemonButtonWithPopupProps } from '../LemonButton' import { TaxonomicFilterValue } from '../TaxonomicFilter/types' -import { SearchDefinitionTypes, UniversalSearchGroupType } from './types' +import { SearchDefinitionTypes, UniversalSearchGroupType, UniversalSearchProps } from './types' import { Popup } from 'lib/components/Popup/Popup' import { UniversalSearch } from './UniversalSearch' import { Input } from 'antd' @@ -38,8 +38,19 @@ export function UniversalSearchPopup({ const [visible, setVisible] = useState(false) const { isSideBarShown } = useValues(navigationLogic) - - const logic = universalSearchLogic() + const universalSearchLogicProps: UniversalSearchProps = { + groupType, + value, + onChange: ({ type }, payload, item) => { + onChange?.(payload, type, item) + setVisible(false) + }, + searchGroupTypes: groupTypes ?? [groupType], + optionsFromProp: undefined, + popoverEnabled: true, + selectFirstItem: true, + } + const logic = universalSearchLogic(universalSearchLogicProps) const { searchQuery, searchPlaceholder } = useValues(logic) return ( From 82d2cba8a7b84b6a79a66bd8e432ebc703135277 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 31 Mar 2022 14:28:33 +0000 Subject: [PATCH 10/18] wip --- ee/clickhouse/views/groups.py | 3 +- .../src/layout/navigation/TopBar/TopBar.tsx | 61 +---------------- .../UniversalSearch/UniversalSearchPopup.tsx | 67 ++++++++++++++++++- .../components/UniversalSearch/searchList.tsx | 2 +- .../lib/components/UniversalSearch/types.ts | 14 ++-- .../UniversalSearch/universalSearchLogic.tsx | 11 +-- frontend/src/scenes/plugins/pluginsLogic.ts | 10 ++- 7 files changed, 85 insertions(+), 83 deletions(-) diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 42f48e20adc58..3a4106773672f 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -61,9 +61,8 @@ class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewse queryset = Group.objects.all() pagination_class = GroupCursorPagination permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] - filter_backends = [DjangoFilterBackend] + filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ["group_type_index"] - filter_backends = [filters.SearchFilter] search_fields = ["group_key"] @action(methods=["GET"], detail=False) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index bc83664dad7bd..2019e97992c72 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -16,9 +16,6 @@ import './TopBar.scss' import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' import { UniversalSearchPopup } from 'lib/components/UniversalSearch/UniversalSearchPopup' import { UniversalSearchGroupType } from 'lib/components/UniversalSearch/types' -import { urls } from 'scenes/urls' -import { combineUrl, router } from 'kea-router' -import { ChartDisplayType, InsightType } from '~/types' export function TopBar(): JSX.Element { const { isSideBarShown, bareNav, mobileLayout, isCreateOrganizationModalShown, isCreateProjectModalShown } = @@ -49,7 +46,6 @@ export function TopBar(): JSX.Element { groupType={UniversalSearchGroupType.Events} groupTypes={[ UniversalSearchGroupType.Events, - // UniversalSearchGroupType.EventProperties, UniversalSearchGroupType.Persons, UniversalSearchGroupType.Actions, UniversalSearchGroupType.Cohorts, @@ -58,62 +54,11 @@ export function TopBar(): JSX.Element { UniversalSearchGroupType.Plugins, UniversalSearchGroupType.Experiments, UniversalSearchGroupType.Dashboards, - // 'groups_0', + // UniversalSearchGroupType.GroupsPrefix, + 'groups_0', 'groups_1', - // 'groups_2', + // // 'groups_2', ]} - onChange={(value, groupType, item) => { - if (groupType === UniversalSearchGroupType.Events) { - // Go to Insights instead? - // router.actions.push(combineUrl(urls.events(), { eventFilter: value }).url) - router.actions.push( - combineUrl( - urls.insightNew({ - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - events: [{ id: value, name: value, type: 'events', math: 'dau' }], - }) - ).url - ) - } else if (groupType === UniversalSearchGroupType.Actions) { - router.actions.push( - combineUrl( - urls.insightNew({ - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [ - { - id: item.id, - name: item.name, - type: 'actions', - order: 0, - }, - ], - }) - ).url - ) - } else if (groupType === UniversalSearchGroupType.Cohorts) { - router.actions.push(urls.cohort(value)) - } else if (groupType === UniversalSearchGroupType.Persons) { - router.actions.push(urls.person(value)) - } else if (groupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) { - router.actions.push(urls.group(item.groupTypeIndex, value)) - } else if (groupType === UniversalSearchGroupType.Insights) { - router.actions.push(urls.insightView(value)) - } else if (groupType === UniversalSearchGroupType.FeatureFlags) { - router.actions.push(urls.featureFlag(value)) - } else if (groupType === UniversalSearchGroupType.Experiments) { - router.actions.push(urls.experiment(value)) - } else if (groupType === UniversalSearchGroupType.Plugins) { - router.actions.push( - combineUrl(urls.plugins(), { tab: item.tab, name: item.name }).url - ) - } else if (groupType === UniversalSearchGroupType.Dashboards) { - router.actions.push(urls.dashboard(value)) - } - }} />
diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 777e66d1caa7a..8599730394cdc 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -6,23 +6,83 @@ import { Popup } from 'lib/components/Popup/Popup' import { UniversalSearch } from './UniversalSearch' import { Button } from 'antd' import { DownOutlined } from '@ant-design/icons' +import { combineUrl, router } from 'kea-router' import clsx from 'clsx' +import { urls } from 'scenes/urls' +import { ActionType, ChartDisplayType, Group, InsightModel, InsightType } from '~/types' +import { PluginSelectionType } from 'scenes/plugins/pluginsLogic' export interface UniversalSearchPopupProps extends Omit { groupType: UniversalSearchGroupType value?: ValueType - onChange: (value: ValueType, groupType: UniversalSearchGroupType, item: SearchDefinitionTypes) => void - + onChange?: (value: ValueType, groupType: UniversalSearchGroupType, item: SearchDefinitionTypes) => void groupTypes?: UniversalSearchGroupType[] renderValue?: (value: ValueType) => JSX.Element dataAttr?: string - eventNames?: string[] placeholder?: React.ReactNode dropdownMatchSelectWidth?: boolean allowClear?: boolean } +function redirectOnSelectItems( + value: TaxonomicFilterValue, + groupType: UniversalSearchGroupType, + item: SearchDefinitionTypes +): void { + if (groupType === UniversalSearchGroupType.Events) { + router.actions.push( + combineUrl( + urls.insightNew({ + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + events: [{ id: value, name: value, type: 'events', math: 'dau' }], + }) + ).url + ) + } else if (groupType === UniversalSearchGroupType.Actions) { + router.actions.push( + combineUrl( + urls.insightNew({ + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [ + { + id: (item as ActionType).id, + name: (item as ActionType).name, + type: 'actions', + order: 0, + }, + ], + }) + ).url + ) + } else if (groupType === UniversalSearchGroupType.Cohorts) { + router.actions.push(urls.cohort(value)) + } else if (groupType === UniversalSearchGroupType.Persons) { + router.actions.push(urls.person(String(value))) + } else if (groupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) { + router.actions.push(urls.group((item as Group).group_type_index, String(value))) + } else if (groupType === UniversalSearchGroupType.Insights) { + router.actions.push(urls.insightView((item as InsightModel).short_id)) + } else if (groupType === UniversalSearchGroupType.FeatureFlags) { + router.actions.push(urls.featureFlag(value)) + } else if (groupType === UniversalSearchGroupType.Experiments) { + router.actions.push(urls.experiment(value)) + } else if (groupType === UniversalSearchGroupType.Plugins) { + router.actions.push( + combineUrl(urls.plugins(), { + tab: (item as PluginSelectionType).tab, + name: (item as PluginSelectionType).name, + }).url + ) + } else if (groupType === UniversalSearchGroupType.Dashboards) { + router.actions.push(urls.dashboard(value)) + } +} + export function UniversalSearchPopup({ groupType, value, @@ -43,6 +103,7 @@ export function UniversalSearchPopup({ groupType={groupType} value={value} onChange={({ type }, payload, item) => { + redirectOnSelectItems(payload, type, item) onChange?.(payload, type, item) setVisible(false) }} diff --git a/frontend/src/lib/components/UniversalSearch/searchList.tsx b/frontend/src/lib/components/UniversalSearch/searchList.tsx index c059e9c02d431..fd3dd2290fece 100644 --- a/frontend/src/lib/components/UniversalSearch/searchList.tsx +++ b/frontend/src/lib/components/UniversalSearch/searchList.tsx @@ -59,7 +59,7 @@ const renderItemContents = ({ {isStale && staleIndicator(parsedLastSeen)} ) : ( - <>{(item.name || group.getName(item)) ?? ''} + <>{group.getName(item) ?? ''} ) } diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts index afef9f95fb081..e3d691377495d 100644 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ b/frontend/src/lib/components/UniversalSearch/types.ts @@ -1,14 +1,14 @@ import { LogicWrapper } from 'kea' +import { PluginSelectionType } from 'scenes/plugins/pluginsLogic' import { ActionType, CohortType, EventDefinition, Experiment, FeatureFlagType, - GroupType, - InsightType, + Group, + InsightModel, PersonType, - PluginType, } from '~/types' import { SimpleOption, TaxonomicFilterValue } from '../TaxonomicFilter/types' @@ -26,10 +26,10 @@ export type SearchDefinitionTypes = | ActionType | Experiment | PersonType - | GroupType + | Group | FeatureFlagType - | InsightType - | PluginType + | InsightModel + | PluginSelectionType export interface ListStorage { results: SearchDefinitionTypes[] @@ -46,7 +46,7 @@ export interface UniversalSearchProps { onChange?: (group: UniversalSearchGroup, value: TaxonomicFilterValue, item: any) => void onClose?: () => void searchGroupTypes: UniversalSearchGroupType[] - // taxonomicFilterLogicKey?: string + universalSearchFilterLogicKey?: string optionsFromProp?: Partial> height?: number width?: number diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx index deb88cca8befa..c8b7428bf4bf7 100644 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx @@ -151,7 +151,7 @@ export const universalSearchLogic = kea({ type: UniversalSearchGroupType.FeatureFlags, logic: featureFlagsLogic, value: 'featureFlags', - getName: (featureFlag: FeatureFlagType) => featureFlag.name || featureFlag.key, + getName: (featureFlag: FeatureFlagType) => featureFlag.key || featureFlag.name, getValue: (featureFlag: FeatureFlagType) => featureFlag.id || '', }, { @@ -181,15 +181,6 @@ export const universalSearchLogic = kea({ getName: (dashboard: DashboardType) => dashboard.name, getValue: (dashboard: DashboardType) => dashboard.id, }, - { - name: 'Dashboards', - searchPlaceholder: 'dashboards', - type: UniversalSearchGroupType.Dashboards, - logic: dashboardsModel, - value: 'nameSortedDashboards', - getName: (dashboard: DashboardType) => dashboard.name, - getValue: (dashboard: DashboardType) => dashboard.id, - }, ...groupAnalyticsTaxonomicGroups, ], ], diff --git a/frontend/src/scenes/plugins/pluginsLogic.ts b/frontend/src/scenes/plugins/pluginsLogic.ts index 47c36fea344e0..f804c3e4e3d64 100644 --- a/frontend/src/scenes/plugins/pluginsLogic.ts +++ b/frontend/src/scenes/plugins/pluginsLogic.ts @@ -25,6 +25,12 @@ export enum PluginSection { Disabled = 'disabled', } +export interface PluginSelectionType { + name: string + url?: string + tab: PluginTab +} + const PAGINATION_DEFAULT_MAX_PAGES = 10 function capturePluginEvent(event: string, plugin: PluginType, type?: PluginInstallationType): void { @@ -53,7 +59,7 @@ async function loadPaginatedResults( return results } -export const pluginsLogic = kea>({ +export const pluginsLogic = kea>({ path: ['scenes', 'plugins', 'pluginsLogic'], actions: { editPlugin: (id: number | null, pluginConfigChanges: Record = {}) => ({ id, pluginConfigChanges }), @@ -642,7 +648,7 @@ export const pluginsLogic = kea>({ allPossiblePlugins: [ (s) => [s.repository, s.plugins], (repository, plugins) => { - const allPossiblePlugins: { name: string; url?: string; tab: PluginTab }[] = [] + const allPossiblePlugins: PluginSelectionType[] = [] for (const plugin of Object.values(plugins) as PluginType[]) { allPossiblePlugins.push({ name: plugin.name, url: plugin.url, tab: PluginTab.Installed }) } From 542acf6dbe619cfbcb4039c37fd2b8f5a206e563 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 31 Mar 2022 14:52:58 +0000 Subject: [PATCH 11/18] reconcile changes --- ee/clickhouse/views/groups.py | 19 ++++++++++++++----- frontend/src/lib/components/Popup/Popup.tsx | 2 +- .../UniversalSearch/UniversalSearchPopup.tsx | 12 ++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 6221d3178ab1f..e4e16b9ef3f97 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -1,8 +1,7 @@ from collections import defaultdict from typing import Dict, List, cast -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, mixins, request, response, serializers, viewsets +from rest_framework import mixins, request, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.pagination import CursorPagination @@ -61,9 +60,19 @@ class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewse queryset = Group.objects.all() pagination_class = GroupCursorPagination permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] - filter_backends = [filters.SearchFilter, DjangoFilterBackend] - filterset_fields = ["group_type_index"] - search_fields = ["group_key"] + # filter_backends = [filters.SearchFilter, DjangoFilterBackend] + # filterset_fields = ["group_type_index"] + # search_fields = ["group_key"] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + group_type_index=self.request.GET["group_type_index"], + group_key__icontains=self.request.GET.get("group_key", ""), + ) + ) @action(methods=["GET"], detail=False) def find(self, request: request.Request, **kw) -> response.Response: diff --git a/frontend/src/lib/components/Popup/Popup.tsx b/frontend/src/lib/components/Popup/Popup.tsx index d099785900acd..558ea4b7688fd 100644 --- a/frontend/src/lib/components/Popup/Popup.tsx +++ b/frontend/src/lib/components/Popup/Popup.tsx @@ -60,7 +60,7 @@ export function Popup({ { name: 'offset', options: { - offset: [0, -250], + offset: [0, 4], }, }, fallbackPlacements diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index dd4bf5179b60f..eefa9bf761055 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -102,6 +102,7 @@ export function UniversalSearchPopup({ groupType, value, onChange: ({ type }, payload, item) => { + redirectOnSelectItems(payload, type, item) onChange?.(payload, type, item) setVisible(false) }, @@ -117,16 +118,7 @@ export function UniversalSearchPopup({
{ - redirectOnSelectItems(payload, type, item) - onChange?.(payload, type, item) - setVisible(false) - }} - searchGroupTypes={groupTypes ?? [groupType]} - /> + } visible={visible} placement="right-start" From db0d0fc85519de2cdf9198cce52ec91751ff5ca1 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 31 Mar 2022 15:12:35 +0000 Subject: [PATCH 12/18] clean up --- ee/clickhouse/views/groups.py | 4 ++-- .../src/lib/components/UniversalSearch/SearchResults.tsx | 6 +++--- .../components/UniversalSearch/UniversalSearchPopup.tsx | 8 ++++++++ .../components/UniversalSearch/universalSearchLogic.tsx | 4 ++-- frontend/src/scenes/plugins/pluginsLogic.ts | 3 +-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index e4e16b9ef3f97..7313447639a34 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -60,9 +60,9 @@ class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewse queryset = Group.objects.all() pagination_class = GroupCursorPagination permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] - # filter_backends = [filters.SearchFilter, DjangoFilterBackend] + # filter_backends = [filters.SearchFilter] # filterset_fields = ["group_type_index"] - # search_fields = ["group_key"] + # search_fields = ["group_key", "group_properties"] def get_queryset(self): return ( diff --git a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx index c9b0d4d0afd9b..4494c5450636e 100644 --- a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx +++ b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx @@ -4,12 +4,12 @@ import { BindLogic, useActions, useValues } from 'kea' import { universalSearchLogic } from './universalSearchLogic' import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' import { SearchList } from 'lib/components/UniversalSearch/searchList' -import { UniversalSearchGroupType, UniversalSearchLogicProps } from './types' +import { UniversalSearchGroupType, UniversalSearchProps } from './types' import clsx from 'clsx' export interface SearchResultsProps { focusInput: () => void - universalSearchLogicProps: UniversalSearchLogicProps + universalSearchLogicProps: UniversalSearchProps } function CategoryPill({ @@ -20,7 +20,7 @@ function CategoryPill({ }: { isActive: boolean groupType: UniversalSearchGroupType - universalSearchLogicProps: UniversalSearchLogicProps + universalSearchLogicProps: UniversalSearchProps onClick: () => void }): JSX.Element { const logic = searchListLogic({ ...universalSearchLogicProps, listGroupType: groupType }) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index eefa9bf761055..24271e14bc1de 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -14,6 +14,7 @@ import { useValues } from 'kea' import { universalSearchLogic } from './universalSearchLogic' import { IconMagnifier } from '../icons' import { Input } from 'antd' +import { useEventListener } from 'lib/hooks/useEventListener' export interface UniversalSearchPopupProps extends Omit { @@ -114,6 +115,13 @@ export function UniversalSearchPopup({ const logic = universalSearchLogic(universalSearchLogicProps) const { searchQuery, searchPlaceholder } = useValues(logic) + useEventListener('keydown', (event) => { + if (event.key === 's' && (event.ctrlKey || event.metaKey)) { + event.preventDefault() + setVisible(!visible) + } + }) + return (
({ path: (key) => ['lib', 'components', 'UniversalSearch', 'universalSearchLogic', key], - props: {} as UniversalSearchLogicProps, + props: {} as UniversalSearchProps, key: () => `universal-search`, connect: { values: [ diff --git a/frontend/src/scenes/plugins/pluginsLogic.ts b/frontend/src/scenes/plugins/pluginsLogic.ts index f804c3e4e3d64..dfb05931f18d7 100644 --- a/frontend/src/scenes/plugins/pluginsLogic.ts +++ b/frontend/src/scenes/plugins/pluginsLogic.ts @@ -733,8 +733,7 @@ export const pluginsLogic = kea ({ - '/project/plugins': (url, { tab, name }) => { - console.log('got values: ', tab, name, url) + '/project/plugins': (_, { tab, name }) => { if (tab && name) { actions.setSearchTerm(name) actions.setPluginTab(tab as PluginTab) From d83facd6735e7447117b17a5c48145b8582d18c0 Mon Sep 17 00:00:00 2001 From: Tim Glaser Date: Thu, 7 Apr 2022 18:30:57 +0100 Subject: [PATCH 13/18] placeholder --- .../src/lib/components/UniversalSearch/UniversalSearchPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 24271e14bc1de..cce3b3c6fa3dc 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -165,7 +165,7 @@ export function UniversalSearchPopup({ } /> From f7b7e5497328cd002849bbb1dd63968a14916dfc Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Tue, 12 Apr 2022 16:53:43 +0100 Subject: [PATCH 14/18] reconcile taxonomic filter and search filter --- .../src/layout/navigation/TopBar/TopBar.tsx | 28 +- .../TaxonomicFilter/InfiniteList.tsx | 4 +- .../TaxonomicFilter/infiniteListLogic.ts | 17 +- .../TaxonomicFilter/taxonomicFilterLogic.tsx | 118 +++++- .../lib/components/TaxonomicFilter/types.ts | 8 + .../UniversalSearch/SearchResults.tsx | 98 ----- .../UniversalSearch/UniversalSearch.tsx | 131 ------- .../UniversalSearch/UniversalSearchPopup.tsx | 75 ++-- .../components/UniversalSearch/searchList.tsx | 158 -------- .../UniversalSearch/searchListLogic.ts | 291 --------------- .../lib/components/UniversalSearch/types.ts | 94 ----- .../UniversalSearch/universalSearchLogic.tsx | 349 ------------------ 12 files changed, 206 insertions(+), 1165 deletions(-) delete mode 100644 frontend/src/lib/components/UniversalSearch/SearchResults.tsx delete mode 100644 frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx delete mode 100644 frontend/src/lib/components/UniversalSearch/searchList.tsx delete mode 100644 frontend/src/lib/components/UniversalSearch/searchListLogic.ts delete mode 100644 frontend/src/lib/components/UniversalSearch/types.ts delete mode 100644 frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 9a7e2c235ae23..cd6ff4266ab02 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -14,7 +14,8 @@ import { CreateProjectModal } from '../../../scenes/project/CreateProjectModal' import './TopBar.scss' import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' import { UniversalSearchPopup } from 'lib/components/UniversalSearch/UniversalSearchPopup' -import { UniversalSearchGroupType } from 'lib/components/UniversalSearch/types' +import { TaxonomicFilterGroupType } from 'lib/components/UniversalSearch/types' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' export function TopBar(): JSX.Element { const { isSideBarShown, bareNav, mobileLayout, isCreateOrganizationModalShown, isCreateProjectModalShown } = @@ -42,20 +43,23 @@ export function TopBar(): JSX.Element {
diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx index fe69314ac0e69..b31c042f6cd63 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx @@ -129,8 +129,8 @@ const renderItemContents = ({ ) : ( <> - {icon} - {item.name ?? ''} + {group.getIcon ? icon : null} + {group.getName(item) || item.name || ''} )}
diff --git a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts index 6da04dbcb4d20..3e86a0ab0e01f 100644 --- a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts +++ b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts @@ -14,6 +14,9 @@ import { TaxonomicFilterGroup, } from 'lib/components/TaxonomicFilter/types' import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' /* by default the pop-up starts open for the first item in the list @@ -67,7 +70,17 @@ export const infiniteListLogic = kea({ key: (props) => `${props.taxonomicFilterLogicKey}-${props.listGroupType}`, connect: (props: InfiniteListLogicProps) => ({ - values: [taxonomicFilterLogic(props), ['searchQuery', 'value', 'groupType', 'taxonomicGroups']], + // TODO: had to connect FF to get the model loaded for filtering + values: [ + taxonomicFilterLogic(props), + ['searchQuery', 'value', 'groupType', 'taxonomicGroups'], + featureFlagsLogic, + ['featureFlags'], + experimentsLogic, + ['experiments'], + pluginsLogic, + ['plugins'], + ], actions: [taxonomicFilterLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], }), @@ -157,7 +170,7 @@ export const infiniteListLogic = kea({ ), searchQuery: values.searchQuery, queryChanged, - count: response.count || 0, + count: response.count || (response.results || []).length, expandedCount: expandedCountResponse?.count, } }, diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index db4fbdcb65f40..f3810e17fb08c 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -11,7 +11,20 @@ import { } from 'lib/components/TaxonomicFilter/types' import { infiniteListLogic } from 'lib/components/TaxonomicFilter/infiniteListLogic' import { personPropertiesModel } from '~/models/personPropertiesModel' -import { ActionType, CohortType, EventDefinition, PersonProperty, PropertyDefinition } from '~/types' +import { + ActionType, + CohortType, + DashboardType, + EventDefinition, + Experiment, + FeatureFlagType, + Group, + InsightModel, + PersonProperty, + PersonType, + PluginType, + PropertyDefinition, +} from '~/types' import { cohortsModel } from '~/models/cohortsModel' import { actionsModel } from '~/models/actionsModel' import { eventDefinitionsModel } from '~/models/eventDefinitionsModel' @@ -23,6 +36,11 @@ import { combineUrl } from 'kea-router' import { ActionStack, CohortIcon } from 'lib/components/icons' import { keyMapping } from 'lib/components/PropertyKeyInfo' import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { dashboardsModel } from '~/models/dashboardsModel' +import { groupDisplayId } from 'scenes/persons/GroupActorHeader' export const eventTaxonomicGroupProps: Pick = { getPopupHeader: (eventDefinition: EventDefinition): string => { @@ -117,8 +135,18 @@ export const taxonomicFilterLogic = kea({ ], eventNames: [() => [(_, props) => props.eventNames], (eventNames) => eventNames ?? []], taxonomicGroups: [ - (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups, selectors.eventNames], - (teamId, groupAnalyticsTaxonomicGroups, eventNames): TaxonomicFilterGroup[] => [ + (selectors) => [ + selectors.currentTeamId, + selectors.groupAnalyticsTaxonomicGroups, + selectors.groupAnalyticsTaxonomicGroupNames, + selectors.eventNames, + ], + ( + teamId, + groupAnalyticsTaxonomicGroups, + groupAnalyticsTaxonomicGroupNames, + eventNames + ): TaxonomicFilterGroup[] => [ { name: 'Events', searchPlaceholder: 'events', @@ -265,7 +293,68 @@ export const taxonomicFilterLogic = kea({ getValue: (option: SimpleOption) => option.name, getPopupHeader: () => `Wildcard`, }, + { + name: 'Persons', + searchPlaceholder: 'persons', + type: TaxonomicFilterGroupType.Persons, + endpoint: `api/projects/${teamId}/persons/`, + getName: (person: PersonType) => person.name || 'Anon user?', + getValue: (person: PersonType) => person.distinct_ids[0], + getPopupHeader: () => `Person`, + }, + { + name: 'Insights', + searchPlaceholder: 'insights', + type: TaxonomicFilterGroupType.Insights, + endpoint: combineUrl(`api/projects/${teamId}/insights/`, { + saved: true, + }).url, + getName: (insight: InsightModel) => insight.name, + getValue: (insight: InsightModel) => insight.short_id, + getPopupHeader: () => `Insights`, + }, + { + name: 'Feature Flags', + searchPlaceholder: 'feature flags', + type: TaxonomicFilterGroupType.FeatureFlags, + logic: featureFlagsLogic, + value: 'featureFlags', + getName: (featureFlag: FeatureFlagType) => featureFlag.key || featureFlag.name, + getValue: (featureFlag: FeatureFlagType) => featureFlag.id || '', + getPopupHeader: () => `Feature Flags`, + }, + { + name: 'Experiments', + searchPlaceholder: 'experiments', + type: TaxonomicFilterGroupType.Experiments, + logic: experimentsLogic, + value: 'experiments', + getName: (experiment: Experiment) => experiment.name, + getValue: (experiment: Experiment) => experiment.id, + getPopupHeader: () => `Experiments`, + }, + { + name: 'Plugins', + searchPlaceholder: 'plugins', + type: TaxonomicFilterGroupType.Plugins, + logic: pluginsLogic, + value: 'allPossiblePlugins', + getName: (plugin: Pick) => plugin.name, + getValue: (plugin: Pick) => plugin.name, + getPopupHeader: () => `Plugins`, + }, + { + name: 'Dashboards', + searchPlaceholder: 'dashboards', + type: TaxonomicFilterGroupType.Dashboards, + logic: dashboardsModel, + value: 'nameSortedDashboards', + getName: (dashboard: DashboardType) => dashboard.name, + getValue: (dashboard: DashboardType) => dashboard.id, + getPopupHeader: () => `Dashboards`, + }, ...groupAnalyticsTaxonomicGroups, + ...groupAnalyticsTaxonomicGroupNames, ], ], activeTaxonomicGroup: [ @@ -277,13 +366,30 @@ export const taxonomicFilterLogic = kea({ (groupTypes, taxonomicGroups): TaxonomicFilterGroupType[] => groupTypes || taxonomicGroups.map((g) => g.type), ], + groupAnalyticsTaxonomicGroupNames: [ + (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], + (groupTypes, teamId, aggregationLabel): TaxonomicFilterGroup[] => + groupTypes.map((type) => ({ + name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).plural)}`, + searchPlaceholder: `${aggregationLabel(type.group_type_index).plural}`, + type: `${TaxonomicFilterGroupType.GroupNamesPrefix}_${type.group_type_index}` as unknown as TaxonomicFilterGroupType, + endpoint: combineUrl(`api/projects/${teamId}/groups/`, { + group_type_index: type.group_type_index, + }).url, + searchAlias: 'group_key', + getPopupHeader: () => `Group Names`, + getName: (group: Group) => groupDisplayId(group.group_key, group.group_properties), + getValue: (group: Group) => group.group_key, + groupTypeIndex: type.group_type_index, + })), + ], groupAnalyticsTaxonomicGroups: [ (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], (groupTypes, teamId, aggregationLabel): TaxonomicFilterGroup[] => groupTypes.map((type) => ({ name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).singular)} properties`, searchPlaceholder: `${aggregationLabel(type.group_type_index).singular} properties`, - type: `${TaxonomicFilterGroupType.GroupsPrefix}_${type.group_type_index}` as TaxonomicFilterGroupType, + type: `${TaxonomicFilterGroupType.GroupsPrefix}_${type.group_type_index}` as unknown as TaxonomicFilterGroupType, logic: groupPropertiesModel, value: `groupProperties_${type.group_type_index}`, valuesEndpoint: (key) => @@ -334,7 +440,9 @@ export const taxonomicFilterLogic = kea({ (allTaxonomicGroups, searchGroupTypes) => { if (searchGroupTypes.length > 1) { searchGroupTypes = searchGroupTypes.filter( - (type) => !type.startsWith(TaxonomicFilterGroupType.GroupsPrefix) + (type) => + !type.startsWith(TaxonomicFilterGroupType.GroupsPrefix) && + !type.startsWith(TaxonomicFilterGroupType.GroupNamesPrefix) ) } const names = searchGroupTypes.map((type) => { diff --git a/frontend/src/lib/components/TaxonomicFilter/types.ts b/frontend/src/lib/components/TaxonomicFilter/types.ts index f4a946071eaa7..22c1430e19892 100644 --- a/frontend/src/lib/components/TaxonomicFilter/types.ts +++ b/frontend/src/lib/components/TaxonomicFilter/types.ts @@ -62,6 +62,14 @@ export enum TaxonomicFilterGroupType { CustomEvents = 'custom_events', Wildcards = 'wildcard', GroupsPrefix = 'groups', + // Types for searching + Persons = 'persons', + FeatureFlags = 'feature_flags', + Insights = 'insights', + Experiments = 'experiments', + Plugins = 'plugins', + Dashboards = 'dashboards', + GroupNamesPrefix = 'name_groups', } export interface InfiniteListLogicProps extends TaxonomicFilterLogicProps { diff --git a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx b/frontend/src/lib/components/UniversalSearch/SearchResults.tsx deleted file mode 100644 index 4494c5450636e..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/SearchResults.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react' -import { Tag } from 'antd' -import { BindLogic, useActions, useValues } from 'kea' -import { universalSearchLogic } from './universalSearchLogic' -import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' -import { SearchList } from 'lib/components/UniversalSearch/searchList' -import { UniversalSearchGroupType, UniversalSearchProps } from './types' -import clsx from 'clsx' - -export interface SearchResultsProps { - focusInput: () => void - universalSearchLogicProps: UniversalSearchProps -} - -function CategoryPill({ - isActive, - groupType, - universalSearchLogicProps, - onClick, -}: { - isActive: boolean - groupType: UniversalSearchGroupType - universalSearchLogicProps: UniversalSearchProps - onClick: () => void -}): JSX.Element { - const logic = searchListLogic({ ...universalSearchLogicProps, listGroupType: groupType }) - const { searchGroups } = useValues(universalSearchLogic) - const { totalResultCount } = useValues(logic) - - const group = searchGroups.find((g) => g.type === groupType) - const canInteract = totalResultCount > 0 - - return ( - - {group?.name} - {': '} - {totalResultCount ?? '...'} - - ) -} - -export function SearchResults({ focusInput, universalSearchLogicProps }: SearchResultsProps): JSX.Element { - const { activeTab, searchGroups, searchGroupTypes } = useValues(universalSearchLogic) - const { setActiveTab } = useActions(universalSearchLogic) - - if (searchGroupTypes.length === 1) { - return ( - - - - ) - } - - const openTab = activeTab || searchGroups[0].type - return ( - <> -
Categories
-
- {searchGroupTypes.map((groupType) => { - return ( - { - setActiveTab(groupType) - focusInput() - }} - /> - ) - })} -
-
- {searchGroups.find((g) => g.type === openTab)?.name || openTab} -
- {searchGroupTypes.map((groupType) => { - return ( -
- - - -
- ) - })} - - ) -} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx deleted file mode 100644 index 5b8bd489ce568..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearch.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import './UniversalSearch.scss' -import React, { useEffect, useRef } from 'react' -import { Input } from 'antd' -import { useValues, useActions, BindLogic } from 'kea' -import { SearchResults } from './SearchResults' -import { IconKeyboard, IconMagnifier } from '../icons' -import { Tooltip } from '../Tooltip' -import clsx from 'clsx' -import { universalSearchLogic } from './universalSearchLogic' -import { UniversalSearchProps } from './types' - -export function UniversalSearch({ - groupType, - value, - onChange, - onClose, - searchGroupTypes, - optionsFromProp, - height, - width, - popoverEnabled = true, - selectFirstItem = true, -}: UniversalSearchProps): JSX.Element { - // Generate a unique key for each unique UniversalSearch that's rendered - - const searchInputRef = useRef(null) - const focusInput = (): void => searchInputRef.current?.focus() - - const universalSearchLogicProps: UniversalSearchProps = { - groupType, - value, - onChange, - searchGroupTypes, - optionsFromProp, - popoverEnabled, - selectFirstItem, - } - - const logic = universalSearchLogic(universalSearchLogicProps) - const { searchQuery, searchPlaceholder } = useValues(logic) - const { setSearchQuery, moveUp, moveDown, tabLeft, tabRight, selectSelected } = useActions(logic) - - useEffect(() => { - window.setTimeout(() => focusInput(), 1) - }, []) - - const style = { - ...(width ? { width } : {}), - ...(height ? { height } : {}), - } - - return ( - -
-
- - } - value={searchQuery} - ref={(ref) => (searchInputRef.current = ref)} - onChange={(e) => setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'ArrowUp') { - e.preventDefault() - moveUp() - } - if (e.key === 'ArrowDown') { - e.preventDefault() - moveDown() - } - if (e.key === 'ArrowLeft') { - e.preventDefault() - tabLeft() - } - if (e.key === 'ArrowRight') { - e.preventDefault() - tabRight() - } - if (e.key === 'Tab') { - e.preventDefault() - if (e.shiftKey) { - tabLeft() - } else { - tabRight() - } - } - - if (e.key === 'Enter') { - e.preventDefault() - selectSelected() - } - if (e.key === 'Escape') { - e.preventDefault() - onClose?.() - } - }} - suffix={ - - You can easily navigate between tabs with your keyboard.{' '} -
- Use tab or right arrow to move to the next tab. -
-
- Use shift + tab or left arrow to move to the previous tab. -
- - } - > - -
- } - /> -
- -
-
- ) -} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index cce3b3c6fa3dc..62100901db8c8 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -1,27 +1,38 @@ +import './UniversalSearch.scss' import React, { useState } from 'react' import { LemonButtonWithPopupProps } from '../LemonButton' -import { TaxonomicFilterValue } from '../TaxonomicFilter/types' -import { SearchDefinitionTypes, UniversalSearchGroupType, UniversalSearchProps } from './types' +import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps, TaxonomicFilterValue } from '../TaxonomicFilter/types' import { Popup } from 'lib/components/Popup/Popup' -import { UniversalSearch } from './UniversalSearch' import { combineUrl, router } from 'kea-router' import { urls } from 'scenes/urls' -import { ActionType, ChartDisplayType, Group, InsightModel, InsightType } from '~/types' +import { + ActionType, + ChartDisplayType, + CohortType, + EventDefinition, + Experiment, + FeatureFlagType, + Group, + InsightModel, + InsightType, + PersonType, +} from '~/types' import { PluginSelectionType } from 'scenes/plugins/pluginsLogic' import clsx from 'clsx' import { navigationLogic } from '~/layout/navigation/navigationLogic' import { useValues } from 'kea' -import { universalSearchLogic } from './universalSearchLogic' import { IconMagnifier } from '../icons' import { Input } from 'antd' import { useEventListener } from 'lib/hooks/useEventListener' +import { taxonomicFilterLogic } from '../TaxonomicFilter/taxonomicFilterLogic' +import { TaxonomicFilter } from '../TaxonomicFilter/TaxonomicFilter' export interface UniversalSearchPopupProps extends Omit { - groupType: UniversalSearchGroupType + groupType: TaxonomicFilterGroupType value?: ValueType - onChange?: (value: ValueType, groupType: UniversalSearchGroupType, item: SearchDefinitionTypes) => void - groupTypes?: UniversalSearchGroupType[] + onChange?: (value: ValueType, groupType: TaxonomicFilterGroupType, item: SearchDefinitionTypes) => void + groupTypes?: TaxonomicFilterGroupType[] renderValue?: (value: ValueType) => JSX.Element dataAttr?: string placeholder?: React.ReactNode @@ -29,12 +40,23 @@ export interface UniversalSearchPopupProps allowClear?: boolean } +type SearchDefinitionTypes = + | EventDefinition + | CohortType + | ActionType + | Experiment + | PersonType + | Group + | FeatureFlagType + | InsightModel + | PluginSelectionType + function redirectOnSelectItems( value: TaxonomicFilterValue, - groupType: UniversalSearchGroupType, + groupType: TaxonomicFilterGroupType, item: SearchDefinitionTypes ): void { - if (groupType === UniversalSearchGroupType.Events) { + if (groupType === TaxonomicFilterGroupType.Events) { router.actions.push( combineUrl( urls.insightNew({ @@ -45,7 +67,7 @@ function redirectOnSelectItems( }) ).url ) - } else if (groupType === UniversalSearchGroupType.Actions) { + } else if (groupType === TaxonomicFilterGroupType.Actions) { router.actions.push( combineUrl( urls.insightNew({ @@ -63,26 +85,26 @@ function redirectOnSelectItems( }) ).url ) - } else if (groupType === UniversalSearchGroupType.Cohorts) { + } else if (groupType === TaxonomicFilterGroupType.Cohorts) { router.actions.push(urls.cohort(value)) - } else if (groupType === UniversalSearchGroupType.Persons) { + } else if (groupType === TaxonomicFilterGroupType.Persons) { router.actions.push(urls.person(String(value))) - } else if (groupType.startsWith(UniversalSearchGroupType.GroupsPrefix)) { + } else if (groupType.startsWith(TaxonomicFilterGroupType.GroupNamesPrefix)) { router.actions.push(urls.group((item as Group).group_type_index, String(value))) - } else if (groupType === UniversalSearchGroupType.Insights) { + } else if (groupType === TaxonomicFilterGroupType.Insights) { router.actions.push(urls.insightView((item as InsightModel).short_id)) - } else if (groupType === UniversalSearchGroupType.FeatureFlags) { + } else if (groupType === TaxonomicFilterGroupType.FeatureFlags) { router.actions.push(urls.featureFlag(value)) - } else if (groupType === UniversalSearchGroupType.Experiments) { + } else if (groupType === TaxonomicFilterGroupType.Experiments) { router.actions.push(urls.experiment(value)) - } else if (groupType === UniversalSearchGroupType.Plugins) { + } else if (groupType === TaxonomicFilterGroupType.Plugins) { router.actions.push( combineUrl(urls.plugins(), { tab: (item as PluginSelectionType).tab, name: (item as PluginSelectionType).name, }).url ) - } else if (groupType === UniversalSearchGroupType.Dashboards) { + } else if (groupType === TaxonomicFilterGroupType.Dashboards) { router.actions.push(urls.dashboard(value)) } } @@ -99,7 +121,7 @@ export function UniversalSearchPopup({ const [visible, setVisible] = useState(false) const { isSideBarShown } = useValues(navigationLogic) - const universalSearchLogicProps: UniversalSearchProps = { + const universalSearchLogicProps: TaxonomicFilterLogicProps = { groupType, value, onChange: ({ type }, payload, item) => { @@ -107,12 +129,13 @@ export function UniversalSearchPopup({ onChange?.(payload, type, item) setVisible(false) }, - searchGroupTypes: groupTypes ?? [groupType], + taxonomicGroupTypes: groupTypes ?? [groupType], optionsFromProp: undefined, popoverEnabled: true, selectFirstItem: true, + taxonomicFilterLogicKey: 'universalSearch', } - const logic = universalSearchLogic(universalSearchLogicProps) + const logic = taxonomicFilterLogic(universalSearchLogicProps) const { searchQuery, searchPlaceholder } = useValues(logic) useEventListener('keydown', (event) => { @@ -126,7 +149,12 @@ export function UniversalSearchPopup({
+ } visible={visible} placement="right-start" @@ -136,6 +164,7 @@ export function UniversalSearchPopup({ name: 'offset', options: { offset: ({ placement }) => { + // eslint-disable-line if (placement === 'right-start') { return [-10, -249 - 243] } else { diff --git a/frontend/src/lib/components/UniversalSearch/searchList.tsx b/frontend/src/lib/components/UniversalSearch/searchList.tsx deleted file mode 100644 index fd3dd2290fece..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/searchList.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import './SearchList.scss' -import '../Popup/Popup.scss' -import React from 'react' -import { Empty, Skeleton, Tag } from 'antd' -import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { useActions, useValues } from 'kea' -import { searchListLogic, NO_ITEM_SELECTED } from './searchListLogic' -import { universalSearchLogic } from './universalSearchLogic' -import { EventDefinition, PersonType } from '~/types' -import { dayjs } from 'lib/dayjs' -import { FEATURE_FLAGS, STALE_EVENT_SECONDS } from 'lib/constants' -import { Tooltip } from '../Tooltip' -import clsx from 'clsx' -import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' -import { SearchDefinitionTypes, UniversalSearchGroup, UniversalSearchGroupType } from './types' - -const staleIndicator = (parsedLastSeen: dayjs.Dayjs | null): JSX.Element => { - return ( - - This event was last seen {parsedLastSeen ? parsedLastSeen.fromNow() : 'a while ago'}. - - } - > - Stale - - ) -} - -const renderItemContents = ({ - item, - listGroupType, - group, - featureFlags, -}: { - item: SearchDefinitionTypes - listGroupType: UniversalSearchGroupType - group: UniversalSearchGroup - featureFlags: FeatureFlagsSet -}): JSX.Element | string => { - const parsedLastSeen = (item as EventDefinition).last_seen_at ? dayjs((item as EventDefinition).last_seen_at) : null - const isStale = - (listGroupType === UniversalSearchGroupType.Events && !parsedLastSeen) || - dayjs().diff(parsedLastSeen, 'seconds') > STALE_EVENT_SECONDS - - return listGroupType === UniversalSearchGroupType.Persons || listGroupType === UniversalSearchGroupType.Events ? ( - <> -
- -
- {isStale && staleIndicator(parsedLastSeen)} - - ) : ( - <>{group.getName(item) ?? ''} - ) -} - -export function SearchList(): JSX.Element { - const { mouseInteractionsEnabled, searchQuery, value, groupType } = useValues(universalSearchLogic) - const { selectItem } = useActions(universalSearchLogic) - const { featureFlags } = useValues(featureFlagLogic) - - const { isLoading, results, index, listGroupType, group, totalResultCount, showPopover } = - useValues(searchListLogic) - const { onRowsRendered, setIndex } = useActions(searchListLogic) - - const showEmptyState = totalResultCount === 0 && !isLoading - - const renderItem: ListRowRenderer = ({ index: rowIndex, style }: ListRowProps): JSX.Element | null => { - const item = results[rowIndex] - const itemValue = item ? group?.getValue?.(item) : null - const isSelected = listGroupType === groupType && itemValue === value - - const commonDivProps: React.HTMLProps = { - key: `item_${rowIndex}`, - className: clsx( - 'taxonomic-list-row', - rowIndex === index && mouseInteractionsEnabled && 'hover', - isSelected && 'selected' - ), - onMouseOver: () => (mouseInteractionsEnabled ? setIndex(rowIndex) : setIndex(NO_ITEM_SELECTED)), - // if the popover is not enabled then don't leave the row selected when the mouse leaves it - onMouseLeave: () => (mouseInteractionsEnabled && !showPopover ? setIndex(NO_ITEM_SELECTED) : null), - style: style, - } - - return item && group ? ( -
selectItem(group, itemValue ?? null, item)} - > - {renderItemContents({ - item, - listGroupType, - group, - featureFlags, - })} -
- ) : ( -
- -
- ) - } - - return ( -
- {showEmptyState ? ( -
- - {searchQuery ? ( - <> - No results for "{searchQuery}" - - ) : ( - 'No results' - )} - - } - /> -
- ) : ( - - {({ height, width }) => ( - - )} - - )} -
- ) -} diff --git a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts b/frontend/src/lib/components/UniversalSearch/searchListLogic.ts deleted file mode 100644 index 8829ad4157410..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/searchListLogic.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { kea } from 'kea' -import { combineUrl } from 'kea-router' -import api from 'lib/api' -import { RenderedRows } from 'react-virtualized/dist/es/List' -import { CohortType, EventDefinition } from '~/types' -import Fuse from 'fuse.js' - -import { ListFuse, LoaderOptions } from 'lib/components/TaxonomicFilter/types' -import { - SearchDefinitionTypes, - SearchListLogicProps, - UniversalSearchGroup, - ListStorage, -} from 'lib/components/UniversalSearch/types' -import { universalSearchLogic } from './universalSearchLogic' - -import { searchListLogicType } from './searchListLogicType' -import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' -import { experimentsLogic } from 'scenes/experiments/experimentsLogic' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -/* - by default the pop-up starts open for the first item in the list - this can be used with actions.setIndex to allow a caller to override that - */ -export const NO_ITEM_SELECTED = -1 - -function appendAtIndex(array: T[], items: any[], startIndex?: number): T[] { - if (startIndex === undefined) { - return [...array, ...items] - } - const arrayCopy = [...array] - items.forEach((item, i) => { - arrayCopy[startIndex + i] = item - }) - return arrayCopy -} - -const createEmptyListStorage = (searchQuery = '', first = false): ListStorage => ({ - results: [], - searchQuery, - count: 0, - first, -}) - -// simple cache with a setTimeout expiry -const API_CACHE_TIMEOUT = 60000 -const apiCache: Record = {} -const apiCacheTimers: Record = {} - -async function fetchCachedListResponse(path: string, searchParams: Record): Promise { - const url = combineUrl(path, searchParams).url - let response - if (apiCache[url]) { - response = apiCache[url] - } else { - response = await api.get(url) - apiCache[url] = response - apiCacheTimers[url] = window.setTimeout(() => { - delete apiCache[url] - delete apiCacheTimers[url] - }, API_CACHE_TIMEOUT) - } - return response -} - -export const searchListLogic = kea({ - path: (key) => ['lib', 'components', 'UniversalSearch', 'searchListLogic', key], - props: {} as SearchListLogicProps, - - key: (props) => `${props.universalSearchLogicKey}-${props.listGroupType}`, - - connect: (props: SearchListLogicProps) => ({ - // TODO: had to connect FF to get the model loaded for filtering - values: [ - universalSearchLogic(props), - ['searchQuery', 'value', 'groupType', 'searchGroups'], - featureFlagsLogic, - ['featureFlags'], - experimentsLogic, - ['experiments'], - pluginsLogic, - ['plugins'], - ], - actions: [universalSearchLogic(props), ['setSearchQuery', 'selectItem', 'searchListResultsReceived']], - }), - - actions: { - selectSelected: true, - moveUp: true, - moveDown: true, - setIndex: (index: number) => ({ index }), - setLimit: (limit: number) => ({ limit }), - onRowsRendered: (rowInfo: RenderedRows) => ({ rowInfo }), - loadRemoteItems: (options: LoaderOptions) => options, - }, - - reducers: ({ props }) => ({ - index: [ - (props.selectFirstItem === false ? NO_ITEM_SELECTED : 0) as number, - { - setIndex: (_, { index }) => index, - loadRemoteItemsSuccess: (state, { remoteItems }) => (remoteItems.queryChanged ? 0 : state), - }, - ], - showPopover: [props.popoverEnabled !== false, {}], - limit: [ - 100, - { - setLimit: (_, { limit }) => limit, - }, - ], - startIndex: [0, { onRowsRendered: (_, { rowInfo: { startIndex } }) => startIndex }], - stopIndex: [0, { onRowsRendered: (_, { rowInfo: { stopIndex } }) => stopIndex }], - }), - - loaders: ({ values }) => ({ - remoteItems: [ - createEmptyListStorage('', true), - { - loadRemoteItems: async ({ offset, limit }, breakpoint) => { - // avoid the 150ms delay on first load - if (!values.remoteItems.first) { - await breakpoint(150) - } else { - // These connected values below might be read before they are available due to circular logic mounting. - // Adding a slight delay (breakpoint) fixes this. - await breakpoint(1) - } - - const { remoteEndpoint, searchQuery } = values - - if (!remoteEndpoint) { - // should not have been here in the first place! - return createEmptyListStorage(searchQuery) - } - - const searchParams = { - [`${values.group?.searchAlias || 'search'}`]: searchQuery, - limit, - offset, - } - - const response = await fetchCachedListResponse(remoteEndpoint, searchParams) - breakpoint() - - const queryChanged = values.items.searchQuery !== values.searchQuery - - return { - results: appendAtIndex( - queryChanged ? [] : values.items.results, - response.results || response, - offset - ), - searchQuery: values.searchQuery, - queryChanged, - count: response.count || (response.results || []).length, - } - }, - }, - ], - }), - - listeners: ({ values, actions, props }) => ({ - onRowsRendered: ({ rowInfo: { startIndex, stopIndex, overscanStopIndex } }) => { - if (values.isRemoteDataSource) { - let loadFrom: number | null = null - for (let i = startIndex; i < (stopIndex + overscanStopIndex) / 2; i++) { - if (!values.results[i]) { - loadFrom = i - break - } - } - if (loadFrom !== null) { - actions.loadRemoteItems({ offset: loadFrom || startIndex, limit: values.limit }) - } - } - }, - setSearchQuery: () => { - if (values.isRemoteDataSource) { - actions.loadRemoteItems({ offset: 0, limit: values.limit }) - } else { - actions.setIndex(0) - } - }, - moveUp: () => { - const { index, totalResultCount } = values - actions.setIndex((index - 1 + totalResultCount) % totalResultCount) - }, - moveDown: () => { - const { index, totalResultCount } = values - actions.setIndex((index + 1) % totalResultCount) - }, - selectSelected: () => { - actions.selectItem(values.group, values.selectedItemValue, values.selectedItem) - }, - loadRemoteItemsSuccess: ({ remoteItems }) => { - actions.searchListResultsReceived(props.listGroupType, remoteItems) - }, - }), - - selectors: { - listGroupType: [() => [(_, props) => props.listGroupType], (listGroupType) => listGroupType], - isLoading: [(s) => [s.remoteItemsLoading], (remoteItemsLoading) => remoteItemsLoading], - group: [ - (s) => [s.listGroupType, s.searchGroups], - (listGroupType, searchGroups): UniversalSearchGroup => - searchGroups.find((g) => g.type === listGroupType) as UniversalSearchGroup, - ], - remoteEndpoint: [(s) => [s.group], (group) => group?.endpoint || null], - isRemoteDataSource: [(s) => [s.remoteEndpoint], (remoteEndpoint) => !!remoteEndpoint], - rawLocalItems: [ - (selectors) => [ - (state, props) => { - const searchGroups = selectors.searchGroups(state) - const group = searchGroups.find((g) => g.type === props.listGroupType) - if (group?.logic && group?.value) { - return group.logic.selectors[group.value]?.(state) || null - } - if (group?.options) { - return group.options - } - if (props.optionsFromProp && Object.keys(props.optionsFromProp).includes(props.listGroupType)) { - return props.optionsFromProp[props.listGroupType] - } - return null - }, - ], - (rawLocalItems: (EventDefinition | CohortType)[]) => rawLocalItems, - ], - fuse: [ - (s) => [s.rawLocalItems, s.group], - (rawLocalItems, group): ListFuse => - new Fuse( - (rawLocalItems || []).map((item) => ({ - name: group?.getName?.(item) || '', - item: item, - })), - { - keys: ['name'], - threshold: 0.3, - } - ), - ], - localItems: [ - (s) => [s.rawLocalItems, s.searchQuery, s.fuse], - (rawLocalItems, searchQuery, fuse): ListStorage => { - if (rawLocalItems) { - const filteredItems = searchQuery - ? fuse.search(searchQuery).map((result) => result.item.item) - : rawLocalItems - - return { - results: filteredItems, - count: filteredItems.length, - searchQuery, - } - } - return createEmptyListStorage() - }, - ], - items: [ - (s) => [s.isRemoteDataSource, s.remoteItems, s.localItems], - (isRemoteDataSource, remoteItems, localItems) => (isRemoteDataSource ? remoteItems : localItems), - ], - totalResultCount: [(s) => [s.items], (items) => items.count || 0], - results: [(s) => [s.items], (items) => items.results], - selectedItem: [ - (s) => [s.index, s.items], - (index, items): SearchDefinitionTypes | undefined => (index >= 0 ? items.results[index] : undefined), - ], - selectedItemValue: [ - (s) => [s.selectedItem, s.group], - (selectedItem, group) => (selectedItem ? group?.getValue?.(selectedItem) || null : null), - ], - selectedItemInView: [ - (s) => [s.index, s.startIndex, s.stopIndex], - (index, startIndex, stopIndex) => typeof index === 'number' && index >= startIndex && index <= stopIndex, - ], - }, - - events: ({ actions, values, props }) => ({ - afterMount: () => { - if (values.isRemoteDataSource) { - actions.loadRemoteItems({ offset: 0, limit: values.limit }) - } else if (values.groupType === props.listGroupType) { - const { value, group, results } = values - actions.setIndex(results.findIndex((r) => group?.getValue?.(r) === value)) - } - }, - }), -}) diff --git a/frontend/src/lib/components/UniversalSearch/types.ts b/frontend/src/lib/components/UniversalSearch/types.ts deleted file mode 100644 index f4c3275d1e8f9..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/types.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { LogicWrapper } from 'kea' -import { PluginSelectionType } from 'scenes/plugins/pluginsLogic' -import { - ActionType, - CohortType, - EventDefinition, - Experiment, - FeatureFlagType, - Group, - InsightModel, - PersonType, -} from '~/types' -import { SimpleOption, TaxonomicFilterValue } from '../TaxonomicFilter/types' - -export interface SearchListLogicProps extends UniversalSearchProps { - listGroupType: UniversalSearchGroupType -} - -export type SearchDefinitionTypes = - | EventDefinition - | CohortType - | ActionType - | Experiment - | PersonType - | Group - | FeatureFlagType - | InsightModel - | PluginSelectionType - -export interface ListStorage { - results: SearchDefinitionTypes[] - searchQuery?: string // Query used for the results currently in state - count: number - expandedCount?: number - queryChanged?: boolean - first?: boolean -} - -export interface UniversalSearchProps { - groupType?: UniversalSearchGroupType - value?: TaxonomicFilterValue - onChange?: (group: UniversalSearchGroup, value: TaxonomicFilterValue, item: any) => void - onClose?: () => void - searchGroupTypes: UniversalSearchGroupType[] - universalSearchFilterLogicKey?: string - optionsFromProp?: Partial> - height?: number - width?: number - popoverEnabled?: boolean - selectFirstItem?: boolean -} - -export enum UniversalSearchGroupType { - Actions = 'actions', - Cohorts = 'cohorts', - // CohortsWithAllUsers = 'cohorts_with_all', - // Elements = 'elements', - Events = 'events', - EventProperties = 'event_properties', - NumericalEventProperties = 'numerical_event_properties', - Persons = 'persons', - // PersonProperties = 'person_properties', - // PageviewUrls = 'pageview_urls', - // Screens = 'screens', - // CustomEvents = 'custom_events', - // Wildcards = 'wildcard', - GroupsPrefix = 'groups', - FeatureFlags = 'feature_flags', - Insights = 'insights', - Experiments = 'experiments', - Plugins = 'plugins', - Dashboards = 'dashboards', -} - -export interface UniversalSearchGroup { - name: string - searchPlaceholder: string - type: UniversalSearchGroupType - endpoint?: string - /** If present, will be used instead of "endpoint" until the user presses "expand results". */ - scopedEndpoint?: string - expandLabel?: (props: { count: number; expandedCount: number }) => React.ReactNode - options?: Record[] - logic?: LogicWrapper - value?: string - searchAlias?: string - valuesEndpoint?: (key: string) => string - getName: (instance: any) => string - getValue: (instance: any) => TaxonomicFilterValue - // getPopupHeader: (instance: any) => string - // getIcon?: (instance: any) => JSX.Element - groupTypeIndex?: number - getFullDetailUrl?: (instance: any) => string -} diff --git a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx b/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx deleted file mode 100644 index aafaff98447f8..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/universalSearchLogic.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { kea } from 'kea' -import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' -import { UniversalSearchGroup, UniversalSearchGroupType, UniversalSearchProps, ListStorage } from './types' -import { searchListLogic } from 'lib/components/UniversalSearch/searchListLogic' -import { - ActionType, - CohortType, - DashboardType, - EventDefinition, - Experiment, - FeatureFlagType, - Group, - InsightModel, - PersonType, - PluginType, -} from '~/types' -import { cohortsModel } from '~/models/cohortsModel' -import { actionsModel } from '~/models/actionsModel' -import { teamLogic } from 'scenes/teamLogic' -import { groupsModel } from '~/models/groupsModel' -import { groupPropertiesModel } from '~/models/groupPropertiesModel' -import { capitalizeFirstLetter } from 'lib/utils' -import { combineUrl } from 'kea-router' - -import { universalSearchLogicType } from './universalSearchLogicType' -import { groupDisplayId } from 'scenes/persons/GroupActorHeader' -import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' -import { experimentsLogic } from 'scenes/experiments/experimentsLogic' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { dashboardsModel } from '~/models/dashboardsModel' - -export const universalSearchLogic = kea({ - path: (key) => ['lib', 'components', 'UniversalSearch', 'universalSearchLogic', key], - props: {} as UniversalSearchProps, - key: () => `universal-search`, - connect: { - values: [ - teamLogic, - ['currentTeamId'], - groupsModel, - ['groupTypes', 'aggregationLabel'], - groupPropertiesModel, - ['allGroupProperties'], - ], - }, - actions: () => ({ - moveUp: true, - moveDown: true, - selectSelected: (onComplete?: () => void) => ({ onComplete }), - enableMouseInteractions: true, - tabLeft: true, - tabRight: true, - setSearchQuery: (searchQuery: string) => ({ searchQuery }), - setActiveTab: (activeTab: UniversalSearchGroupType) => ({ activeTab }), - selectItem: (group: UniversalSearchGroup, value: TaxonomicFilterValue | null, item: any) => ({ - group, - value, - item, - }), - searchListResultsReceived: (groupType: UniversalSearchGroupType, results: ListStorage) => ({ - groupType, - results, - }), - }), - - reducers: ({ selectors }) => ({ - searchQuery: [ - '', - { - setSearchQuery: (_, { searchQuery }) => searchQuery, - }, - ], - activeTab: [ - (state: any): UniversalSearchGroupType => { - return selectors.groupType(state) || selectors.searchGroupTypes(state)[0] - }, - { - setActiveTab: (_, { activeTab }) => activeTab, - }, - ], - mouseInteractionsEnabled: [ - // This fixes a bug with keyboard up/down scrolling when the mouse is over the list. - // Otherwise shifting list elements cause the "hover" action to be triggered randomly. - true, - { - moveUp: () => false, - moveDown: () => false, - setActiveTab: () => true, - enableMouseInteractions: () => true, - }, - ], - }), - - // NB, don't change to the async "selectors: (logic) => {}", as this causes a white screen when infiniteListLogic-s - // connect to taxonomicFilterLogic to select their initial values. They won't be built yet and will be unknown. - selectors: { - universalSerchLogicKey: [ - () => [(_, props) => props.universalSerchLogicKey], - (universalSerchLogicKey) => universalSerchLogicKey, - ], - searchGroups: [ - (selectors) => [selectors.currentTeamId, selectors.groupAnalyticsTaxonomicGroups], - (teamId, groupAnalyticsTaxonomicGroups): UniversalSearchGroup[] => [ - { - name: 'Events', - searchPlaceholder: 'events', - type: UniversalSearchGroupType.Events, - endpoint: `api/projects/${teamId}/event_definitions`, - getName: (eventDefinition: EventDefinition) => eventDefinition.name, - getValue: (eventDefinition: EventDefinition) => eventDefinition.name, - }, - { - name: 'Actions', - searchPlaceholder: 'actions', - type: UniversalSearchGroupType.Actions, - logic: actionsModel, - value: 'actions', - getName: (action: ActionType) => action.name || '', - getValue: (action: ActionType) => action.id, - }, - { - name: 'Persons', - searchPlaceholder: 'persons', - type: UniversalSearchGroupType.Persons, - endpoint: `api/projects/${teamId}/persons/`, - getName: (person: PersonType) => person.name || 'Anon user?', - getValue: (person: PersonType) => person.distinct_ids[0], - }, - { - name: 'Insights', - searchPlaceholder: 'insights', - type: UniversalSearchGroupType.Insights, - endpoint: combineUrl(`api/projects/${teamId}/insights/`, { - saved: true, - }).url, - getName: (insight: InsightModel) => insight.name, - getValue: (insight: InsightModel) => insight.short_id, - }, - { - name: 'Cohorts', - searchPlaceholder: 'cohorts', - type: UniversalSearchGroupType.Cohorts, - logic: cohortsModel, - value: 'cohorts', - getName: (cohort: CohortType) => cohort.name || `Cohort ${cohort.id}`, - getValue: (cohort: CohortType) => cohort.id, - }, - { - name: 'Feature Flags', - searchPlaceholder: 'feature flags', - type: UniversalSearchGroupType.FeatureFlags, - logic: featureFlagsLogic, - value: 'featureFlags', - getName: (featureFlag: FeatureFlagType) => featureFlag.key || featureFlag.name, - getValue: (featureFlag: FeatureFlagType) => featureFlag.id || '', - }, - { - name: 'Experiments', - searchPlaceholder: 'experiments', - type: UniversalSearchGroupType.Experiments, - logic: experimentsLogic, - value: 'experiments', - getName: (experiment: Experiment) => experiment.name, - getValue: (experiment: Experiment) => experiment.id, - }, - { - name: 'Plugins', - searchPlaceholder: 'plugins', - type: UniversalSearchGroupType.Plugins, - logic: pluginsLogic, - value: 'allPossiblePlugins', - getName: (plugin: Pick) => plugin.name, - getValue: (plugin: Pick) => plugin.name, - }, - { - name: 'Dashboards', - searchPlaceholder: 'dashboards', - type: UniversalSearchGroupType.Dashboards, - logic: dashboardsModel, - value: 'nameSortedDashboards', - getName: (dashboard: DashboardType) => dashboard.name, - getValue: (dashboard: DashboardType) => dashboard.id, - }, - ...groupAnalyticsTaxonomicGroups, - ], - ], - activeTaxonomicGroup: [ - (s) => [s.activeTab, s.searchGroups], - (activeTab, searchGroups) => searchGroups.find((g) => g.type === activeTab), - ], - searchGroupTypes: [ - (selectors) => [(_, props) => props.searchGroupTypes, selectors.searchGroups], - (groupTypes, searchGroups): UniversalSearchGroupType[] => groupTypes || searchGroups.map((g) => g.type), - ], - groupAnalyticsTaxonomicGroups: [ - (selectors) => [selectors.groupTypes, selectors.currentTeamId, selectors.aggregationLabel], - (groupTypes, teamId, aggregationLabel): UniversalSearchGroup[] => - groupTypes.map((type) => ({ - name: `${capitalizeFirstLetter(aggregationLabel(type.group_type_index).plural)}`, - searchPlaceholder: `${aggregationLabel(type.group_type_index).plural}`, - type: `${UniversalSearchGroupType.GroupsPrefix}_${type.group_type_index}` as unknown as UniversalSearchGroupType, - endpoint: combineUrl(`api/projects/${teamId}/groups/`, { - group_type_index: type.group_type_index, - }).url, - searchAlias: 'group_key', - getName: (group: Group) => groupDisplayId(group.group_key, group.group_properties), - getValue: (group: Group) => group.group_key, - groupTypeIndex: type.group_type_index, - })), - ], - searchListLogics: [ - (s) => [s.searchGroupTypes, (_, props) => props], - (searchGroupTypes, props): Record> => - Object.fromEntries( - searchGroupTypes.map((groupType) => [ - groupType, - searchListLogic.build({ - ...props, - listGroupType: groupType, - }), - ]) - ), - ], - infiniteListCounts: [ - (s) => [ - (state, props) => - Object.fromEntries( - Object.entries(s.searchListLogics(state, props)).map(([groupType, logic]) => [ - groupType, - logic.isMounted() ? logic.selectors.totalResultCount(state, logic.props) : 0, - ]) - ), - ], - (infiniteListCounts) => infiniteListCounts, - ], - value: [() => [(_, props) => props.value], (value) => value], - groupType: [() => [(_, props) => props.groupType], (groupType) => groupType], - currentTabIndex: [ - (s) => [s.searchGroupTypes, s.activeTab], - (groupTypes, activeTab) => Math.max(groupTypes.indexOf(activeTab || ''), 0), - ], - searchPlaceholder: [ - (s) => [s.searchGroups, s.searchGroupTypes], - (allTaxonomicGroups, searchGroupTypes) => { - if (searchGroupTypes.length > 1) { - searchGroupTypes = searchGroupTypes.filter( - (type) => !type.startsWith(UniversalSearchGroupType.GroupsPrefix) - ) - } - const names = searchGroupTypes.map((type) => { - const taxonomicGroup = allTaxonomicGroups.find( - (tGroup) => tGroup.type == type - ) as UniversalSearchGroup - return taxonomicGroup.searchPlaceholder - }) - return names - .map( - (name, index) => - `${index !== 0 ? (index === searchGroupTypes.length - 1 ? ' or ' : ', ') : ''}${name}` - ) - .join('') - }, - ], - }, - listeners: ({ actions, values, props }) => ({ - selectItem: ({ group, value, item }) => { - if (item && value) { - props.onChange?.(group, value, item) - } - actions.setSearchQuery('') - }, - - moveUp: async (_, breakpoint) => { - if (values.activeTab) { - searchListLogic({ - ...props, - listGroupType: values.activeTab, - }).actions.moveUp() - } - await breakpoint(100) - actions.enableMouseInteractions() - }, - - moveDown: async (_, breakpoint) => { - if (values.activeTab) { - searchListLogic({ - ...props, - listGroupType: values.activeTab, - }).actions.moveDown() - } - await breakpoint(100) - actions.enableMouseInteractions() - }, - - selectSelected: async (_, breakpoint) => { - if (values.activeTab) { - searchListLogic({ - ...props, - listGroupType: values.activeTab, - }).actions.selectSelected() - } - await breakpoint(100) - actions.enableMouseInteractions() - }, - - tabLeft: () => { - const { currentTabIndex, searchGroupTypes, infiniteListCounts } = values - for (let i = 1; i < searchGroupTypes.length; i++) { - const newIndex = (currentTabIndex - i + searchGroupTypes.length) % searchGroupTypes.length - if (infiniteListCounts[searchGroupTypes[newIndex]] > 0) { - actions.setActiveTab(searchGroupTypes[newIndex]) - return - } - } - }, - - tabRight: () => { - const { currentTabIndex, searchGroupTypes, infiniteListCounts } = values - for (let i = 1; i < searchGroupTypes.length; i++) { - const newIndex = (currentTabIndex + i) % searchGroupTypes.length - if (infiniteListCounts[searchGroupTypes[newIndex]] > 0) { - actions.setActiveTab(searchGroupTypes[newIndex]) - return - } - } - }, - - setSearchQuery: () => { - const { activeTaxonomicGroup, infiniteListCounts } = values - - // Taxonomic group with a local data source, zero results after searching. - // Open the next tab. - if ( - activeTaxonomicGroup && - !activeTaxonomicGroup.endpoint && - infiniteListCounts[activeTaxonomicGroup.type] === 0 - ) { - actions.tabRight() - } - }, - - searchListResultsReceived: ({ groupType, results }) => { - // Open the next tab if no results on an active tab. - if (groupType === values.activeTab && !results.count) { - actions.tabRight() - } - }, - }), -}) From 6cd7323f7fe990f7341346f64c38434e98897870 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 14 Apr 2022 10:49:20 +0100 Subject: [PATCH 15/18] fix errors --- frontend/src/layout/navigation/TopBar/TopBar.tsx | 12 ++++-------- .../UniversalSearch/UniversalSearchPopup.tsx | 3 ++- frontend/src/models/groupsModel.ts | 11 ++++++++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index cd6ff4266ab02..651da823529e4 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -14,8 +14,8 @@ import { CreateProjectModal } from '../../../scenes/project/CreateProjectModal' import './TopBar.scss' import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' import { UniversalSearchPopup } from 'lib/components/UniversalSearch/UniversalSearchPopup' -import { TaxonomicFilterGroupType } from 'lib/components/UniversalSearch/types' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { groupsModel } from '~/models/groupsModel' export function TopBar(): JSX.Element { const { isSideBarShown, bareNav, mobileLayout, isCreateOrganizationModalShown, isCreateProjectModalShown } = @@ -25,6 +25,8 @@ export function TopBar(): JSX.Element { const { isInviteModalShown } = useValues(inviteLogic) const { hideInviteModal } = useActions(inviteLogic) + const { groupNamesTaxonomicTypes } = useValues(groupsModel) + return ( <> @@ -54,13 +56,7 @@ export function TopBar(): JSX.Element { TaxonomicFilterGroupType.Plugins, TaxonomicFilterGroupType.Experiments, TaxonomicFilterGroupType.Dashboards, - // TaxonomicFilterGroupType.GroupsPrefix, - 'groups_0', - 'groups_1', - 'name_groups_0', - 'name_groups_1', - 'name_groups_2', - // // 'groups_2', + ...groupNamesTaxonomicTypes, ]} />
diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 62100901db8c8..6b5c62bb28268 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -138,6 +138,7 @@ export function UniversalSearchPopup({ const logic = taxonomicFilterLogic(universalSearchLogicProps) const { searchQuery, searchPlaceholder } = useValues(logic) + // Command+S shortcut to get to universal search useEventListener('keydown', (event) => { if (event.key === 's' && (event.ctrlKey || event.metaKey)) { event.preventDefault() @@ -163,8 +164,8 @@ export function UniversalSearchPopup({ modifier={{ name: 'offset', options: { + // @ts-ignore offset: ({ placement }) => { - // eslint-disable-line if (placement === 'right-start') { return [-10, -249 - 243] } else { diff --git a/frontend/src/models/groupsModel.ts b/frontend/src/models/groupsModel.ts index d51d5a723a9d9..593e8063a5a18 100644 --- a/frontend/src/models/groupsModel.ts +++ b/frontend/src/models/groupsModel.ts @@ -43,7 +43,16 @@ export const groupsModel = kea({ (groupTypes): TaxonomicFilterGroupType[] => { return groupTypes.map( (groupType: GroupType) => - `${TaxonomicFilterGroupType.GroupsPrefix}_${groupType.group_type_index}` as TaxonomicFilterGroupType + `${TaxonomicFilterGroupType.GroupsPrefix}_${groupType.group_type_index}` as unknown as TaxonomicFilterGroupType + ) + }, + ], + groupNamesTaxonomicTypes: [ + (s) => [s.groupTypes], + (groupTypes): TaxonomicFilterGroupType[] => { + return groupTypes.map( + (groupType: GroupType) => + `${TaxonomicFilterGroupType.GroupNamesPrefix}_${groupType.group_type_index}` as unknown as TaxonomicFilterGroupType ) }, ], From 1f37a1c07fd433898eb44484b4e2448daf226dd7 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 14 Apr 2022 11:10:45 +0100 Subject: [PATCH 16/18] introduce feature flag, clean up remaining --- ee/clickhouse/views/groups.py | 3 - .../src/layout/navigation/TopBar/TopBar.scss | 39 +++++++++ .../src/layout/navigation/TopBar/TopBar.tsx | 44 ++++++---- .../UniversalSearch/SearchList.scss | 84 ------------------- .../UniversalSearch/UniversalSearch.scss | 66 +-------------- .../UniversalSearch/UniversalSearchPopup.tsx | 4 +- frontend/src/lib/constants.tsx | 1 + 7 files changed, 70 insertions(+), 171 deletions(-) delete mode 100644 frontend/src/lib/components/UniversalSearch/SearchList.scss diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 7313447639a34..6182e72fa2b60 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -60,9 +60,6 @@ class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewse queryset = Group.objects.all() pagination_class = GroupCursorPagination permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] - # filter_backends = [filters.SearchFilter] - # filterset_fields = ["group_type_index"] - # search_fields = ["group_key", "group_properties"] def get_queryset(self): return ( diff --git a/frontend/src/layout/navigation/TopBar/TopBar.scss b/frontend/src/layout/navigation/TopBar/TopBar.scss index 33be943823fb3..4a4f232788034 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.scss +++ b/frontend/src/layout/navigation/TopBar/TopBar.scss @@ -95,6 +95,45 @@ } } +.SearchBox { + transition: background-color 200ms ease, border-color 200ms ease; + display: none; + align-items: center; + width: 15rem; + height: 100%; + padding: 0 0.625rem; + border-radius: var(--radius); + border: 1px solid var(--border-dark); + color: var(--text-muted); + user-select: none; + cursor: pointer; + &:hover { + background-color: var(--bg-shaded); + } + &:active { + border-color: var(--border-active); + } + @media screen and (min-width: $sm) { + display: flex; + } +} + +.SearchBox__primary-area { + flex-grow: 1; + display: flex; + align-items: center; +} + +.SearchBox__magnifier { + color: var(--text-default); + font-size: 1rem; + padding-right: 0.5rem; +} + +.SearchBox__keyboard-shortcut { + font-weight: 600; +} + .SitePopover { max-width: 22rem; } diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 651da823529e4..b9c7df7f27db5 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -16,6 +16,9 @@ import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' import { UniversalSearchPopup } from 'lib/components/UniversalSearch/UniversalSearchPopup' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { groupsModel } from '~/models/groupsModel' +import { SearchBox } from './SearchBox' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' export function TopBar(): JSX.Element { const { isSideBarShown, bareNav, mobileLayout, isCreateOrganizationModalShown, isCreateProjectModalShown } = @@ -24,7 +27,7 @@ export function TopBar(): JSX.Element { useActions(navigationLogic) const { isInviteModalShown } = useValues(inviteLogic) const { hideInviteModal } = useActions(inviteLogic) - + const { featureFlags } = useValues(featureFlagLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) return ( @@ -43,23 +46,28 @@ export function TopBar(): JSX.Element { -
- -
+ + {featureFlags[FEATURE_FLAGS.UNIVERSAL_SEARCH] ? ( +
+ +
+ ) : ( + + )}
diff --git a/frontend/src/lib/components/UniversalSearch/SearchList.scss b/frontend/src/lib/components/UniversalSearch/SearchList.scss deleted file mode 100644 index 62e1109f87c43..0000000000000 --- a/frontend/src/lib/components/UniversalSearch/SearchList.scss +++ /dev/null @@ -1,84 +0,0 @@ -.taxonomic-infinite-list { - min-height: 200px; - - &.empty-infinite-list { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - .no-infinite-results { - color: #666; - } - } - - .taxonomic-list-row { - display: flex; - align-items: center; - justify-content: space-between; - color: #2d2d2d; - padding: 4px 12px; - cursor: pointer; - border: none; - - .taxonomic-list-row-contents { - display: flex; - align-items: center; - - .taxonomic-list-row-contents-icon { - width: 30px; - - svg.taxonomy-icon { - vertical-align: middle; - flex-shrink: 0; - height: 18.5px; - - &.taxonomy-icon-muted { - path { - fill: var(--taxonomy-icon-muted); - } - } - - &.taxonomy-icon-verified { - height: 24px; - path { - fill: var(--success); - } - - &:not(.taxonomy-icon-ph) { - margin-bottom: -4px; // Slightly larger height requires offset to look vertically centered - } - } - } - } - } - - & > div { - max-width: 100%; - - & > span { - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - &.hover { - background: rgba(0, 0, 0, 0.1); - } - - &.selected { - font-weight: bold; - } - - &.skeleton-row { - // center-align this antd skeleton - .ant-skeleton-paragraph { - margin-bottom: 0; - } - } - &.expand-row { - color: var(--primary); - } - } -} diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss index a58a909d4ce7d..3e3bf4517e4e3 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss @@ -1,23 +1,8 @@ @import '~/vars'; -.SearchBox { - // transition: background-color 200ms ease, border-color 200ms ease; - // display: none; - // align-items: center; +.universal-search-box { max-width: 483px; - // height: 100%; - // padding: 0 0.625rem; - // border-radius: var(--radius); - // border: 1px solid var(--border-dark); - // color: var(--text-muted); - // user-select: none; cursor: pointer; - // &:hover { - // background-color: var(--bg-shaded); - // } - // &:active { - // border-color: var(--border-active); - // } transition: 200ms ease margin; .ant-input-affix-wrapper, @@ -25,7 +10,7 @@ background: var(--bg-bridge); } - &.SearchBox--sidebar-shown { + &.universal-search-box--sidebar-shown { margin-left: 55px; } @media screen and (min-width: $sm) { @@ -63,51 +48,4 @@ margin-top: 10px; } } - - .taxonomic-group-title { - display: flex; - width: 100%; - align-items: stretch; - color: var(--text-muted); - text-transform: uppercase; - font-size: 12px; - line-height: 12px; - padding-top: 16px; - margin-bottom: 12px; - font-weight: 600; - &.with-border { - border-top: 1px solid var(--border-light); - } - } - - .taxonomic-pills { - margin-top: 8px; - margin-bottom: 8px; - .ant-tag { - transition: none; - margin-right: 2px; - margin-bottom: 2px; - cursor: pointer; - color: var(--primary); - background: var(--bg-side); - border-color: var(--bg-side); - &.taxonomic-count-zero { - color: var(--text-muted); - cursor: not-allowed; - } - &.taxonomic-pill-active { - color: var(--text-light); - background: var(--primary); - border-color: var(--primary); - } - } - } - - .magnifier-icon { - font-size: 18px; - color: var(--text-muted); - &.magnifier-icon-active { - color: var(--primary); - } - } } diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 6b5c62bb28268..b9b50407088b1 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -187,8 +187,8 @@ export function UniversalSearchPopup({ className={clsx( { 'full-width': fullWidth }, '', - 'SearchBox', - isSideBarShown && 'SearchBox--sidebar-shown' + 'universal-search-box', + isSideBarShown && 'universal-search-box--sidebar-shown' )} style={style} > diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 352f3941ac7f1..ddcd75540b6d6 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -116,6 +116,7 @@ export const FEATURE_FLAGS = { FEATURE_FLAGS_ACTIVITY_LOG: '8545-ff-activity-log', // owner: @pauldambra SMOOTHING_INTERVAL: 'smoothing-interval', // owner: @timgl TUNE_RECORDING_SNAPSHOT_LIMIT: 'tune-recording-snapshot-limit', // owner: @rcmarron + UNIVERSAL_SEARCH: 'universal-search', // owner: @neilkakkar } /** Which self-hosted plan's features are available with Cloud's "Standard" plan (aka card attached). */ From c647b6a46741eb40f61ef37cfbb4fd0cb597c934 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Fri, 15 Apr 2022 10:13:57 +0100 Subject: [PATCH 17/18] move experiments and plugins elsewhere --- .../lib/components/TaxonomicFilter/infiniteListLogic.ts | 7 ------- .../components/UniversalSearch/UniversalSearchPopup.tsx | 7 ++++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts index 3e86a0ab0e01f..cf7fd19044bdb 100644 --- a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts +++ b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts @@ -15,8 +15,6 @@ import { } from 'lib/components/TaxonomicFilter/types' import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' -import { experimentsLogic } from 'scenes/experiments/experimentsLogic' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' /* by default the pop-up starts open for the first item in the list @@ -70,16 +68,11 @@ export const infiniteListLogic = kea({ key: (props) => `${props.taxonomicFilterLogicKey}-${props.listGroupType}`, connect: (props: InfiniteListLogicProps) => ({ - // TODO: had to connect FF to get the model loaded for filtering values: [ taxonomicFilterLogic(props), ['searchQuery', 'value', 'groupType', 'taxonomicGroups'], featureFlagsLogic, ['featureFlags'], - experimentsLogic, - ['experiments'], - pluginsLogic, - ['plugins'], ], actions: [taxonomicFilterLogic(props), ['setSearchQuery', 'selectItem', 'infiniteListResultsReceived']], }), diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index b9b50407088b1..80b55bcdd7c7c 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -17,7 +17,7 @@ import { InsightType, PersonType, } from '~/types' -import { PluginSelectionType } from 'scenes/plugins/pluginsLogic' +import { PluginSelectionType, pluginsLogic } from 'scenes/plugins/pluginsLogic' import clsx from 'clsx' import { navigationLogic } from '~/layout/navigation/navigationLogic' import { useValues } from 'kea' @@ -26,6 +26,7 @@ import { Input } from 'antd' import { useEventListener } from 'lib/hooks/useEventListener' import { taxonomicFilterLogic } from '../TaxonomicFilter/taxonomicFilterLogic' import { TaxonomicFilter } from '../TaxonomicFilter/TaxonomicFilter' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' export interface UniversalSearchPopupProps extends Omit { @@ -146,6 +147,10 @@ export function UniversalSearchPopup({ } }) + // Ensure some logics are mounted + useValues(experimentsLogic) + useValues(pluginsLogic) + return (
Date: Fri, 15 Apr 2022 12:19:07 +0100 Subject: [PATCH 18/18] address comment --- .../UniversalSearch/UniversalSearchPopup.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx index 80b55bcdd7c7c..51c43e5d1c98d 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopup.tsx @@ -20,7 +20,7 @@ import { import { PluginSelectionType, pluginsLogic } from 'scenes/plugins/pluginsLogic' import clsx from 'clsx' import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { useValues } from 'kea' +import { useMountedLogic, useValues } from 'kea' import { IconMagnifier } from '../icons' import { Input } from 'antd' import { useEventListener } from 'lib/hooks/useEventListener' @@ -119,6 +119,10 @@ export function UniversalSearchPopup({ style, fullWidth = true, }: UniversalSearchPopupProps): JSX.Element { + // Ensure some logics are mounted + useMountedLogic(experimentsLogic) + useMountedLogic(pluginsLogic) + const [visible, setVisible] = useState(false) const { isSideBarShown } = useValues(navigationLogic) @@ -147,10 +151,6 @@ export function UniversalSearchPopup({ } }) - // Ensure some logics are mounted - useValues(experimentsLogic) - useValues(pluginsLogic) - return (