From 77ca1d2cf3f55a5858ff8de953e451c14a1e6c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Thu, 16 Nov 2023 16:36:22 +0100 Subject: [PATCH 01/24] allow selecting search entities --- posthog/api/search.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index e346c3a905737..9c580696be0e7 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -4,7 +4,7 @@ from django.db.models import Model, Value, CharField, F, QuerySet from django.db.models.functions import Cast from django.http import HttpResponse -from rest_framework import viewsets +from rest_framework import viewsets, serializers from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -15,21 +15,44 @@ LIMIT = 25 +ENTITIES = [Action, Cohort, Insight, Dashboard, FeatureFlag, Experiment] + + +def class_to_type(klass: type[Model]): + """Converts the class name to snake case.""" + return re.sub("(?!^)([A-Z]+)", r"_\1", klass.__name__).lower() + + +entity_map = {class_to_type(entity): {"klass": entity} for entity in ENTITIES} + + +class QuerySerializer(serializers.Serializer): + q = serializers.CharField(required=False, default="") + entities = serializers.MultipleChoiceField(required=False, choices=[class_to_type(entity) for entity in ENTITIES]) + + def validate_q(self, value: str | None): + return process_query(value) + class SearchViewSet(StructuredViewSetMixin, viewsets.ViewSet): permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] def list(self, request: Request, **kw) -> HttpResponse: - query = process_query(request.GET.get("q", "").strip()) + query_serializer = QuerySerializer(data=self.request.query_params) + query_serializer.is_valid(raise_exception=True) + params = query_serializer.validated_data + counts = {} + entities = params["entities"] if len(params["entities"]) > 0 else set(entity_map.keys()) + query = params["q"] # empty queryset to union things onto it qs = Dashboard.objects.annotate(type=Value("empty", output_field=CharField())).filter(team=self.team).none() - for klass in (Action, Cohort, Insight, Dashboard, Experiment, FeatureFlag): - klass_qs, type = class_queryset(klass, team=self.team, query=query) + for klass in [entity_map[entity]["klass"] for entity in entities]: + klass_qs, entity_name = class_queryset(klass, team=self.team, query=query) qs = qs.union(klass_qs) - counts[type] = klass_qs.count() + counts[entity_name] = klass_qs.count() if query: qs = qs.order_by("-rank") @@ -78,8 +101,3 @@ def class_queryset(klass: type[Model], team: Team, query: str | None): qs = qs.values(*values) return qs, type - - -def class_to_type(klass: type[Model]): - """Converts the class name to snake case.""" - return re.sub("(?!^)([A-Z]+)", r"_\1", klass.__name__).lower() From 6b5dc53da6c0792a70dca6f15eb191154b79a21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 10:48:16 +0100 Subject: [PATCH 02/24] implement extra_fields and entity filtering --- posthog/api/search.py | 73 ++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index 9c580696be0e7..8da9b8bd8085a 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -2,7 +2,7 @@ from typing import Any from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.db.models import Model, Value, CharField, F, QuerySet -from django.db.models.functions import Cast +from django.db.models.functions import Cast, JSONObject from django.http import HttpResponse from rest_framework import viewsets, serializers from rest_framework.permissions import IsAuthenticated @@ -15,20 +15,22 @@ LIMIT = 25 -ENTITIES = [Action, Cohort, Insight, Dashboard, FeatureFlag, Experiment] - -def class_to_type(klass: type[Model]): - """Converts the class name to snake case.""" - return re.sub("(?!^)([A-Z]+)", r"_\1", klass.__name__).lower() - - -entity_map = {class_to_type(entity): {"klass": entity} for entity in ENTITIES} +ENTITY_MAP = { + "action": {"klass": Action}, + "cohort": {"klass": Cohort}, + "insight": {"klass": Insight, "extra_fields": ["derived_name"]}, + "dashboard": {"klass": Dashboard}, + "experiment": {"klass": Experiment}, + "feature_flag": {"klass": FeatureFlag}, +} class QuerySerializer(serializers.Serializer): + """Validates and formats query params.""" + q = serializers.CharField(required=False, default="") - entities = serializers.MultipleChoiceField(required=False, choices=[class_to_type(entity) for entity in ENTITIES]) + entities = serializers.MultipleChoiceField(required=False, choices=list(ENTITY_MAP.keys())) def validate_q(self, value: str | None): return process_query(value) @@ -38,22 +40,31 @@ class SearchViewSet(StructuredViewSetMixin, viewsets.ViewSet): permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] def list(self, request: Request, **kw) -> HttpResponse: + # parse query params query_serializer = QuerySerializer(data=self.request.query_params) query_serializer.is_valid(raise_exception=True) params = query_serializer.validated_data counts = {} - entities = params["entities"] if len(params["entities"]) > 0 else set(entity_map.keys()) + # get entities to search from params or default to all entities + entities = params["entities"] if len(params["entities"]) > 0 else set(ENTITY_MAP.keys()) query = params["q"] # empty queryset to union things onto it qs = Dashboard.objects.annotate(type=Value("empty", output_field=CharField())).filter(team=self.team).none() - for klass in [entity_map[entity]["klass"] for entity in entities]: - klass_qs, entity_name = class_queryset(klass, team=self.team, query=query) + # add entities + for entity_meta in [ENTITY_MAP[entity] for entity in entities]: + klass_qs, entity_name = class_queryset( + klass=entity_meta.get("klass"), + team=self.team, + query=query, + extra_fields=entity_meta.get("extra_fields"), + ) qs = qs.union(klass_qs) counts[entity_name] = klass_qs.count() + # order by rank if query: qs = qs.order_by("-rank") @@ -77,27 +88,47 @@ def process_query(query: str): return query -def class_queryset(klass: type[Model], team: Team, query: str | None): +def class_queryset( + klass: type[Model], + team: Team, + query: str | None, + extra_fields: dict | None, +): """Builds a queryset for the class.""" - type = class_to_type(klass) - values = ["type", "result_id", "name"] + entity_type = class_to_entity_name(klass) + values = ["type", "result_id", "name", "extra_fields"] - qs: QuerySet[Any] = klass.objects.filter(team=team) - qs = qs.annotate(type=Value(type, output_field=CharField())) + qs: QuerySet[Any] = klass.objects.filter(team=team) # filter team + qs = qs.annotate(type=Value(entity_type, output_field=CharField())) # entity type - if type == "insight": + # entity id + if entity_type == "insight": qs = qs.annotate(result_id=F("short_id")) else: qs = qs.annotate(result_id=Cast("pk", CharField())) + # extra fields + if extra_fields: + qs = qs.annotate(extra_fields=JSONObject(**{field: field for field in extra_fields})) + else: + qs = qs.annotate(extra_fields=JSONObject()) + + # full-text search rank if query: qs = qs.annotate( rank=SearchRank( SearchVector("name", config="simple"), SearchQuery(query, config="simple", search_type="raw") - ) + ), ) qs = qs.filter(rank__gt=0.05) values.append("rank") + # specify fields to fetch qs = qs.values(*values) - return qs, type + + return qs, entity_type + + +def class_to_entity_name(klass: type[Model]): + """Converts the class name to snake case.""" + return re.sub("(?!^)([A-Z]+)", r"_\1", klass.__name__).lower() From 23469aa2a1a1fb0b274059d622d14d5b4649138c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 11:22:43 +0100 Subject: [PATCH 03/24] add tests --- posthog/api/search.py | 2 +- posthog/api/test/test_search.py | 58 ++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index 8da9b8bd8085a..0fad20e00c128 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -32,7 +32,7 @@ class QuerySerializer(serializers.Serializer): q = serializers.CharField(required=False, default="") entities = serializers.MultipleChoiceField(required=False, choices=list(ENTITY_MAP.keys())) - def validate_q(self, value: str | None): + def validate_q(self, value: str): return process_query(value) diff --git a/posthog/api/test/test_search.py b/posthog/api/test/test_search.py index 3324dc18db6f7..3a5c0e4f2e69b 100644 --- a/posthog/api/test/test_search.py +++ b/posthog/api/test/test_search.py @@ -5,15 +5,22 @@ from posthog.api.search import process_query from posthog.test.base import APIBaseTest -from posthog.models import Dashboard, FeatureFlag, Team +from posthog.models import Dashboard, FeatureFlag, Team, Insight class TestSearch(APIBaseTest): + insight_1: Insight + dashboard_1: Dashboard + def setUp(self): super().setUp() + Insight.objects.create(team=self.team, derived_name="derived name") + self.insight_1 = Insight.objects.create(team=self.team, name="second insight") + Insight.objects.create(team=self.team, name="third insight") + Dashboard.objects.create(team=self.team, created_by=self.user) - Dashboard.objects.create(name="second dashboard", team=self.team, created_by=self.user) + self.dashboard_1 = Dashboard.objects.create(name="second dashboard", team=self.team, created_by=self.user) Dashboard.objects.create(name="third dashboard", team=self.team, created_by=self.user) FeatureFlag.objects.create(key="a", team=self.team, created_by=self.user) @@ -24,25 +31,66 @@ def test_search(self): response = self.client.get("/api/projects/@current/search?q=sec") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 2) + self.assertEqual(len(response.json()["results"]), 3) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 1) self.assertEqual(response.json()["counts"]["feature_flag"], 1) + self.assertEqual(response.json()["counts"]["insight"], 1) def test_search_without_query(self): response = self.client.get("/api/projects/@current/search") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 6) + self.assertEqual(len(response.json()["results"]), 9) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 3) self.assertEqual(response.json()["counts"]["feature_flag"], 3) + self.assertEqual(response.json()["counts"]["insight"], 3) + + def test_search_filtered_by_entity(self): + response = self.client.get("/api/projects/@current/search?q=sec&entities=insight&entities=dashboard") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()["results"]), 2) + self.assertEqual(response.json()["counts"]["dashboard"], 1) + self.assertEqual(response.json()["counts"]["insight"], 1) + + def test_response_format_and_ids(self): + response = self.client.get("/api/projects/@current/search?q=sec&entities=insight&entities=dashboard") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["results"][0], + { + "name": "second dashboard", + "rank": response.json()["results"][0]["rank"], + "type": "dashboard", + "result_id": str(self.dashboard_1.id), + "extra_fields": {}, + }, + ) + self.assertEqual( + response.json()["results"][1], + { + "name": "second insight", + "rank": response.json()["results"][1]["rank"], + "type": "insight", + "result_id": self.insight_1.short_id, + "extra_fields": {"derived_name": None}, + }, + ) + + def test_extra_fields(self): + response = self.client.get("/api/projects/@current/search?entities=insight") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["results"][0]["extra_fields"], {"derived_name": "derived name"}) def test_search_with_fully_invalid_query(self): response = self.client.get("/api/projects/@current/search?q=%3E") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 6) + self.assertEqual(len(response.json()["results"]), 9) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 3) self.assertEqual(response.json()["counts"]["feature_flag"], 3) From 05bfe5523ac73c81aacc24aa25324ead63f15afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 13:36:39 +0100 Subject: [PATCH 04/24] mypy ignores --- posthog/api/search.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index 0fad20e00c128..d7c9d88c21c2f 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -2,7 +2,7 @@ from typing import Any from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.db.models import Model, Value, CharField, F, QuerySet -from django.db.models.functions import Cast, JSONObject +from django.db.models.functions import Cast, JSONObject # type: ignore from django.http import HttpResponse from rest_framework import viewsets, serializers from rest_framework.permissions import IsAuthenticated @@ -56,10 +56,10 @@ def list(self, request: Request, **kw) -> HttpResponse: # add entities for entity_meta in [ENTITY_MAP[entity] for entity in entities]: klass_qs, entity_name = class_queryset( - klass=entity_meta.get("klass"), + klass=entity_meta.get("klass"), # type: ignore team=self.team, query=query, - extra_fields=entity_meta.get("extra_fields"), + extra_fields=entity_meta.get("extra_fields"), # type: ignore ) qs = qs.union(klass_qs) counts[entity_name] = klass_qs.count() From 3d903ff4592f9eb5c6939e53ce4631f0bb3b8228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 14:13:29 +0100 Subject: [PATCH 05/24] fix name for insights and feature flags --- .../components/CommandBar/SearchResult.tsx | 19 +++++++++++++++++-- .../src/lib/components/CommandBar/types.ts | 7 ++++++- posthog/api/search.py | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index af88481772ff5..ba567025c5907 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -3,7 +3,7 @@ import { useActions, useValues } from 'kea' import { resultTypeToName } from './constants' import { searchBarLogic, urlForResult } from './searchBarLogic' -import { SearchResult as SearchResultType } from './types' +import { SearchResult, SearchResult as SearchResultType } from './types' import { LemonSkeleton } from '@posthog/lemon-ui' type SearchResultProps = { @@ -63,7 +63,9 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: >
{resultTypeToName[result.type]} - {result.name} + + + {location.host} {urlForResult(result)} @@ -82,3 +84,16 @@ export const SearchResultSkeleton = (): JSX.Element => (
) + +type ResultNameProps = { + result: SearchResult +} +const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { + if (result.type === 'insight') { + return result.name ? {result.name} : {result.extra_fields.derived_name} + } else if (result.type === 'feature_flag') { + return result.name ? {result.name} : {result.extra_fields.key} + } else { + return {result.name} + } +} diff --git a/frontend/src/lib/components/CommandBar/types.ts b/frontend/src/lib/components/CommandBar/types.ts index 4afc7e3ff66d2..7fff9c5c5ddfe 100644 --- a/frontend/src/lib/components/CommandBar/types.ts +++ b/frontend/src/lib/components/CommandBar/types.ts @@ -8,7 +8,12 @@ export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experi export type ResultTypeWithAll = ResultType | 'all' -export type SearchResult = { result_id: string; type: ResultType; name: string | null } +export type SearchResult = { + result_id: string + type: ResultType + name: string | null + extra_fields: Record +} export type SearchResponse = { results: SearchResult[] diff --git a/posthog/api/search.py b/posthog/api/search.py index d7c9d88c21c2f..1bcbec182bad7 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -22,7 +22,7 @@ "insight": {"klass": Insight, "extra_fields": ["derived_name"]}, "dashboard": {"klass": Dashboard}, "experiment": {"klass": Experiment}, - "feature_flag": {"klass": FeatureFlag}, + "feature_flag": {"klass": FeatureFlag, "extra_fields": ["key"]}, } From c04cfdfbd25463aa39f95872627b6f14b9899753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 14:13:42 +0100 Subject: [PATCH 06/24] styling improvements --- frontend/src/lib/components/CommandBar/ActionResult.tsx | 9 +++------ frontend/src/lib/components/CommandBar/ActionResults.tsx | 4 +++- frontend/src/lib/components/CommandBar/CommandBar.tsx | 4 +++- frontend/src/lib/components/CommandBar/SearchResult.tsx | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/ActionResult.tsx b/frontend/src/lib/components/CommandBar/ActionResult.tsx index a67082f3dfc98..5835595074291 100644 --- a/frontend/src/lib/components/CommandBar/ActionResult.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResult.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef } from 'react' import { useActions } from 'kea' import { actionBarLogic } from './actionBarLogic' -import { getNameFromActionScope } from './utils' import { CommandResultDisplayable } from '../CommandPalette/commandPaletteLogic' type SearchResultProps = { @@ -41,11 +40,9 @@ export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Elemen }} ref={ref} > -
- {result.source.scope && ( - {getNameFromActionScope(result.source.scope)} - )} - {result.display} +
+ + {result.display}
diff --git a/frontend/src/lib/components/CommandBar/ActionResults.tsx b/frontend/src/lib/components/CommandBar/ActionResults.tsx index 059954357506e..a8d15abde7f17 100644 --- a/frontend/src/lib/components/CommandBar/ActionResults.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResults.tsx @@ -15,7 +15,9 @@ type ResultsGroupProps = { const ResultsGroup = ({ scope, results, activeResultIndex }: ResultsGroupProps): JSX.Element => { return ( <> -
{getNameFromActionScope(scope)}
+
+ {getNameFromActionScope(scope)} +
{results.map((result) => (
{barStatus === BarStatus.SHOW_SEARCH ? : } diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index ba567025c5907..d461195ac5a0c 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -63,7 +63,7 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: >
{resultTypeToName[result.type]} - + From 99ea80d33e7d6dba927c0dfd35a61e02238c275a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 14:15:53 +0100 Subject: [PATCH 07/24] fix type import --- frontend/src/lib/components/CommandBar/SearchResult.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index d461195ac5a0c..7217807010825 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -3,7 +3,7 @@ import { useActions, useValues } from 'kea' import { resultTypeToName } from './constants' import { searchBarLogic, urlForResult } from './searchBarLogic' -import { SearchResult, SearchResult as SearchResultType } from './types' +import { SearchResult as SearchResultType } from './types' import { LemonSkeleton } from '@posthog/lemon-ui' type SearchResultProps = { @@ -86,7 +86,7 @@ export const SearchResultSkeleton = (): JSX.Element => ( ) type ResultNameProps = { - result: SearchResult + result: SearchResultType } const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { if (result.type === 'insight') { From b45af88f120ea0aabb898fbbca6ea181f0216ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 15:31:16 +0100 Subject: [PATCH 08/24] active tab handling --- .../lib/components/CommandBar/searchBarLogic.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts index fd69e079ec1a5..77eb9cd127774 100644 --- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts @@ -25,17 +25,23 @@ export const searchBarLogic = kea([ setIsAutoScrolling: (scrolling: boolean) => ({ scrolling }), openResult: (index: number) => ({ index }), }), - loaders({ + loaders(({ values }) => ({ searchResponse: [ null as SearchResponse | null, { - setSearchQuery: async ({ query }, breakpoint) => { + loadSearchResponse: async (_, breakpoint) => { await breakpoint(300) - return await api.get(`api/projects/@current/search?q=${query}`) + if (values.activeTab === 'all') { + return await api.get(`api/projects/@current/search?q=${values.searchQuery}`) + } else { + return await api.get( + `api/projects/@current/search?q=${values.searchQuery}&entities=${values.activeTab}` + ) + } }, }, ], - }), + })), reducers({ searchQuery: ['', { setSearchQuery: (_, { query }) => query }], keyboardResultIndex: [ @@ -92,6 +98,8 @@ export const searchBarLogic = kea([ router.actions.push(urlForResult(result)) actions.hideCommandBar() }, + setSearchQuery: actions.loadSearchResponse, + setActiveTab: actions.loadSearchResponse, })), afterMount(({ actions, values, cache }) => { // load initial results From df091c63f199aeb937e4b915495ca2f42cbd2d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 15:36:12 +0100 Subject: [PATCH 09/24] add count defaults --- posthog/api/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index 1bcbec182bad7..add18be9c9307 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -45,7 +45,7 @@ def list(self, request: Request, **kw) -> HttpResponse: query_serializer.is_valid(raise_exception=True) params = query_serializer.validated_data - counts = {} + counts = {key: None for key in ENTITY_MAP} # get entities to search from params or default to all entities entities = params["entities"] if len(params["entities"]) > 0 else set(ENTITY_MAP.keys()) query = params["q"] From 5e8a84960914e6a734392621f7f994cf105a8af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 15:53:08 +0100 Subject: [PATCH 10/24] better loading state --- .../components/CommandBar/SearchBarTab.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx index f218824187a26..4f8cf76d1472f 100644 --- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx @@ -1,8 +1,9 @@ -import { useActions } from 'kea' +import { useActions, useValues } from 'kea' import { resultTypeToName } from './constants' import { searchBarLogic } from './searchBarLogic' import { ResultTypeWithAll } from './types' +import { Spinner } from 'lib/lemon-ui/Spinner' type SearchBarTabProps = { type: ResultTypeWithAll @@ -18,7 +19,27 @@ export const SearchBarTab = ({ type, active, count }: SearchBarTabProps): JSX.El onClick={() => setActiveTab(type)} > {resultTypeToName[type]} - {count != null && {count}} +
) } + +type CountProps = { + type: ResultTypeWithAll + active: boolean + count?: number | null +} + +const Count = ({ type, active, count }: CountProps): JSX.Element | null => { + const { searchResponseLoading } = useValues(searchBarLogic) + + if (type === 'all') { + return null + } else if (active && searchResponseLoading) { + return + } else if (count != null) { + return {count} + } else { + return + } +} From d187ebecf9d4fd5ae2728ae003ed41c0ab86e61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 16:01:11 +0100 Subject: [PATCH 11/24] focus input after tab click --- .../src/lib/components/CommandBar/SearchBar.tsx | 7 +++++-- .../lib/components/CommandBar/SearchBarTab.tsx | 8 ++++++-- .../lib/components/CommandBar/SearchInput.tsx | 6 ++++-- .../lib/components/CommandBar/SearchTabs.tsx | 17 ++++++++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchBar.tsx b/frontend/src/lib/components/CommandBar/SearchBar.tsx index fad41fbfa7978..1fa53068dc031 100644 --- a/frontend/src/lib/components/CommandBar/SearchBar.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBar.tsx @@ -1,4 +1,5 @@ import { useMountedLogic } from 'kea' +import { useRef } from 'react' import { searchBarLogic } from './searchBarLogic' @@ -9,11 +10,13 @@ import { SearchTabs } from './SearchTabs' export const SearchBar = (): JSX.Element => { useMountedLogic(searchBarLogic) // load initial results + const inputRef = useRef(null) + return (
- + - +
) } diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx index 4f8cf76d1472f..9521fa02a600d 100644 --- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx @@ -9,14 +9,18 @@ type SearchBarTabProps = { type: ResultTypeWithAll active: boolean count?: number | null + inputRef: Ref } -export const SearchBarTab = ({ type, active, count }: SearchBarTabProps): JSX.Element => { +export const SearchBarTab = ({ type, active, count, inputRef }: SearchBarTabProps): JSX.Element => { const { setActiveTab } = useActions(searchBarLogic) return (
setActiveTab(type)} + onClick={() => { + setActiveTab(type) + inputRef.current?.focus() + }} > {resultTypeToName[type]} diff --git a/frontend/src/lib/components/CommandBar/SearchInput.tsx b/frontend/src/lib/components/CommandBar/SearchInput.tsx index 40c08c942c5bb..1418e84b3607b 100644 --- a/frontend/src/lib/components/CommandBar/SearchInput.tsx +++ b/frontend/src/lib/components/CommandBar/SearchInput.tsx @@ -4,14 +4,16 @@ import { LemonInput } from '@posthog/lemon-ui' import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { searchBarLogic } from './searchBarLogic' +import { forwardRef, Ref } from 'react' -export const SearchInput = (): JSX.Element => { +export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref): JSX.Element { const { searchQuery } = useValues(searchBarLogic) const { setSearchQuery } = useActions(searchBarLogic) return (
{ />
) -} +}) diff --git a/frontend/src/lib/components/CommandBar/SearchTabs.tsx b/frontend/src/lib/components/CommandBar/SearchTabs.tsx index bc83ed1110a76..984857305b6bb 100644 --- a/frontend/src/lib/components/CommandBar/SearchTabs.tsx +++ b/frontend/src/lib/components/CommandBar/SearchTabs.tsx @@ -1,10 +1,15 @@ import { useValues } from 'kea' +import { Ref } from 'react' import { searchBarLogic } from './searchBarLogic' import { SearchBarTab } from './SearchBarTab' import { ResultType } from './types' -export const SearchTabs = (): JSX.Element | null => { +type SearchTabsProps = { + inputRef: Ref +} + +export const SearchTabs = ({ inputRef }: SearchTabsProps): JSX.Element | null => { const { searchResponse, activeTab } = useValues(searchBarLogic) if (!searchResponse) { @@ -13,9 +18,15 @@ export const SearchTabs = (): JSX.Element | null => { return (
- + {Object.entries(searchResponse.counts).map(([type, count]) => ( - + ))}
) From d3dd7981a0320ef79943d326f9494d3ef7f2a37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 16:54:44 +0100 Subject: [PATCH 12/24] add notebooks and weighted search fields --- .../components/CommandBar/SearchBarTab.tsx | 1 + .../components/CommandBar/SearchResult.tsx | 13 ++++--- .../lib/components/CommandBar/constants.ts | 1 + .../components/CommandBar/searchBarLogic.ts | 2 ++ .../src/lib/components/CommandBar/types.ts | 2 +- posthog/api/search.py | 36 +++++++++++++------ 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx index 9521fa02a600d..913dbf486db82 100644 --- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx @@ -1,3 +1,4 @@ +import { Ref } from 'react' import { useActions, useValues } from 'kea' import { resultTypeToName } from './constants' diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index 7217807010825..6df108a3f336d 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -89,11 +89,14 @@ type ResultNameProps = { result: SearchResultType } const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { - if (result.type === 'insight') { - return result.name ? {result.name} : {result.extra_fields.derived_name} - } else if (result.type === 'feature_flag') { - return result.name ? {result.name} : {result.extra_fields.key} + const { type, extra_fields } = result + if (type === 'insight') { + return extra_fields.name ? {extra_fields.name} : {extra_fields.derived_name} + } else if (type === 'feature_flag') { + return {extra_fields.key} + } else if (type === 'notebook') { + return {extra_fields.title} } else { - return {result.name} + return {extra_fields.name} } } diff --git a/frontend/src/lib/components/CommandBar/constants.ts b/frontend/src/lib/components/CommandBar/constants.ts index 4b0d973cdb95a..14396bb019f20 100644 --- a/frontend/src/lib/components/CommandBar/constants.ts +++ b/frontend/src/lib/components/CommandBar/constants.ts @@ -8,6 +8,7 @@ export const resultTypeToName: Record = { experiment: 'Experiments', feature_flag: 'Feature flags', insight: 'Insights', + notebook: 'Notebooks', } export const actionScopeToName: Record = { diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts index 77eb9cd127774..bf609bc9a2c94 100644 --- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts @@ -158,6 +158,8 @@ export const urlForResult = (result: SearchResult): string => { return urls.featureFlag(result.result_id) case 'insight': return urls.insightView(result.result_id as InsightShortId) + case 'notebook': + return urls.notebook(result.result_id) default: throw new Error(`No action for type '${result.type}' defined.`) } diff --git a/frontend/src/lib/components/CommandBar/types.ts b/frontend/src/lib/components/CommandBar/types.ts index 7fff9c5c5ddfe..1f3278f3727f6 100644 --- a/frontend/src/lib/components/CommandBar/types.ts +++ b/frontend/src/lib/components/CommandBar/types.ts @@ -4,7 +4,7 @@ export enum BarStatus { SHOW_ACTIONS = 'show_actions', } -export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experiment' | 'feature_flag' +export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experiment' | 'feature_flag' | 'notebook' export type ResultTypeWithAll = ResultType | 'all' diff --git a/posthog/api/search.py b/posthog/api/search.py index add18be9c9307..7be8c674618e3 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -1,3 +1,4 @@ +import functools import re from typing import Any from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector @@ -12,18 +13,29 @@ from posthog.api.routing import StructuredViewSetMixin from posthog.permissions import ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission from posthog.models import Action, Cohort, Insight, Dashboard, FeatureFlag, Experiment, Team +from posthog.models.notebook.notebook import Notebook LIMIT = 25 ENTITY_MAP = { - "action": {"klass": Action}, - "cohort": {"klass": Cohort}, - "insight": {"klass": Insight, "extra_fields": ["derived_name"]}, - "dashboard": {"klass": Dashboard}, - "experiment": {"klass": Experiment}, - "feature_flag": {"klass": FeatureFlag, "extra_fields": ["key"]}, + "insight": { + "klass": Insight, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "derived_name"], + }, + "dashboard": {"klass": Dashboard, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, + "experiment": {"klass": Experiment, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, + "feature_flag": {"klass": FeatureFlag, "search_fields": {"name": "A"}, "extra_fields": ["key"]}, + "notebook": {"klass": Notebook, "search_fields": {"title": "A"}, "extra_fields": ["title"]}, + "action": {"klass": Action, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, + "cohort": {"klass": Cohort, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, } +""" +Map of entity names to their class, search_fields and extra_fields. + +The value in search_fields corresponds to the PostgreSQL weighting i.e. A, B, C or D. +""" class QuerySerializer(serializers.Serializer): @@ -59,6 +71,7 @@ def list(self, request: Request, **kw) -> HttpResponse: klass=entity_meta.get("klass"), # type: ignore team=self.team, query=query, + search_fields=entity_meta.get("search_fields"), # type: ignore extra_fields=entity_meta.get("extra_fields"), # type: ignore ) qs = qs.union(klass_qs) @@ -92,17 +105,18 @@ def class_queryset( klass: type[Model], team: Team, query: str | None, + search_fields: dict[str, str], extra_fields: dict | None, ): """Builds a queryset for the class.""" entity_type = class_to_entity_name(klass) - values = ["type", "result_id", "name", "extra_fields"] + values = ["type", "result_id", "extra_fields"] qs: QuerySet[Any] = klass.objects.filter(team=team) # filter team qs = qs.annotate(type=Value(entity_type, output_field=CharField())) # entity type # entity id - if entity_type == "insight": + if entity_type == "insight" or entity_type == "notebook": qs = qs.annotate(result_id=F("short_id")) else: qs = qs.annotate(result_id=Cast("pk", CharField())) @@ -115,10 +129,10 @@ def class_queryset( # full-text search rank if query: + search_vectors = [SearchVector(key, weight=value, config="simple") for key, value in search_fields.items()] + combined_vector = functools.reduce(lambda a, b: a + b, search_vectors) qs = qs.annotate( - rank=SearchRank( - SearchVector("name", config="simple"), SearchQuery(query, config="simple", search_type="raw") - ), + rank=SearchRank(combined_vector, SearchQuery(query, config="simple", search_type="raw")), ) qs = qs.filter(rank__gt=0.05) values.append("rank") From 0c34ce7183d4c8843ebbf0dc682a08af249f4c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 17:02:06 +0100 Subject: [PATCH 13/24] customize search --- posthog/api/search.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index 7be8c674618e3..b472c27f0e1d1 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -21,15 +21,35 @@ ENTITY_MAP = { "insight": { "klass": Insight, + "search_fields": {"name": "A", "derived_name": "B", "description": "C"}, + "extra_fields": ["name", "derived_name", "description"], + }, + "dashboard": { + "klass": Dashboard, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "experiment": { + "klass": Experiment, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "feature_flag": {"klass": FeatureFlag, "search_fields": {"key": "A", "name": "C"}, "extra_fields": ["key", "name"]}, + "notebook": { + "klass": Notebook, + "search_fields": {"title": "A", "text_content": "C"}, + "extra_fields": ["title", "text_content"], + }, + "action": { + "klass": Action, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "cohort": { + "klass": Cohort, "search_fields": {"name": "A", "description": "C"}, - "extra_fields": ["name", "derived_name"], + "extra_fields": ["name", "description"], }, - "dashboard": {"klass": Dashboard, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, - "experiment": {"klass": Experiment, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, - "feature_flag": {"klass": FeatureFlag, "search_fields": {"name": "A"}, "extra_fields": ["key"]}, - "notebook": {"klass": Notebook, "search_fields": {"title": "A"}, "extra_fields": ["title"]}, - "action": {"klass": Action, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, - "cohort": {"klass": Cohort, "search_fields": {"name": "A"}, "extra_fields": ["name"]}, } """ Map of entity names to their class, search_fields and extra_fields. From 999cb631b079521fc6d2deaf3a48314014d4506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 18:03:34 +0100 Subject: [PATCH 14/24] wip --- .../components/CommandBar/ActionResult.tsx | 4 +- .../components/CommandBar/SearchResult.tsx | 20 +++++-- .../CommandBar/SearchResultPreview.tsx | 37 ++++++++++++ .../components/CommandBar/SearchResults.tsx | 56 ++++++++++--------- 4 files changed, 85 insertions(+), 32 deletions(-) create mode 100644 frontend/src/lib/components/CommandBar/SearchResultPreview.tsx diff --git a/frontend/src/lib/components/CommandBar/ActionResult.tsx b/frontend/src/lib/components/CommandBar/ActionResult.tsx index 5835595074291..3ae32bd484a14 100644 --- a/frontend/src/lib/components/CommandBar/ActionResult.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResult.tsx @@ -24,9 +24,7 @@ export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Elemen return (
{ onMouseEnterResult(result.index) }} diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index 6df108a3f336d..a460f070a1d58 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -42,8 +42,8 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: return (
{ if (isAutoScrolling) { return @@ -76,7 +76,7 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: } export const SearchResultSkeleton = (): JSX.Element => ( -
+
@@ -88,7 +88,8 @@ export const SearchResultSkeleton = (): JSX.Element => ( type ResultNameProps = { result: SearchResultType } -const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { + +export const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { const { type, extra_fields } = result if (type === 'insight') { return extra_fields.name ? {extra_fields.name} : {extra_fields.derived_name} @@ -100,3 +101,14 @@ const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { return {extra_fields.name} } } + +export const ResultDescription = ({ result }: ResultNameProps): JSX.Element | null => { + const { type, extra_fields } = result + if (type === 'feature_flag') { + return {extra_fields.name} + } else if (type === 'notebook') { + return {extra_fields.text_content} + } else { + return {extra_fields.description} + } +} diff --git a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx new file mode 100644 index 0000000000000..5e2153f97ff3b --- /dev/null +++ b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx @@ -0,0 +1,37 @@ +import { useLayoutEffect, useRef } from 'react' +import { useActions, useValues } from 'kea' + +import { resultTypeToName } from './constants' +import { searchBarLogic, urlForResult } from './searchBarLogic' +import { SearchResult as SearchResultType } from './types' +import { LemonSkeleton } from '@posthog/lemon-ui' +import { ResultDescription, ResultName } from 'lib/components/CommandBar/SearchResult' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' + +export const SearchResultPreview = (): JSX.Element | null => { + const { activeResultIndex, filterSearchResults } = useValues(searchBarLogic) + + if (!filterSearchResults || filterSearchResults.length === 0) { + return null + } + + const result = filterSearchResults[activeResultIndex] + console.debug('result', result, activeResultIndex, filterSearchResults) + const { type } = result + + return ( +
+
{resultTypeToName[type]}
+
+ +
+
+ + {/*{type === 'insight' && }*/} +
+ + {/*
{JSON.stringify(result, null, 2)}
*/} +
+ ) +} diff --git a/frontend/src/lib/components/CommandBar/SearchResults.tsx b/frontend/src/lib/components/CommandBar/SearchResults.tsx index 1b2ab8ee00bd7..9a1e217d69b4d 100644 --- a/frontend/src/lib/components/CommandBar/SearchResults.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResults.tsx @@ -4,37 +4,43 @@ import { DetectiveHog } from '../hedgehogs' import { searchBarLogic } from './searchBarLogic' import { SearchResult, SearchResultSkeleton } from './SearchResult' +import { SearchResultPreview } from './SearchResultPreview' export const SearchResults = (): JSX.Element => { const { filterSearchResults, searchResponseLoading, activeResultIndex, keyboardResultIndex } = useValues(searchBarLogic) return ( -
- {searchResponseLoading && ( - <> - - - - - )} - {!searchResponseLoading && filterSearchResults?.length === 0 && ( -
-

No results

-

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

- -
- )} - {!searchResponseLoading && - filterSearchResults?.map((result, index) => ( - - ))} +
+
+ {searchResponseLoading && ( + <> + + + + + )} + {!searchResponseLoading && filterSearchResults?.length === 0 && ( +
+

No results

+

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

+ +
+ )} + {!searchResponseLoading && + filterSearchResults?.map((result, index) => ( + + ))} +
+
+ +
) } From e4093e235880b741b541d53e77bff98db2d5a3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Fri, 17 Nov 2023 18:12:18 +0100 Subject: [PATCH 15/24] better descriptions --- frontend/src/lib/components/CommandBar/SearchResult.tsx | 8 ++++++-- .../src/lib/components/CommandBar/SearchResultPreview.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index a460f070a1d58..c0e6d674cb1b0 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -105,10 +105,14 @@ export const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { export const ResultDescription = ({ result }: ResultNameProps): JSX.Element | null => { const { type, extra_fields } = result if (type === 'feature_flag') { - return {extra_fields.name} + return extra_fields.name && extra_fields.name !== extra_fields.key ? ( + {extra_fields.name} + ) : ( + No description. + ) } else if (type === 'notebook') { return {extra_fields.text_content} } else { - return {extra_fields.description} + return extra_fields.description ? {extra_fields.description} : No description. } } diff --git a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx index 5e2153f97ff3b..c0b89acc3be5f 100644 --- a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx @@ -26,7 +26,7 @@ export const SearchResultPreview = (): JSX.Element | null => {
-
+
{/*{type === 'insight' && }*/}
From cc9abb3d0799bd9ef795cde82054ac4fb6f93f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 20 Nov 2023 11:26:16 +0100 Subject: [PATCH 16/24] cleanup --- .../CommandBar/SearchResultPreview.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx index c0b89acc3be5f..f91b09a865dd3 100644 --- a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx @@ -1,13 +1,9 @@ -import { useLayoutEffect, useRef } from 'react' -import { useActions, useValues } from 'kea' +import { useValues } from 'kea' import { resultTypeToName } from './constants' -import { searchBarLogic, urlForResult } from './searchBarLogic' -import { SearchResult as SearchResultType } from './types' -import { LemonSkeleton } from '@posthog/lemon-ui' +import { searchBarLogic } from './searchBarLogic' + import { ResultDescription, ResultName } from 'lib/components/CommandBar/SearchResult' -import { NodeKind } from '~/queries/schema' -import { Query } from '~/queries/Query/Query' export const SearchResultPreview = (): JSX.Element | null => { const { activeResultIndex, filterSearchResults } = useValues(searchBarLogic) @@ -17,21 +13,16 @@ export const SearchResultPreview = (): JSX.Element | null => { } const result = filterSearchResults[activeResultIndex] - console.debug('result', result, activeResultIndex, filterSearchResults) - const { type } = result return (
-
{resultTypeToName[type]}
+
{resultTypeToName[result.type]}
- {/*{type === 'insight' && }*/}
- - {/*
{JSON.stringify(result, null, 2)}
*/}
) } From b94b052979b9bc8c46b9bfefd180f2e52d2cb82f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:31:35 +0000 Subject: [PATCH 17/24] Update UI snapshots for `chromium` (1) --- ...ipeline--pipeline-transformations-page.png | Bin 128268 -> 129087 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png index 80fdf0c1d863214fe15729c5686d24709b7b3e80..11799f73e5bb98d9df2bfb88f1b7ef73707c4512 100644 GIT binary patch delta 29505 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 OMqWl)I$O%n=f41R%`KP! delta 28719 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 From 2c17e9c19af9ee38c90ea957106e4a61780715bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 20 Nov 2023 11:44:45 +0100 Subject: [PATCH 18/24] fix ref type --- frontend/src/lib/components/CommandBar/SearchBar.tsx | 2 +- frontend/src/lib/components/CommandBar/SearchBarTab.tsx | 4 ++-- frontend/src/lib/components/CommandBar/SearchTabs.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchBar.tsx b/frontend/src/lib/components/CommandBar/SearchBar.tsx index 1fa53068dc031..7a4163e487a91 100644 --- a/frontend/src/lib/components/CommandBar/SearchBar.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBar.tsx @@ -10,7 +10,7 @@ import { SearchTabs } from './SearchTabs' export const SearchBar = (): JSX.Element => { useMountedLogic(searchBarLogic) // load initial results - const inputRef = useRef(null) + const inputRef = useRef(null) return (
diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx index 913dbf486db82..c2dcb75f0917b 100644 --- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx @@ -1,4 +1,4 @@ -import { Ref } from 'react' +import { RefObject } from 'react' import { useActions, useValues } from 'kea' import { resultTypeToName } from './constants' @@ -10,7 +10,7 @@ type SearchBarTabProps = { type: ResultTypeWithAll active: boolean count?: number | null - inputRef: Ref + inputRef: RefObject } export const SearchBarTab = ({ type, active, count, inputRef }: SearchBarTabProps): JSX.Element => { diff --git a/frontend/src/lib/components/CommandBar/SearchTabs.tsx b/frontend/src/lib/components/CommandBar/SearchTabs.tsx index 984857305b6bb..fe6e9a9edb2ad 100644 --- a/frontend/src/lib/components/CommandBar/SearchTabs.tsx +++ b/frontend/src/lib/components/CommandBar/SearchTabs.tsx @@ -1,12 +1,12 @@ import { useValues } from 'kea' -import { Ref } from 'react' +import { RefObject } from 'react' import { searchBarLogic } from './searchBarLogic' import { SearchBarTab } from './SearchBarTab' import { ResultType } from './types' type SearchTabsProps = { - inputRef: Ref + inputRef: RefObject } export const SearchTabs = ({ inputRef }: SearchTabsProps): JSX.Element | null => { From 5e35308d3ad74e00f07f5fbb1e5293018ed6212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obermu=CC=88ller?= Date: Mon, 20 Nov 2023 11:50:23 +0100 Subject: [PATCH 19/24] fix mypy and tests --- posthog/api/search.py | 2 +- posthog/api/test/test_search.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/posthog/api/search.py b/posthog/api/search.py index b472c27f0e1d1..a48f716f902f7 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -92,7 +92,7 @@ def list(self, request: Request, **kw) -> HttpResponse: team=self.team, query=query, search_fields=entity_meta.get("search_fields"), # type: ignore - extra_fields=entity_meta.get("extra_fields"), # type: ignore + extra_fields=entity_meta.get("extra_fields"), ) qs = qs.union(klass_qs) counts[entity_name] = klass_qs.count() diff --git a/posthog/api/test/test_search.py b/posthog/api/test/test_search.py index 3a5c0e4f2e69b..543d2d5adc048 100644 --- a/posthog/api/test/test_search.py +++ b/posthog/api/test/test_search.py @@ -62,21 +62,23 @@ def test_response_format_and_ids(self): self.assertEqual( response.json()["results"][0], { - "name": "second dashboard", "rank": response.json()["results"][0]["rank"], "type": "dashboard", "result_id": str(self.dashboard_1.id), - "extra_fields": {}, + "extra_fields": {"description": "", "name": "second dashboard"}, }, ) self.assertEqual( response.json()["results"][1], { - "name": "second insight", "rank": response.json()["results"][1]["rank"], "type": "insight", "result_id": self.insight_1.short_id, - "extra_fields": {"derived_name": None}, + "extra_fields": { + "derived_name": None, + "name": "second insight", + "description": None, + }, }, ) @@ -84,7 +86,10 @@ def test_extra_fields(self): response = self.client.get("/api/projects/@current/search?entities=insight") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["results"][0]["extra_fields"], {"derived_name": "derived name"}) + self.assertEqual( + response.json()["results"][0]["extra_fields"], + {"derived_name": "derived name", "description": None, "name": None}, + ) def test_search_with_fully_invalid_query(self): response = self.client.get("/api/projects/@current/search?q=%3E") From ec05b8de0fd3d0100014310e206b074b6033421f Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 20 Nov 2023 17:16:36 +0100 Subject: [PATCH 20/24] Align skeleton result size with actual results --- .../src/lib/components/CommandBar/SearchResult.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index c0e6d674cb1b0..8c5364ca87101 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -76,12 +76,10 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: } export const SearchResultSkeleton = (): JSX.Element => ( -
-
- - - -
+
+ + +
) From 840bc85c569395d223f0330e8ad7c8420def9a50 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 20 Nov 2023 17:29:51 +0100 Subject: [PATCH 21/24] Fix hard-coded light mode color --- frontend/src/lib/components/CommandBar/ActionResults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/CommandBar/ActionResults.tsx b/frontend/src/lib/components/CommandBar/ActionResults.tsx index a8d15abde7f17..ed44ab499d03e 100644 --- a/frontend/src/lib/components/CommandBar/ActionResults.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResults.tsx @@ -15,7 +15,7 @@ type ResultsGroupProps = { const ResultsGroup = ({ scope, results, activeResultIndex }: ResultsGroupProps): JSX.Element => { return ( <> -
+
{getNameFromActionScope(scope)}
{results.map((result) => ( From b12c93fec8857049b9f4c4d7faf4f83d57e60f62 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 20 Nov 2023 17:30:14 +0100 Subject: [PATCH 22/24] Add website-inspired search placeholder --- frontend/src/lib/components/CommandBar/SearchInput.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/lib/components/CommandBar/SearchInput.tsx b/frontend/src/lib/components/CommandBar/SearchInput.tsx index 1418e84b3607b..ce03cc5af4cc7 100644 --- a/frontend/src/lib/components/CommandBar/SearchInput.tsx +++ b/frontend/src/lib/components/CommandBar/SearchInput.tsx @@ -5,8 +5,10 @@ import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardSh import { searchBarLogic } from './searchBarLogic' import { forwardRef, Ref } from 'react' +import { teamLogic } from 'scenes/teamLogic' export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref): JSX.Element { + const { currentTeam } = useValues(teamLogic) const { searchQuery } = useValues(searchBarLogic) const { setSearchQuery } = useActions(searchBarLogic) @@ -22,6 +24,7 @@ export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref
) From df8935321d3aa448c0c5270baf952310d16ce96f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:17:25 +0000 Subject: [PATCH 23/24] Update query snapshots --- .../api/test/__snapshots__/test_decide.ambr | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 655c44eff5ab9..c268d70875ea4 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -88,6 +88,22 @@ LIMIT 21 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10 + ' + SELECT "posthog_pluginconfig"."id", + "posthog_pluginconfig"."web_token", + "posthog_pluginsourcefile"."updated_at", + "posthog_plugin"."updated_at", + "posthog_pluginconfig"."updated_at" + FROM "posthog_pluginconfig" + INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") + INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") + WHERE ("posthog_pluginconfig"."enabled" + AND "posthog_pluginsourcefile"."filename" = 'site.ts' + AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' + AND "posthog_pluginconfig"."team_id" = 2) + ' +--- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.2 ' SELECT "posthog_organizationmembership"."id", @@ -119,6 +135,17 @@ ' --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.3 + ' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ + ' +--- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.4 ' SELECT (1) AS "a" FROM "posthog_grouptypemapping" @@ -126,7 +153,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.4 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -137,7 +164,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -148,7 +175,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -159,7 +186,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 ' SELECT "posthog_user"."id", "posthog_user"."password", @@ -188,7 +215,7 @@ LIMIT 21 ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 ' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -211,22 +238,6 @@ AND "posthog_featureflag"."team_id" = 2) ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 - ' - SELECT "posthog_pluginconfig"."id", - "posthog_pluginconfig"."web_token", - "posthog_pluginsourcefile"."updated_at", - "posthog_plugin"."updated_at", - "posthog_pluginconfig"."updated_at" - FROM "posthog_pluginconfig" - INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") - INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") - WHERE ("posthog_pluginconfig"."enabled" - AND "posthog_pluginsourcefile"."filename" = 'site.ts' - AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' - AND "posthog_pluginconfig"."team_id" = 2) - ' ---- # name: TestDecide.test_flag_with_behavioural_cohorts ' SELECT "posthog_user"."id", From 34fabb0b5494311819447c13e202c6a31e7d99ad Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:28:00 +0000 Subject: [PATCH 24/24] Update query snapshots --- .../api/test/__snapshots__/test_decide.ambr | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index c268d70875ea4..f0c0385bd2ccc 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -135,17 +135,6 @@ ' --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.3 - ' - SELECT "posthog_instancesetting"."id", - "posthog_instancesetting"."key", - "posthog_instancesetting"."raw_value" - FROM "posthog_instancesetting" - WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED' - ORDER BY "posthog_instancesetting"."id" ASC - LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ - ' ---- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.4 ' SELECT (1) AS "a" FROM "posthog_grouptypemapping" @@ -153,7 +142,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.4 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -164,7 +153,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -175,7 +164,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.6 ' SELECT "posthog_instancesetting"."id", "posthog_instancesetting"."key", @@ -186,7 +175,7 @@ LIMIT 1 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 ' SELECT "posthog_user"."id", "posthog_user"."password", @@ -215,7 +204,7 @@ LIMIT 21 ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 ' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -238,6 +227,22 @@ AND "posthog_featureflag"."team_id" = 2) ' --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 + ' + SELECT "posthog_pluginconfig"."id", + "posthog_pluginconfig"."web_token", + "posthog_pluginsourcefile"."updated_at", + "posthog_plugin"."updated_at", + "posthog_pluginconfig"."updated_at" + FROM "posthog_pluginconfig" + INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") + INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") + WHERE ("posthog_pluginconfig"."enabled" + AND "posthog_pluginsourcefile"."filename" = 'site.ts' + AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' + AND "posthog_pluginconfig"."team_id" = 2) + ' +--- # name: TestDecide.test_flag_with_behavioural_cohorts ' SELECT "posthog_user"."id",