From 13a943c3ddca385ced3a4e444483200c080e0ef6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 23 Nov 2022 22:29:44 +0000 Subject: [PATCH 01/90] add a big reload button --- .../src/queries/nodes/DataTable/DataTable.tsx | 12 ++-- .../queries/nodes/DataTable/ReloadQuery.tsx | 20 ++++++ frontend/src/queries/schema.json | 70 ++++++++++++++----- frontend/src/queries/schema.ts | 2 + 4 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/ReloadQuery.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index ef61e142478dc..9703500cb481d 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,6 +1,6 @@ import { DataTableColumn, DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' import { useState } from 'react' -import { useValues } from 'kea' +import { useValues, BindLogic } from 'kea' import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' import { LemonTable, LemonTableColumn } from 'lib/components/LemonTable' import { EventType, PropertyFilterType } from '~/types' @@ -16,6 +16,7 @@ import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFi import { EventDetails } from 'scenes/events' import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' +import { ReloadQuery } from '~/queries/nodes/DataTable/ReloadQuery' interface DataTableProps { query: DataTableNode @@ -95,10 +96,12 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const showEventFilter = query.showEventFilter ?? true const showMore = query.showMore ?? true const showExport = query.showExport ?? true + const showReload = query.showReload ?? true const expandable = query.expandable ?? true const [id] = useState(() => uniqueNode++) - const logic = dataNodeLogic({ query: query.source, key: `DataTable.${id}` }) + const dataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } + const logic = dataNodeLogic(dataNodeLogicProps) const { response, responseLoading } = useValues(logic) const rows = (response as null | EventsNode['response'])?.results ?? [] const lemonColumns: LemonTableColumn[] = columns.map(({ type, key }) => ({ @@ -120,9 +123,10 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { } return ( - <> + {(showPropertyFilter || showEventFilter || showExport) && (
+ {showReload && } {showEventFilter && ( setQuery?.({ ...query, source })} /> )} @@ -151,7 +155,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { : undefined } /> - + ) } diff --git a/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx b/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx new file mode 100644 index 0000000000000..54b279e2fbbf8 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx @@ -0,0 +1,20 @@ +import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons' +import { LemonButton } from 'lib/components/LemonButton' +import { useActions, useValues } from 'kea' +import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' + +export function ReloadQuery(): JSX.Element { + const { responseLoading } = useValues(dataNodeLogic) + const { loadData } = useActions(dataNodeLogic) + + return ( + : } + > + {responseLoading ? 'Loading' : 'Reload'} + + ) +} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 98d752a1957aa..31b81522b092d 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -894,9 +894,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "type": "string" }, @@ -920,9 +917,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "type": "string" }, @@ -946,9 +940,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "enum": ["tag_name", "text", "href", "selector"], "type": "string" @@ -972,9 +963,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "const": "$session_duration", "type": "string" @@ -998,9 +986,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "const": "id", "type": "string" @@ -1021,9 +1006,6 @@ { "additionalProperties": false, "properties": { - "group_type_index": { - "type": ["number", "null"] - }, "key": { "const": "duration", "type": "string" @@ -1043,6 +1025,54 @@ } }, "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "group_type_index": { + "type": ["number", "null"] + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "operator": { + "$ref": "#/definitions/PropertyOperator" + }, + "type": { + "const": "group", + "type": "string" + }, + "value": { + "$ref": "#/definitions/PropertyFilterValue" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "operator": { + "$ref": "#/definitions/PropertyOperator" + }, + "type": { + "const": "feature", + "description": "Event property with \"$feature/\" prepended", + "type": "string" + }, + "value": { + "$ref": "#/definitions/PropertyFilterValue" + } + }, + "type": "object" } ] }, @@ -1170,6 +1200,10 @@ "description": "Include a property filter above the table (default: true)", "type": "boolean" }, + "showReload": { + "description": "Show a reload button", + "type": "boolean" + }, "source": { "$ref": "#/definitions/EventsNode", "description": "Source of the events" diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index cecedae9e54ef..857b8c3a8205c 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -56,6 +56,8 @@ export interface DataTableNode extends Node { showMore?: boolean /** Show the export button */ showExport?: boolean + /** Show a reload button */ + showReload?: boolean /** Can expand row to show raw event data (default: true) */ expandable?: boolean } From ab15ffd164dfbd2aac28667c06c59e9c5731345b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 24 Nov 2022 15:43:47 +0000 Subject: [PATCH 02/90] reload button --- frontend/src/queries/nodes/DataTable/ReloadQuery.tsx | 7 ++++--- frontend/src/queries/schema.json | 1 + .../organization/Settings/Permissions/permissionsLogic.tsx | 2 +- .../src/scenes/organization/Settings/Roles/rolesLogic.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx b/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx index 54b279e2fbbf8..27b9f2103f754 100644 --- a/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx +++ b/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx @@ -1,7 +1,8 @@ -import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons' -import { LemonButton } from 'lib/components/LemonButton' import { useActions, useValues } from 'kea' import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { LemonButton } from 'lib/components/LemonButton' +import { IconRefresh } from 'lib/components/icons' +import { Spinner } from 'lib/components/Spinner/Spinner' export function ReloadQuery(): JSX.Element { const { responseLoading } = useValues(dataNodeLogic) @@ -12,7 +13,7 @@ export function ReloadQuery(): JSX.Element { type="secondary" onClick={loadData} disabled={responseLoading} - icon={responseLoading ? : } + icon={responseLoading ? : } > {responseLoading ? 'Loading' : 'Reload'} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 31b81522b092d..88dd71650c2b4 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1134,6 +1134,7 @@ "enum": [ "ActionsLineGraph", "ActionsLineGraphCumulative", + "ActionsAreaGraph", "ActionsTable", "ActionsPie", "ActionsBar", diff --git a/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx b/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx index 246d3d9579f29..0616bb2ede8d5 100644 --- a/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx +++ b/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx @@ -2,7 +2,7 @@ import { afterMount, kea, selectors, path } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import { OrganizationResourcePermissionType, Resource, AccessLevel } from '~/types' -import { permissionsLogicType } from './permissionsLogicType' +import type { permissionsLogicType } from './permissionsLogicType' const ResourceDisplayMapping: Record = { [Resource.FEATURE_FLAGS]: 'Feature Flags', diff --git a/frontend/src/scenes/organization/Settings/Roles/rolesLogic.tsx b/frontend/src/scenes/organization/Settings/Roles/rolesLogic.tsx index 7d8d22ab44063..079e9ac7c1df9 100644 --- a/frontend/src/scenes/organization/Settings/Roles/rolesLogic.tsx +++ b/frontend/src/scenes/organization/Settings/Roles/rolesLogic.tsx @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders' import api from 'lib/api' import { teamMembersLogic } from 'scenes/project/Settings/teamMembersLogic' import { AccessLevel, Resource, RoleMemberType, RoleType, UserBasicType } from '~/types' -import { rolesLogicType } from './rolesLogicType' +import type { rolesLogicType } from './rolesLogicType' export const rolesLogic = kea([ path(['scenes', 'organization', 'rolesLogic']), From 6262de68f91418baaf552eb2ffd97e28e94f4b49 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 24 Nov 2022 16:16:05 +0000 Subject: [PATCH 03/90] load next data --- .../src/queries/nodes/DataTable/DataTable.tsx | 11 +++- frontend/src/queries/nodes/dataNodeLogic.ts | 54 ++++++++++++++++++- frontend/src/queries/query.ts | 12 ++++- frontend/src/queries/schema.ts | 4 ++ frontend/src/types.ts | 1 + 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 9703500cb481d..fba87e2a0e09f 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,6 +1,6 @@ import { DataTableColumn, DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' import { useState } from 'react' -import { useValues, BindLogic } from 'kea' +import { useValues, BindLogic, useActions } from 'kea' import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' import { LemonTable, LemonTableColumn } from 'lib/components/LemonTable' import { EventType, PropertyFilterType } from '~/types' @@ -17,6 +17,7 @@ import { EventDetails } from 'scenes/events' import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' import { ReloadQuery } from '~/queries/nodes/DataTable/ReloadQuery' +import { LemonButton } from 'lib/components/LemonButton' interface DataTableProps { query: DataTableNode @@ -102,7 +103,8 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const [id] = useState(() => uniqueNode++) const dataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } const logic = dataNodeLogic(dataNodeLogicProps) - const { response, responseLoading } = useValues(logic) + const { response, responseLoading, canLoadNextData } = useValues(logic) + const { loadNextData } = useActions(logic) const rows = (response as null | EventsNode['response'])?.results ?? [] const lemonColumns: LemonTableColumn[] = columns.map(({ type, key }) => ({ dataIndex: `${type}.${key}` as any, @@ -155,6 +157,11 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { : undefined } /> + {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && ( + + Load more events + + )} ) } diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/dataNodeLogic.ts index 1a9002c79d38c..1880f151b5e29 100644 --- a/frontend/src/queries/nodes/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/dataNodeLogic.ts @@ -1,8 +1,9 @@ import { kea, path, props, key, afterMount, selectors, propsChanged } from 'kea' import { loaders } from 'kea-loaders' import type { dataNodeLogicType } from './dataNodeLogicType' -import { DataNode } from '~/queries/schema' +import { DataNode, EventsNode } from '~/queries/schema' import { query } from '~/queries/query' +import { isEventsNode } from '~/queries/utils' export interface DataNodeLogicProps { key: string @@ -18,7 +19,6 @@ export const dataNodeLogic = kea([ actions.loadData() } }), - selectors({ query: [() => [(_, props) => props.query], (query) => query] }), loaders(({ values }) => ({ response: [ null as DataNode['response'] | null, @@ -26,9 +26,59 @@ export const dataNodeLogic = kea([ loadData: async () => { return (await query(values.query)) ?? null }, + loadNewData: async () => { + if (!values.canLoadNewData) { + return + } + const response = values.response as EventsNode['response'] | null + const diffQuery: EventsNode = + response && response.results?.length > 0 + ? { + ...values.query, + after: response.results[0].timestamp, + } + : values.query + const results = (await query(diffQuery)) ?? null + return { + results: [...(results?.results ?? []), ...(response?.results ?? [])], + } + }, + loadNextData: async () => { + if (!values.canLoadNewData) { + return + } + const response = values.response as EventsNode['response'] | null + const diffQuery: EventsNode = + response && response.results?.length > 0 + ? { + ...values.query, + before: response.results[response.results.length - 1].timestamp, + } + : values.query + const results = (await query(diffQuery)) ?? null + return { + results: [...(response?.results ?? []), ...(results?.results ?? [])], + next: response?.next, + } + }, }, ], })), + selectors({ + query: [() => [(_, props) => props.query], (query) => query], + canLoadNewData: [ + (s) => [s.query], + (query) => { + return isEventsNode(query) + }, + ], + canLoadNextData: [ + (s) => [s.query, s.response], + (query, response) => { + return isEventsNode(query) && response?.next + }, + ], + }), afterMount(({ actions }) => { actions.loadData() }), diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index b7191ccd5db36..3f6c94377d1b3 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -15,10 +15,18 @@ import { import { toParams } from 'lib/utils' // Return data for a given query -export async function query(query: N, methodOptions?: ApiMethodOptions): Promise { +export async function query( + query: N, + methodOptions?: ApiMethodOptions +): Promise { if (isEventsNode(query)) { return await api.events.list( - { properties: query.properties, ...(query.event ? { event: query.event } : {}) }, + { + properties: query.properties, + ...(query.event ? { event: query.event } : {}), + before: query.before, + after: query.after, + }, query.limit ) } else if (isLegacyQuery(query)) { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 857b8c3a8205c..c38fa882b6bf5 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -34,6 +34,10 @@ export interface EventsNode extends DataNode { event?: string properties?: AnyPropertyFilter[] | PropertyGroupFilter limit?: number + /** Only fetch events that happened before this timestamp */ + before?: string + /** Only fetch events that happened after this timestamp */ + after?: string response?: { results: EventType[] next?: string diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 318d651f2c36b..1de1c25e65558 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1389,6 +1389,7 @@ export interface EventsListQueryParams { orderBy?: string[] action_id?: number after?: string + before?: string limit?: number } From 46d3e717e24610f37e69eb724f08ce3170f89c1f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 25 Nov 2022 08:57:17 +0000 Subject: [PATCH 04/90] rename "more" to "actions" --- frontend/src/queries/nodes/DataTable/DataTable.tsx | 5 +++-- frontend/src/queries/schema.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index fba87e2a0e09f..7efc05e5b93de 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -95,7 +95,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const columns = query.columns ? normalizeDataTableColumns(query.columns) : defaultDataTableColumns const showPropertyFilter = query.showPropertyFilter ?? true const showEventFilter = query.showEventFilter ?? true - const showMore = query.showMore ?? true + const showActions = query.showActions ?? true const showExport = query.showExport ?? true const showReload = query.showReload ?? true const expandable = query.expandable ?? true @@ -105,6 +105,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const logic = dataNodeLogic(dataNodeLogicProps) const { response, responseLoading, canLoadNextData } = useValues(logic) const { loadNextData } = useActions(logic) + const rows = (response as null | EventsNode['response'])?.results ?? [] const lemonColumns: LemonTableColumn[] = columns.map(({ type, key }) => ({ dataIndex: `${type}.${key}` as any, @@ -114,7 +115,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { }, })) - if (showMore) { + if (showActions) { lemonColumns.push({ dataIndex: 'more' as any, title: '', diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index c38fa882b6bf5..7fff5eeba4316 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -56,8 +56,8 @@ export interface DataTableNode extends Node { showEventFilter?: boolean /** Include a property filter above the table (default: true) */ showPropertyFilter?: boolean - /** Show the "..." menu at the end of the row */ - showMore?: boolean + /** Show the kebab menu at the end of the row */ + showActions?: boolean /** Show the export button */ showExport?: boolean /** Show a reload button */ From 442ef3510e42181501d88a6209ee1358334ffd5b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 25 Nov 2022 11:01:51 +0000 Subject: [PATCH 05/90] new events that load --- .../src/queries/nodes/DataTable/DataTable.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 7efc05e5b93de..d1da3db0b000a 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -18,6 +18,8 @@ import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' import { ReloadQuery } from '~/queries/nodes/DataTable/ReloadQuery' import { LemonButton } from 'lib/components/LemonButton' +import { Spinner } from 'lib/components/Spinner/Spinner' +import { IconRefresh } from 'lib/components/icons' interface DataTableProps { query: DataTableNode @@ -103,8 +105,8 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const [id] = useState(() => uniqueNode++) const dataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } const logic = dataNodeLogic(dataNodeLogicProps) - const { response, responseLoading, canLoadNextData } = useValues(logic) - const { loadNextData } = useActions(logic) + const { response, responseLoading, canLoadNextData, canLoadNewData } = useValues(logic) + const { loadNextData, loadNewData } = useActions(logic) const rows = (response as null | EventsNode['response'])?.results ?? [] const lemonColumns: LemonTableColumn[] = columns.map(({ type, key }) => ({ @@ -129,7 +131,19 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {(showPropertyFilter || showEventFilter || showExport) && (
- {showReload && } + {showReload && + (canLoadNewData ? ( + : } + > + Load new events + + ) : ( + + ))} {showEventFilter && ( setQuery?.({ ...query, source })} /> )} From b1d657730054452e6ef0f21b60ea4e9dd4ef9ba5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 25 Nov 2022 12:35:28 +0000 Subject: [PATCH 06/90] refactor new/next loading --- .../src/queries/nodes/DataTable/DataTable.tsx | 138 ++++-------------- .../src/queries/nodes/DataTable/LoadNew.tsx | 21 +++ .../src/queries/nodes/DataTable/LoadNext.tsx | 14 ++ .../DataTable/{ReloadQuery.tsx => Reload.tsx} | 2 +- .../queries/nodes/DataTable/renderColumn.tsx | 45 ++++++ .../queries/nodes/DataTable/renderTitle.tsx | 21 +++ frontend/src/queries/nodes/dataNodeLogic.ts | 41 ++++-- 7 files changed, 162 insertions(+), 120 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/LoadNew.tsx create mode 100644 frontend/src/queries/nodes/DataTable/LoadNext.tsx rename frontend/src/queries/nodes/DataTable/{ReloadQuery.tsx => Reload.tsx} (93%) create mode 100644 frontend/src/queries/nodes/DataTable/renderColumn.tsx create mode 100644 frontend/src/queries/nodes/DataTable/renderTitle.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index d1da3db0b000a..4d9db53c5425d 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,25 +1,19 @@ import { DataTableColumn, DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' import { useState } from 'react' -import { useValues, BindLogic, useActions } from 'kea' +import { useValues, BindLogic } from 'kea' import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' import { LemonTable, LemonTableColumn } from 'lib/components/LemonTable' import { EventType, PropertyFilterType } from '~/types' -import { autoCaptureEventToDescription } from 'lib/utils' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { urls } from 'scenes/urls' -import { PersonHeader } from 'scenes/persons/PersonHeader' -import { Link } from 'lib/components/Link' -import { Property } from 'lib/components/Property' -import { TZLabel } from 'lib/components/TZLabel' import { EventName } from '~/queries/nodes/EventsNode/EventName' import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' import { EventDetails } from 'scenes/events' import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' -import { ReloadQuery } from '~/queries/nodes/DataTable/ReloadQuery' -import { LemonButton } from 'lib/components/LemonButton' -import { Spinner } from 'lib/components/Spinner/Spinner' -import { IconRefresh } from 'lib/components/icons' +import { LoadNew } from '~/queries/nodes/DataTable/LoadNew' +import { Reload } from '~/queries/nodes/DataTable/Reload' +import { LoadNext } from '~/queries/nodes/DataTable/LoadNext' +import { renderTitle } from '~/queries/nodes/DataTable/renderTitle' +import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' interface DataTableProps { query: DataTableNode @@ -35,62 +29,6 @@ export const defaultDataTableStringColumns: DataTableStringColumn[] = [ ] export const defaultDataTableColumns: DataTableColumn[] = normalizeDataTableColumns(defaultDataTableStringColumns) -function renderTitle(type: PropertyFilterType, key: string): JSX.Element | string { - if (type === PropertyFilterType.Meta) { - if (key === 'timestamp') { - return 'Time' - } - return key - } else if (type === PropertyFilterType.Event || type === PropertyFilterType.Element) { - return - } else if (type === PropertyFilterType.Person) { - if (key === '') { - return 'Person' - } else { - return - } - } else { - return String(type) - } -} - -function renderColumn(type: PropertyFilterType, key: string, record: EventType): JSX.Element | string { - if (type === PropertyFilterType.Meta) { - if (key === 'event') { - if (record.event === '$autocapture') { - return autoCaptureEventToDescription(record) - } else { - const content = - const url = record.properties.$sentry_url - return url ? ( - - {content} - - ) : ( - content - ) - } - } else if (key === 'timestamp') { - return - } else { - return String(record[key]) - } - } else if (type === PropertyFilterType.Event) { - return - } else if (type === PropertyFilterType.Person) { - if (key === '') { - return ( - - - - ) - } else { - return - } - } - return
Unknown
-} - let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { @@ -105,45 +43,37 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const [id] = useState(() => uniqueNode++) const dataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } const logic = dataNodeLogic(dataNodeLogicProps) - const { response, responseLoading, canLoadNextData, canLoadNewData } = useValues(logic) - const { loadNextData, loadNewData } = useActions(logic) + const { response, responseLoading, canLoadNextData, canLoadNewData, nextDataLoading, newDataLoading } = + useValues(logic) - const rows = (response as null | EventsNode['response'])?.results ?? [] - const lemonColumns: LemonTableColumn[] = columns.map(({ type, key }) => ({ - dataIndex: `${type}.${key}` as any, - title: renderTitle(type, key), - render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(type, key, record) - }, - })) + const dataSource = (response as null | EventsNode['response'])?.results ?? [] - if (showActions) { - lemonColumns.push({ - dataIndex: 'more' as any, - title: '', - render: function RenderMore(_: any, record: EventType) { - return + const lemonColumns: LemonTableColumn[] = [ + ...columns.map(({ type, key }) => ({ + dataIndex: `${type}.${key}` as any, + title: renderTitle(type, key), + render: function RenderDataTableColumn(_: any, record: EventType) { + return renderColumn(type, key, record) }, - }) - } + })), + ...(showActions + ? [ + { + dataIndex: 'more' as any, + title: '', + render: function RenderMore(_: any, record: EventType) { + return + }, + }, + ] + : []), + ] return ( {(showPropertyFilter || showEventFilter || showExport) && (
- {showReload && - (canLoadNewData ? ( - : } - > - Load new events - - ) : ( - - ))} + {showReload && (canLoadNewData ? : )} {showEventFilter && ( setQuery?.({ ...query, source })} /> )} @@ -157,9 +87,9 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element {
)} - {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && ( - - Load more events - - )} + {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && }
) } diff --git a/frontend/src/queries/nodes/DataTable/LoadNew.tsx b/frontend/src/queries/nodes/DataTable/LoadNew.tsx new file mode 100644 index 0000000000000..a78836ef7ba31 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/LoadNew.tsx @@ -0,0 +1,21 @@ +import { useActions, useValues } from 'kea' +import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { LemonButton } from 'lib/components/LemonButton' +import { IconRefresh } from 'lib/components/icons' +import { Spinner } from 'lib/components/Spinner/Spinner' + +export function LoadNew(): JSX.Element { + const { responseLoading } = useValues(dataNodeLogic) + const { loadNewData } = useActions(dataNodeLogic) + + return ( + : } + > + Load new events + + ) +} diff --git a/frontend/src/queries/nodes/DataTable/LoadNext.tsx b/frontend/src/queries/nodes/DataTable/LoadNext.tsx new file mode 100644 index 0000000000000..294a588a47d0b --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/LoadNext.tsx @@ -0,0 +1,14 @@ +import { useActions, useValues } from 'kea' +import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { LemonButton } from 'lib/components/LemonButton' + +export function LoadNext(): JSX.Element { + const { responseLoading } = useValues(dataNodeLogic) + const { loadNextData } = useActions(dataNodeLogic) + + return ( + + Load more events + + ) +} diff --git a/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx b/frontend/src/queries/nodes/DataTable/Reload.tsx similarity index 93% rename from frontend/src/queries/nodes/DataTable/ReloadQuery.tsx rename to frontend/src/queries/nodes/DataTable/Reload.tsx index 27b9f2103f754..ae32c5349ca23 100644 --- a/frontend/src/queries/nodes/DataTable/ReloadQuery.tsx +++ b/frontend/src/queries/nodes/DataTable/Reload.tsx @@ -4,7 +4,7 @@ import { LemonButton } from 'lib/components/LemonButton' import { IconRefresh } from 'lib/components/icons' import { Spinner } from 'lib/components/Spinner/Spinner' -export function ReloadQuery(): JSX.Element { +export function Reload(): JSX.Element { const { responseLoading } = useValues(dataNodeLogic) const { loadData } = useActions(dataNodeLogic) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx new file mode 100644 index 0000000000000..2abb6d1859b9b --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -0,0 +1,45 @@ +import { EventType, PropertyFilterType } from '~/types' +import { autoCaptureEventToDescription } from 'lib/utils' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { Link } from 'lib/components/Link' +import { TZLabel } from 'lib/components/TZLabel' +import { Property } from 'lib/components/Property' +import { urls } from 'scenes/urls' +import { PersonHeader } from 'scenes/persons/PersonHeader' + +export function renderColumn(type: PropertyFilterType, key: string, record: EventType): JSX.Element | string { + if (type === PropertyFilterType.Meta) { + if (key === 'event') { + if (record.event === '$autocapture') { + return autoCaptureEventToDescription(record) + } else { + const content = + const url = record.properties.$sentry_url + return url ? ( + + {content} + + ) : ( + content + ) + } + } else if (key === 'timestamp') { + return + } else { + return String(record[key]) + } + } else if (type === PropertyFilterType.Event) { + return + } else if (type === PropertyFilterType.Person) { + if (key === '') { + return ( + + + + ) + } else { + return + } + } + return
Unknown
+} diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx new file mode 100644 index 0000000000000..63f6117ac79d3 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -0,0 +1,21 @@ +import { PropertyFilterType } from '~/types' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' + +export function renderTitle(type: PropertyFilterType, key: string): JSX.Element | string { + if (type === PropertyFilterType.Meta) { + if (key === 'timestamp') { + return 'Time' + } + return key + } else if (type === PropertyFilterType.Event || type === PropertyFilterType.Element) { + return + } else if (type === PropertyFilterType.Person) { + if (key === '') { + return 'Person' + } else { + return + } + } else { + return String(type) + } +} diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/dataNodeLogic.ts index 1880f151b5e29..840f0631e9e06 100644 --- a/frontend/src/queries/nodes/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/dataNodeLogic.ts @@ -1,4 +1,4 @@ -import { kea, path, props, key, afterMount, selectors, propsChanged } from 'kea' +import { kea, path, props, key, afterMount, selectors, propsChanged, reducers } from 'kea' import { loaders } from 'kea-loaders' import type { dataNodeLogicType } from './dataNodeLogicType' import { DataNode, EventsNode } from '~/queries/schema' @@ -30,40 +30,51 @@ export const dataNodeLogic = kea([ if (!values.canLoadNewData) { return } - const response = values.response as EventsNode['response'] | null + const oldResponse = values.response as EventsNode['response'] | null const diffQuery: EventsNode = - response && response.results?.length > 0 + oldResponse && oldResponse.results?.length > 0 ? { ...values.query, - after: response.results[0].timestamp, + after: oldResponse.results[0].timestamp, } : values.query - const results = (await query(diffQuery)) ?? null + const newResponse = (await query(diffQuery)) ?? null return { - results: [...(results?.results ?? []), ...(response?.results ?? [])], + results: [...(newResponse?.results ?? []), ...(oldResponse?.results ?? [])], + next: oldResponse?.next, } }, loadNextData: async () => { if (!values.canLoadNewData) { return } - const response = values.response as EventsNode['response'] | null + const oldResponse = values.response as EventsNode['response'] | null const diffQuery: EventsNode = - response && response.results?.length > 0 + oldResponse && oldResponse.results?.length > 0 ? { ...values.query, - before: response.results[response.results.length - 1].timestamp, + before: oldResponse.results[oldResponse.results.length - 1].timestamp, } : values.query - const results = (await query(diffQuery)) ?? null + const newResponse = (await query(diffQuery)) ?? null return { - results: [...(response?.results ?? []), ...(results?.results ?? [])], - next: response?.next, + results: [...(oldResponse?.results ?? []), ...(newResponse?.results ?? [])], + next: oldResponse?.next, } }, }, ], })), + reducers({ + newDataLoading: [ + false, + { loadNewData: () => true, loadNewDataSuccess: () => false, loadDataFailure: () => false }, + ], + nextDataLoading: [ + false, + { loadNextData: () => true, loadNextDataSuccess: () => false, loadNextDataFailure: () => false }, + ], + }), selectors({ query: [() => [(_, props) => props.query], (query) => query], canLoadNewData: [ @@ -75,7 +86,11 @@ export const dataNodeLogic = kea([ canLoadNextData: [ (s) => [s.query, s.response], (query, response) => { - return isEventsNode(query) && response?.next + return ( + isEventsNode(query) && + (response as EventsNode['response'])?.next && + ((response as EventsNode['response'])?.results?.length ?? 0) > 0 + ) }, ], }), From 7ab38887c0b2b1febc74777b21963b01434220a0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 25 Nov 2022 12:42:50 +0000 Subject: [PATCH 07/90] bug --- frontend/src/queries/nodes/dataNodeLogic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/dataNodeLogic.ts index 840f0631e9e06..96ef93a659c43 100644 --- a/frontend/src/queries/nodes/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/dataNodeLogic.ts @@ -45,7 +45,7 @@ export const dataNodeLogic = kea([ } }, loadNextData: async () => { - if (!values.canLoadNewData) { + if (!values.canLoadNextData) { return } const oldResponse = values.response as EventsNode['response'] | null From 4ac98bdbe4fbd0a8f56ac5b88727716c9691d146 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 25 Nov 2022 12:59:22 +0000 Subject: [PATCH 08/90] autoload --- .../src/queries/nodes/DataTable/AutoLoad.tsx | 25 +++++++++++++ .../src/queries/nodes/DataTable/DataTable.tsx | 4 +- frontend/src/queries/nodes/dataNodeLogic.ts | 37 ++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/AutoLoad.tsx diff --git a/frontend/src/queries/nodes/DataTable/AutoLoad.tsx b/frontend/src/queries/nodes/DataTable/AutoLoad.tsx new file mode 100644 index 0000000000000..276ec6187b746 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/AutoLoad.tsx @@ -0,0 +1,25 @@ +import { useActions, useValues } from 'kea' +import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { LemonSwitch } from 'lib/components/LemonSwitch/LemonSwitch' +import { useEffect } from 'react' + +export function AutoLoad(): JSX.Element { + const { autoLoadEnabled } = useValues(dataNodeLogic) + const { startAutoLoad, stopAutoLoad, toggleAutoLoad } = useActions(dataNodeLogic) + + useEffect(() => { + startAutoLoad() + return () => stopAutoLoad() + }, []) + + return ( + + ) +} diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 4d9db53c5425d..45de0a365add7 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -9,11 +9,11 @@ import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFi import { EventDetails } from 'scenes/events' import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' -import { LoadNew } from '~/queries/nodes/DataTable/LoadNew' import { Reload } from '~/queries/nodes/DataTable/Reload' import { LoadNext } from '~/queries/nodes/DataTable/LoadNext' import { renderTitle } from '~/queries/nodes/DataTable/renderTitle' import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' +import { AutoLoad } from '~/queries/nodes/DataTable/AutoLoad' interface DataTableProps { query: DataTableNode @@ -73,7 +73,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {(showPropertyFilter || showEventFilter || showExport) && (
- {showReload && (canLoadNewData ? : )} + {showReload && (canLoadNewData ? : )} {showEventFilter && ( setQuery?.({ ...query, source })} /> )} diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/dataNodeLogic.ts index 96ef93a659c43..936a45b6288c5 100644 --- a/frontend/src/queries/nodes/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/dataNodeLogic.ts @@ -1,15 +1,18 @@ -import { kea, path, props, key, afterMount, selectors, propsChanged, reducers } from 'kea' +import { kea, path, props, key, afterMount, selectors, propsChanged, reducers, actions, beforeUnmount } from 'kea' import { loaders } from 'kea-loaders' import type { dataNodeLogicType } from './dataNodeLogicType' import { DataNode, EventsNode } from '~/queries/schema' import { query } from '~/queries/query' import { isEventsNode } from '~/queries/utils' +import { subscriptions } from 'kea-subscriptions' export interface DataNodeLogicProps { key: string query: DataNode } +const AUTOLOAD_INTERVAL = 5000 + export const dataNodeLogic = kea([ path(['queries', 'nodes', 'dataNodeLogic']), props({} as DataNodeLogicProps), @@ -19,6 +22,11 @@ export const dataNodeLogic = kea([ actions.loadData() } }), + actions({ + startAutoLoad: true, + stopAutoLoad: true, + toggleAutoLoad: true, + }), loaders(({ values }) => ({ response: [ null as DataNode['response'] | null, @@ -74,6 +82,8 @@ export const dataNodeLogic = kea([ false, { loadNextData: () => true, loadNextDataSuccess: () => false, loadNextDataFailure: () => false }, ], + autoLoadEnabled: [false, { toggleAutoLoad: (state) => !state }], + autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], }), selectors({ query: [() => [(_, props) => props.query], (query) => query], @@ -93,8 +103,33 @@ export const dataNodeLogic = kea([ ) }, ], + autoLoadRunning: [ + (s) => [s.autoLoadEnabled, s.autoLoadStarted], + (autoLoadEnabled, autoLoadStarted) => autoLoadEnabled && autoLoadStarted, + ], }), + subscriptions(({ actions, cache, values }) => ({ + autoLoadRunning: (autoLoadRunning) => { + if (cache.autoLoadInterval) { + window.clearInterval(cache.autoLoadInterval) + cache.autoLoadInterval = null + } + if (autoLoadRunning) { + actions.loadNewData() + cache.autoLoadInterval = window.setInterval(() => { + if (!values.responseLoading) { + actions.loadNewData() + } + }, AUTOLOAD_INTERVAL) + } + }, + })), afterMount(({ actions }) => { actions.loadData() }), + beforeUnmount(({ actions, values }) => { + if (values.autoLoadRunning) { + actions.stopAutoLoad() + } + }), ]) From edab9b6497ea28307c0aabb98b372882a0e609d6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 30 Nov 2022 09:29:09 +0100 Subject: [PATCH 09/90] new scene with a flag --- frontend/src/lib/constants.tsx | 1 + frontend/src/scenes/events/Events.tsx | 14 ++-- frontend/src/scenes/events/EventsScene.tsx | 57 ++++++++++++++++ .../src/scenes/events/eventsSceneLogic.tsx | 67 +++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 frontend/src/scenes/events/EventsScene.tsx create mode 100644 frontend/src/scenes/events/eventsSceneLogic.tsx diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index e131517b917a5..9f7a7699c1e63 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -136,6 +136,7 @@ export const FEATURE_FLAGS = { ROLE_BASED_ACCESS: 'role-based-access', // owner: #team-experiments, @liyiy SECONDARY_ONBOARDING_EXPERIMENT: 'secondary-onboarding-experiment', // owner: #team-growth DASHBOARD_TEMPLATES: 'dashboard-templates', // owner @pauldambra + DATA_EXPLORATION_LIVE_EVENTS: 'data-exploration-live-events', // owner @pauldambra } /** Which self-hosted plan's features are available with Cloud's "Standard" plan (aka card attached). */ diff --git a/frontend/src/scenes/events/Events.tsx b/frontend/src/scenes/events/Events.tsx index 019af4103c1ac..535f84cc19ef5 100644 --- a/frontend/src/scenes/events/Events.tsx +++ b/frontend/src/scenes/events/Events.tsx @@ -1,21 +1,25 @@ import { SceneExport } from 'scenes/sceneTypes' -import { eventsTableLogic } from 'scenes/events/eventsTableLogic' import { EventsTable } from 'scenes/events/EventsTable' -import { urls } from 'scenes/urls' import { PageHeader } from 'lib/components/PageHeader' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { EventsScene } from 'scenes/events/EventsScene' export const scene: SceneExport = { component: Events, - logic: eventsTableLogic, - paramsToProps: ({ params: { fixedFilters } }) => ({ fixedFilters, key: 'EventsTable', sceneUrl: urls.events() }), + // logic: eventsTableLogic, + // paramsToProps: ({ params: { fixedFilters } }) => ({ fixedFilters, key: 'EventsTable', sceneUrl: urls.events() }), } export function Events(): JSX.Element { + const { featureFlags } = useValues(featureFlagLogic) + const useDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] return ( <>
- + {useDataExploration ? : } ) } diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx new file mode 100644 index 0000000000000..639cd3fbec126 --- /dev/null +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -0,0 +1,57 @@ +import { useActions, useValues } from 'kea' +import { eventsSceneLogic } from 'scenes/events/eventsSceneLogic' +import { Query } from '~/queries/Query/Query' +import { DataTableNode, NodeKind } from '~/queries/schema' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' +import { isDataTableNode } from '~/queries/utils' +import { tableConfigLogic } from 'lib/components/ResizableTable/tableConfigLogic' +import { teamLogic } from 'scenes/teamLogic' +import { LemonTableConfig } from 'lib/components/ResizableTable/TableConfig' + +export function EventsScene(): JSX.Element { + const { properties, eventFilter } = useValues(eventsSceneLogic) + const { setProperties, setEventFilter } = useActions(eventsSceneLogic) + const { currentTeam } = useValues(teamLogic) + const { selectedColumns } = useValues( + tableConfigLogic({ + startingColumns: (currentTeam && currentTeam.live_events_columns) ?? [], + }) + ) + + const columns = + !selectedColumns || selectedColumns === 'DEFAULT' || selectedColumns.length === 0 + ? defaultDataTableStringColumns + : [ + 'meta.event', + 'person', + ...selectedColumns.map((c) => { + return `event.${c}` + }), + 'meta.timestamp', + ] + + const query: DataTableNode = { + kind: NodeKind.DataTableNode, + columns, + source: { + kind: NodeKind.EventsNode, + properties: properties, + event: eventFilter, + limit: 100, + }, + } + return ( + <> + + { + if (isDataTableNode(query)) { + setProperties(query.source.properties ?? []) + setEventFilter(query.source.event ?? '') + } + }} + /> + + ) +} diff --git a/frontend/src/scenes/events/eventsSceneLogic.tsx b/frontend/src/scenes/events/eventsSceneLogic.tsx new file mode 100644 index 0000000000000..b6dd1291cf01c --- /dev/null +++ b/frontend/src/scenes/events/eventsSceneLogic.tsx @@ -0,0 +1,67 @@ +import { actions, kea, path, reducers } from 'kea' + +import type { eventsSceneLogicType } from './eventsSceneLogicType' +import { AnyPropertyFilter, PropertyFilter } from '~/types' +import { actionToUrl, router, urlToAction } from 'kea-router' +import equal from 'fast-deep-equal' + +export const eventsSceneLogic = kea([ + path(['scenes', 'events', 'eventsSceneLogic']), + + actions({ + setProperties: (properties: AnyPropertyFilter[]) => ({ properties }), + setEventFilter: (event: string) => ({ event }), + }), + reducers({ + properties: [ + [] as PropertyFilter[], + { + setProperties: (_, { properties }) => properties as PropertyFilter[], + }, + ], + eventFilter: [ + '', + { + setEventFilter: (_, { event }) => event, + }, + ], + }), + actionToUrl(({ values }) => ({ + setProperties: () => { + return [ + router.values.location.pathname, + { + ...router.values.searchParams, + properties: values.properties.length === 0 ? undefined : values.properties, + }, + router.values.hashParams, + { replace: true }, + ] + }, + setEventFilter: () => { + return [ + router.values.location.pathname, + { + ...router.values.searchParams, + eventFilter: values.eventFilter || undefined, + }, + router.values.hashParams, + { replace: true }, + ] + }, + })), + + urlToAction(({ actions, values }) => ({ + '*': (_: Record, searchParams: Record): void => { + const nextProperties = searchParams.properties || values.properties || {} + if (!equal(nextProperties, values.properties)) { + actions.setProperties(nextProperties) + } + + const nextEventFilter = searchParams.eventFilter || '' + if (!equal(nextEventFilter, values.eventFilter)) { + actions.setEventFilter(nextEventFilter) + } + }, + })), +]) From 3c2e539e3995da6fde32d978efc4ca611f945766 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 1 Dec 2022 12:29:06 +0100 Subject: [PATCH 10/90] use only string columns --- .../src/queries/nodes/DataTable/DataTable.tsx | 36 ++++-------- .../nodes/DataTable/DataTableExport.tsx | 11 +--- .../queries/nodes/DataTable/renderColumn.tsx | 57 +++++++++---------- .../queries/nodes/DataTable/renderTitle.tsx | 27 ++++----- frontend/src/queries/schema.ts | 13 +---- frontend/src/scenes/events/EventsScene.tsx | 9 +-- 6 files changed, 54 insertions(+), 99 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 45de0a365add7..334e80cddff47 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,9 +1,9 @@ -import { DataTableColumn, DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' +import { DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' import { LemonTable, LemonTableColumn } from 'lib/components/LemonTable' -import { EventType, PropertyFilterType } from '~/types' +import { EventType } from '~/types' import { EventName } from '~/queries/nodes/EventsNode/EventName' import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' import { EventDetails } from 'scenes/events' @@ -21,18 +21,17 @@ interface DataTableProps { } export const defaultDataTableStringColumns: DataTableStringColumn[] = [ - 'meta.event', + 'event', 'person', - 'event.$current_url', - 'person.email', - 'meta.timestamp', + 'properties.$current_url', + 'person.properties.email', + 'timestamp', ] -export const defaultDataTableColumns: DataTableColumn[] = normalizeDataTableColumns(defaultDataTableStringColumns) let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { - const columns = query.columns ? normalizeDataTableColumns(query.columns) : defaultDataTableColumns + const columns = query.columns ?? defaultDataTableStringColumns const showPropertyFilter = query.showPropertyFilter ?? true const showEventFilter = query.showEventFilter ?? true const showActions = query.showActions ?? true @@ -49,11 +48,11 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const dataSource = (response as null | EventsNode['response'])?.results ?? [] const lemonColumns: LemonTableColumn[] = [ - ...columns.map(({ type, key }) => ({ - dataIndex: `${type}.${key}` as any, - title: renderTitle(type, key), + ...columns.map((key) => ({ + dataIndex: key as any, + title: renderTitle(key), render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(type, key, record) + return renderColumn(key, record) }, })), ...(showActions @@ -106,16 +105,3 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { ) } - -function normalizeDataTableColumns(input: (DataTableStringColumn | DataTableColumn)[]): DataTableColumn[] { - return input.map((column) => { - if (typeof column === 'string') { - const [first, ...rest] = column.split('.') - return { - type: first as PropertyFilterType, - key: rest.join('.'), - } - } - return column - }) -} diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index d4c591513e688..0e66c58d1bb18 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -5,7 +5,7 @@ import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' import api from 'lib/api' import { DataTableNode } from '~/queries/schema' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/DataTable' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { const exportContext = { @@ -17,14 +17,7 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void max_limit: 3500, } if (onlySelectedColumns) { - const queryColumns = query.columns || defaultDataTableColumns - exportContext['columns'] = queryColumns.map((column) => { - if (typeof column === 'string') { - return column - } else { - return `${column.type}.${column.key}` - } - }) + exportContext['columns'] = query.columns ?? defaultDataTableStringColumns } triggerExport({ export_format: ExporterFormat.CSV, diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 2abb6d1859b9b..1c0cca443aec0 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -1,4 +1,4 @@ -import { EventType, PropertyFilterType } from '~/types' +import { EventType } from '~/types' import { autoCaptureEventToDescription } from 'lib/utils' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { Link } from 'lib/components/Link' @@ -7,39 +7,34 @@ import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' -export function renderColumn(type: PropertyFilterType, key: string, record: EventType): JSX.Element | string { - if (type === PropertyFilterType.Meta) { - if (key === 'event') { - if (record.event === '$autocapture') { - return autoCaptureEventToDescription(record) - } else { - const content = - const url = record.properties.$sentry_url - return url ? ( - - {content} - - ) : ( - content - ) - } - } else if (key === 'timestamp') { - return +export function renderColumn(key: string, record: EventType): JSX.Element | string { + if (key === 'event') { + if (record.event === '$autocapture') { + return autoCaptureEventToDescription(record) } else { - return String(record[key]) - } - } else if (type === PropertyFilterType.Event) { - return - } else if (type === PropertyFilterType.Person) { - if (key === '') { - return ( - - + const content = + const url = record.properties.$sentry_url + return url ? ( + + {content} + ) : ( + content ) - } else { - return } + } else if (key === 'timestamp') { + return + } else if (key.startsWith('properties.')) { + return + } else if (key.startsWith('person.properties.')) { + return + } else if (key === 'person') { + return ( + + + + ) + } else { + return String(record[key]) } - return
Unknown
} diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index 63f6117ac79d3..eaca7be9665d7 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -1,21 +1,18 @@ import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -export function renderTitle(type: PropertyFilterType, key: string): JSX.Element | string { - if (type === PropertyFilterType.Meta) { - if (key === 'timestamp') { - return 'Time' - } - return key - } else if (type === PropertyFilterType.Event || type === PropertyFilterType.Element) { - return - } else if (type === PropertyFilterType.Person) { - if (key === '') { - return 'Person' - } else { - return - } +export function renderTitle(key: string): JSX.Element | string { + if (key === 'timestamp') { + return 'Time' + } else if (key === 'event') { + return 'Event' + } else if (key === 'person') { + return 'Person' + } else if (key.startsWith('properties.')) { + return + } else if (key.startsWith('person.properties.')) { + return } else { - return String(type) + return String(key) } } diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 80d5f5618c711..fa52266a4f135 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -6,7 +6,6 @@ import { BreakdownType, PropertyGroupFilter, EventType, - PropertyFilterType, IntervalType, BaseMathType, PropertyMathType, @@ -87,7 +86,7 @@ export interface DataTableNode extends Node { /** Source of the events */ source: EventsNode /** Columns shown in the table */ - columns?: DataTableColumn[] | DataTableStringColumn[] + columns?: DataTableStringColumn[] /** Include an event filter above the table (default: true) */ showEventFilter?: boolean /** Include a property filter above the table (default: true) */ @@ -102,11 +101,6 @@ export interface DataTableNode extends Node { expandable?: boolean } -export interface DataTableColumn { - type: PropertyFilterType - key: string -} - // Base class should not be used directly interface InsightsQueryBase extends Node { /** Date range for the query */ @@ -128,10 +122,7 @@ export interface TrendsQuery extends InsightsQueryBase { breakdown?: BreakdownFilter } -// TODO: not supported by "ts-json-schema-generator" nor "typescript-json-schema" :( -// export type PropertyColumnString = `${PropertyFilterType}.${string}` -export type PropertyColumnString = string -export type DataTableStringColumn = PropertyColumnString | 'person' +export type DataTableStringColumn = string // Legacy queries diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index 639cd3fbec126..938c5a04eafa8 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -21,14 +21,7 @@ export function EventsScene(): JSX.Element { const columns = !selectedColumns || selectedColumns === 'DEFAULT' || selectedColumns.length === 0 ? defaultDataTableStringColumns - : [ - 'meta.event', - 'person', - ...selectedColumns.map((c) => { - return `event.${c}` - }), - 'meta.timestamp', - ] + : ['event', 'person', ...selectedColumns.map((c) => `properties.${c}`), 'timestamp'] const query: DataTableNode = { kind: NodeKind.DataTableNode, From 832311ae6d9d5a90745120c2ef608866f8cec1e3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 00:06:42 +0100 Subject: [PATCH 11/90] move around --- .../components/ResizableTable/TableConfig.tsx | 10 +- .../{DataTable => DataNode}/AutoLoad.tsx | 2 +- .../src/queries/nodes/DataNode/DataNode.tsx | 2 +- .../nodes/{DataTable => DataNode}/LoadNew.tsx | 2 +- .../{DataTable => DataNode}/LoadNext.tsx | 2 +- .../nodes/{DataTable => DataNode}/Reload.tsx | 2 +- .../nodes/{ => DataNode}/dataNodeLogic.ts | 0 .../src/queries/nodes/DataTable/DataTable.tsx | 94 ++++++++++--------- .../queries/nodes/DataTable/dataTableLogic.ts | 22 +++++ frontend/src/queries/schema.ts | 2 + 10 files changed, 89 insertions(+), 49 deletions(-) rename frontend/src/queries/nodes/{DataTable => DataNode}/AutoLoad.tsx (90%) rename frontend/src/queries/nodes/{DataTable => DataNode}/LoadNew.tsx (89%) rename frontend/src/queries/nodes/{DataTable => DataNode}/LoadNext.tsx (86%) rename frontend/src/queries/nodes/{DataTable => DataNode}/Reload.tsx (90%) rename frontend/src/queries/nodes/{ => DataNode}/dataNodeLogic.ts (100%) create mode 100644 frontend/src/queries/nodes/DataTable/dataTableLogic.ts diff --git a/frontend/src/lib/components/ResizableTable/TableConfig.tsx b/frontend/src/lib/components/ResizableTable/TableConfig.tsx index 79915ca8eab00..4a84860ba070e 100644 --- a/frontend/src/lib/components/ResizableTable/TableConfig.tsx +++ b/frontend/src/lib/components/ResizableTable/TableConfig.tsx @@ -90,7 +90,15 @@ function ColumnConfigurator({ immutableColumns, defaultColumns }: TableConfigPro style={{ height: `${rowItemHeight}px` }} > {!disabled && } - +
{disabled ? ( diff --git a/frontend/src/queries/nodes/DataTable/AutoLoad.tsx b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx similarity index 90% rename from frontend/src/queries/nodes/DataTable/AutoLoad.tsx rename to frontend/src/queries/nodes/DataNode/AutoLoad.tsx index 276ec6187b746..b897412a8f317 100644 --- a/frontend/src/queries/nodes/DataTable/AutoLoad.tsx +++ b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx @@ -1,5 +1,5 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonSwitch } from 'lib/components/LemonSwitch/LemonSwitch' import { useEffect } from 'react' diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index c762a7acf5e59..4f703fc268c28 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' import { DataNode as DataNodeType } from '~/queries/schema' import { useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { Spinner } from 'lib/components/Spinner/Spinner' interface DataNodeProps { diff --git a/frontend/src/queries/nodes/DataTable/LoadNew.tsx b/frontend/src/queries/nodes/DataNode/LoadNew.tsx similarity index 89% rename from frontend/src/queries/nodes/DataTable/LoadNew.tsx rename to frontend/src/queries/nodes/DataNode/LoadNew.tsx index a78836ef7ba31..406fa2421acc7 100644 --- a/frontend/src/queries/nodes/DataTable/LoadNew.tsx +++ b/frontend/src/queries/nodes/DataNode/LoadNew.tsx @@ -1,5 +1,5 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonButton } from 'lib/components/LemonButton' import { IconRefresh } from 'lib/components/icons' import { Spinner } from 'lib/components/Spinner/Spinner' diff --git a/frontend/src/queries/nodes/DataTable/LoadNext.tsx b/frontend/src/queries/nodes/DataNode/LoadNext.tsx similarity index 86% rename from frontend/src/queries/nodes/DataTable/LoadNext.tsx rename to frontend/src/queries/nodes/DataNode/LoadNext.tsx index 294a588a47d0b..8f08d67e79e46 100644 --- a/frontend/src/queries/nodes/DataTable/LoadNext.tsx +++ b/frontend/src/queries/nodes/DataNode/LoadNext.tsx @@ -1,5 +1,5 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonButton } from 'lib/components/LemonButton' export function LoadNext(): JSX.Element { diff --git a/frontend/src/queries/nodes/DataTable/Reload.tsx b/frontend/src/queries/nodes/DataNode/Reload.tsx similarity index 90% rename from frontend/src/queries/nodes/DataTable/Reload.tsx rename to frontend/src/queries/nodes/DataNode/Reload.tsx index ae32c5349ca23..9fa45a757897d 100644 --- a/frontend/src/queries/nodes/DataTable/Reload.tsx +++ b/frontend/src/queries/nodes/DataNode/Reload.tsx @@ -1,5 +1,5 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonButton } from 'lib/components/LemonButton' import { IconRefresh } from 'lib/components/icons' import { Spinner } from 'lib/components/Spinner/Spinner' diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts similarity index 100% rename from frontend/src/queries/nodes/dataNodeLogic.ts rename to frontend/src/queries/nodes/DataNode/dataNodeLogic.ts diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 334e80cddff47..b6d62c959c525 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,7 +1,7 @@ import { DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/dataNodeLogic' +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonTable, LemonTableColumn } from 'lib/components/LemonTable' import { EventType } from '~/types' import { EventName } from '~/queries/nodes/EventsNode/EventName' @@ -9,11 +9,13 @@ import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFi import { EventDetails } from 'scenes/events' import { EventActions } from '~/queries/nodes/DataTable/EventActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' -import { Reload } from '~/queries/nodes/DataTable/Reload' -import { LoadNext } from '~/queries/nodes/DataTable/LoadNext' +import { Reload } from '~/queries/nodes/DataNode/Reload' +import { LoadNext } from '~/queries/nodes/DataNode/LoadNext' import { renderTitle } from '~/queries/nodes/DataTable/renderTitle' import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' -import { AutoLoad } from '~/queries/nodes/DataTable/AutoLoad' +import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' +import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/dataTableLogic' +import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator' interface DataTableProps { query: DataTableNode @@ -31,21 +33,23 @@ export const defaultDataTableStringColumns: DataTableStringColumn[] = [ let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { - const columns = query.columns ?? defaultDataTableStringColumns const showPropertyFilter = query.showPropertyFilter ?? true const showEventFilter = query.showEventFilter ?? true const showActions = query.showActions ?? true const showExport = query.showExport ?? true const showReload = query.showReload ?? true + const showColumnConfigurator = query.showColumnConfigurator ?? true const expandable = query.expandable ?? true const [id] = useState(() => uniqueNode++) - const dataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } - const logic = dataNodeLogic(dataNodeLogicProps) - const { response, responseLoading, canLoadNextData, canLoadNewData, nextDataLoading, newDataLoading } = - useValues(logic) - const dataSource = (response as null | EventsNode['response'])?.results ?? [] + const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } + const { response, responseLoading, canLoadNextData, canLoadNewData, nextDataLoading, newDataLoading } = useValues( + dataNodeLogic(dataNodeLogicProps) + ) + + const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}` } + const { columns } = useValues(dataTableLogic(dataTableLogicProps)) const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ @@ -67,41 +71,45 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { ] : []), ] + const dataSource = (response as null | EventsNode['response'])?.results ?? [] return ( - - {(showPropertyFilter || showEventFilter || showExport) && ( -
- {showReload && (canLoadNewData ? : )} - {showEventFilter && ( - setQuery?.({ ...query, source })} /> - )} - {showPropertyFilter && ( - setQuery?.({ ...query, source })} - /> - )} - {showExport && } -
- )} - - }, - rowExpandable: () => true, - noIndent: true, - } - : undefined - } - /> - {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } + + + {(showPropertyFilter || showEventFilter || showExport) && ( +
+ {showReload && (canLoadNewData ? : )} + {showEventFilter && ( + setQuery?.({ ...query, source })} /> + )} + {showPropertyFilter && ( + setQuery?.({ ...query, source })} + /> + )} + {showExport && } + {showColumnConfigurator && } +
+ )} + + }, + rowExpandable: () => true, + noIndent: true, + } + : undefined + } + /> + {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } +
) } diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts new file mode 100644 index 0000000000000..9ab366d54c69f --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -0,0 +1,22 @@ +import { actions, kea, key, path, props, reducers } from 'kea' +import type { dataTableLogicType } from './dataTableLogicType' +import { DataTableNode, DataTableStringColumn } from '~/queries/schema' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' + +export interface DataTableLogicProps { + key: string + query: DataTableNode +} + +export const dataTableLogic = kea([ + props({} as DataTableLogicProps), + key((props) => props.key), + path(['queries', 'nodes', 'DataTable', 'dataTableLogic']), + actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), + reducers(({ props }) => ({ + columns: [ + (props.query.columns || defaultDataTableStringColumns) as DataTableStringColumn[], + { setColumns: (_, { columns }) => columns }, + ], + })), +]) diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index fa52266a4f135..a7488401fc5e1 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -97,6 +97,8 @@ export interface DataTableNode extends Node { showExport?: boolean /** Show a reload button */ showReload?: boolean + /** Show a button to configure the table's columns */ + showColumnConfigurator?: boolean /** Can expand row to show raw event data (default: true) */ expandable?: boolean } From ff576daeeeabd6a478a75c8b71df02eb59d035eb Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 00:06:56 +0100 Subject: [PATCH 12/90] configurator --- .../DataTable/columnConfiguratorLogic.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx diff --git a/frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx new file mode 100644 index 0000000000000..745047772c1cd --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx @@ -0,0 +1,27 @@ +import { actions, kea, path, props, reducers } from 'kea' + +import type { columnConfiguratorLogicType } from './columnConfiguratorLogicType' + +export interface ColumnConfiguratorLogicProps { + key: string + columns: string[] +} + +export const columnConfiguratorLogic = kea([ + props({} as ColumnConfiguratorLogicProps), + path(['queries', 'nodes', 'DataTable', 'columnConfiguratorLogic']), + actions({ + showModal: true, + hideModal: true, + }), + reducers({ + modalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + setSelectedColumns: () => false, + }, + ], + }), +]) From 2b4b05e6e8ac6f477438c2ccdb350c9939738d38 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 10:05:06 +0100 Subject: [PATCH 13/90] refactor again --- frontend/src/queries/examples.ts | 2 +- .../columnConfiguratorLogic.tsx | 0 .../src/queries/nodes/DataTable/DataTable.tsx | 26 +++---------- .../nodes/DataTable/DataTableExport.tsx | 2 +- .../queries/nodes/DataTable/dataTableLogic.ts | 2 +- .../src/queries/nodes/DataTable/defaults.ts | 9 +++++ frontend/src/scenes/events/EventsScene.tsx | 37 ++++++++----------- .../src/scenes/events/eventsSceneLogic.tsx | 24 ++++++++++++ 8 files changed, 57 insertions(+), 45 deletions(-) rename frontend/src/queries/nodes/DataTable/{ => ColumnConfigurator}/columnConfiguratorLogic.tsx (100%) create mode 100644 frontend/src/queries/nodes/DataTable/defaults.ts diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 6929294bc6637..b77f708dd5c67 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -8,7 +8,7 @@ import { PropertyMathType, FilterLogicalOperator, } from '~/types' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' const Events: EventsNode = { kind: NodeKind.EventsNode, diff --git a/frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx similarity index 100% rename from frontend/src/queries/nodes/DataTable/columnConfiguratorLogic.tsx rename to frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index b6d62c959c525..b3393e5ad784a 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,4 +1,4 @@ -import { DataTableNode, DataTableStringColumn, EventsNode } from '~/queries/schema' +import { DataTableNode, EventsNode } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -15,21 +15,13 @@ import { renderTitle } from '~/queries/nodes/DataTable/renderTitle' import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/dataTableLogic' -import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator' +import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' interface DataTableProps { query: DataTableNode setQuery?: (node: DataTableNode) => void } -export const defaultDataTableStringColumns: DataTableStringColumn[] = [ - 'event', - 'person', - 'properties.$current_url', - 'person.properties.email', - 'timestamp', -] - let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { @@ -72,22 +64,16 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { : []), ] const dataSource = (response as null | EventsNode['response'])?.results ?? [] + const setQuerySource = (source: EventsNode): void => setQuery?.({ ...query, source }) return ( - {(showPropertyFilter || showEventFilter || showExport) && ( + {(showReload || showEventFilter || showPropertyFilter || showExport || showColumnConfigurator) && (
{showReload && (canLoadNewData ? : )} - {showEventFilter && ( - setQuery?.({ ...query, source })} /> - )} - {showPropertyFilter && ( - setQuery?.({ ...query, source })} - /> - )} + {showEventFilter && } + {showPropertyFilter && } {showExport && } {showColumnConfigurator && }
diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 0e66c58d1bb18..9aec2f19d1348 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -5,7 +5,7 @@ import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' import api from 'lib/api' import { DataTableNode } from '~/queries/schema' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { const exportContext = { diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 9ab366d54c69f..b21aabfa1047a 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -1,7 +1,7 @@ import { actions, kea, key, path, props, reducers } from 'kea' import type { dataTableLogicType } from './dataTableLogicType' import { DataTableNode, DataTableStringColumn } from '~/queries/schema' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' +import { defaultDataTableStringColumns } from './defaults' export interface DataTableLogicProps { key: string diff --git a/frontend/src/queries/nodes/DataTable/defaults.ts b/frontend/src/queries/nodes/DataTable/defaults.ts new file mode 100644 index 0000000000000..ccd8ba08a7bef --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/defaults.ts @@ -0,0 +1,9 @@ +import { DataTableStringColumn } from '~/queries/schema' + +export const defaultDataTableStringColumns: DataTableStringColumn[] = [ + 'event', + 'person', + 'properties.$current_url', + 'person.properties.email', + 'timestamp', +] diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index 938c5a04eafa8..f362b6e414f6e 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -2,26 +2,13 @@ import { useActions, useValues } from 'kea' import { eventsSceneLogic } from 'scenes/events/eventsSceneLogic' import { Query } from '~/queries/Query/Query' import { DataTableNode, NodeKind } from '~/queries/schema' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/DataTable' import { isDataTableNode } from '~/queries/utils' -import { tableConfigLogic } from 'lib/components/ResizableTable/tableConfigLogic' -import { teamLogic } from 'scenes/teamLogic' -import { LemonTableConfig } from 'lib/components/ResizableTable/TableConfig' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { objectsEqual } from 'lib/utils' export function EventsScene(): JSX.Element { - const { properties, eventFilter } = useValues(eventsSceneLogic) - const { setProperties, setEventFilter } = useActions(eventsSceneLogic) - const { currentTeam } = useValues(teamLogic) - const { selectedColumns } = useValues( - tableConfigLogic({ - startingColumns: (currentTeam && currentTeam.live_events_columns) ?? [], - }) - ) - - const columns = - !selectedColumns || selectedColumns === 'DEFAULT' || selectedColumns.length === 0 - ? defaultDataTableStringColumns - : ['event', 'person', ...selectedColumns.map((c) => `properties.${c}`), 'timestamp'] + const { properties, eventFilter, columns } = useValues(eventsSceneLogic) + const { setProperties, setEventFilter, setColumns } = useActions(eventsSceneLogic) const query: DataTableNode = { kind: NodeKind.DataTableNode, @@ -35,13 +22,19 @@ export function EventsScene(): JSX.Element { } return ( <> - { - if (isDataTableNode(query)) { - setProperties(query.source.properties ?? []) - setEventFilter(query.source.event ?? '') + setQuery={(newQuery) => { + if (isDataTableNode(newQuery)) { + if (!objectsEqual(newQuery.source.properties ?? [], query.source.properties ?? [])) { + setProperties(newQuery.source.properties ?? []) + } + if (!objectsEqual(newQuery.source.event ?? '', query.source.event ?? '')) { + setEventFilter(newQuery.source.event ?? '') + } + if (!objectsEqual(newQuery.columns ?? [], query.columns ?? [])) { + setColumns(newQuery.columns ?? defaultDataTableStringColumns) + } } }} /> diff --git a/frontend/src/scenes/events/eventsSceneLogic.tsx b/frontend/src/scenes/events/eventsSceneLogic.tsx index b6dd1291cf01c..f9cd972bf4df5 100644 --- a/frontend/src/scenes/events/eventsSceneLogic.tsx +++ b/frontend/src/scenes/events/eventsSceneLogic.tsx @@ -4,6 +4,7 @@ import type { eventsSceneLogicType } from './eventsSceneLogicType' import { AnyPropertyFilter, PropertyFilter } from '~/types' import { actionToUrl, router, urlToAction } from 'kea-router' import equal from 'fast-deep-equal' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' export const eventsSceneLogic = kea([ path(['scenes', 'events', 'eventsSceneLogic']), @@ -11,6 +12,7 @@ export const eventsSceneLogic = kea([ actions({ setProperties: (properties: AnyPropertyFilter[]) => ({ properties }), setEventFilter: (event: string) => ({ event }), + setColumns: (columns: string[]) => ({ columns }), }), reducers({ properties: [ @@ -25,6 +27,12 @@ export const eventsSceneLogic = kea([ setEventFilter: (_, { event }) => event, }, ], + columns: [ + defaultDataTableStringColumns as string[], + { + setColumns: (_, { columns }) => columns, + }, + ], }), actionToUrl(({ values }) => ({ setProperties: () => { @@ -49,6 +57,17 @@ export const eventsSceneLogic = kea([ { replace: true }, ] }, + setColumns: () => { + return [ + router.values.location.pathname, + { + ...router.values.searchParams, + columns: values.columns || undefined, + }, + router.values.hashParams, + { replace: true }, + ] + }, })), urlToAction(({ actions, values }) => ({ @@ -62,6 +81,11 @@ export const eventsSceneLogic = kea([ if (!equal(nextEventFilter, values.eventFilter)) { actions.setEventFilter(nextEventFilter) } + + const nextColumns = searchParams.columns || defaultDataTableStringColumns + if (!equal(nextColumns, values.columns)) { + actions.setColumns(nextColumns) + } }, })), ]) From 5757766307d30bb7ddcf106a16cbf29063f7fd22 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 10:28:31 +0100 Subject: [PATCH 14/90] column config --- .../ColumnConfigurator/ColumnConfigurator.tsx | 251 ++++++++++++++++++ .../columnConfiguratorLogic.tsx | 44 ++- .../src/queries/nodes/DataTable/DataTable.tsx | 2 +- .../queries/nodes/DataTable/dataTableLogic.ts | 7 +- 4 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx new file mode 100644 index 0000000000000..d5dc0aef861d8 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -0,0 +1,251 @@ +import { useActions, useValues, BindLogic } from 'kea' +import { LemonButton } from 'lib/components/LemonButton' +import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' +import { IconTuning, SortableDragIcon } from 'lib/components/icons' +import { RestrictedArea, RestrictedComponentProps, RestrictionScope } from 'lib/components/RestrictedArea' +import { LemonCheckbox } from 'lib/components/LemonCheckbox' +import clsx from 'clsx' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { Tooltip } from 'lib/components/Tooltip' +import { CloseOutlined, LockOutlined } from '@ant-design/icons' +import { + SortableContainer as sortableContainer, + SortableElement as sortableElement, + SortableHandle as sortableHandle, +} from 'react-sortable-hoc' +import VirtualizedList, { ListRowProps } from 'react-virtualized/dist/es/List' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import Modal from 'antd/lib/modal/Modal' +import { Button, Col, Row, Space } from 'antd' +import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { TeamMembershipLevel } from 'lib/constants' +import { useState } from 'react' +import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic' +import { defaultDataTableStringColumns } from '../defaults' +import { DataTableNode } from '~/queries/schema' + +let uniqueNode = 0 + +interface ColumnConfiguratorProps { + query: DataTableNode + setQuery?: (node: DataTableNode) => void +} + +export function ColumnConfigurator({ query, setQuery }: ColumnConfiguratorProps): JSX.Element { + const { columns } = useValues(dataTableLogic) + + const [key] = useState(() => String(uniqueNode++)) + const columnConfiguratorLogicProps: ColumnConfiguratorLogicProps = { + key, + columns, + setColumns: (columns: string[]) => { + setQuery?.({ ...query, columns }) + }, + } + const { showModal } = useActions(columnConfiguratorLogic(columnConfiguratorLogicProps)) + + return ( + + } + onClick={showModal} + > + Configure columns + + + + ) +} + +function ColumnConfiguratorModal(): JSX.Element { + // the virtualised list doesn't support gaps between items in the list + // setting the container to be larger than we need + // and adding a container with a smaller height to each row item + // allows the new row item to set a margin around itself + const rowContainerHeight = 36 + const rowItemHeight = 32 + + const { modalVisible, columns } = useValues(columnConfiguratorLogic) + const { hideModal, setColumns, resetColumns, selectColumn, unselectColumn, save, toggleSaveAsDefault } = + useActions(columnConfiguratorLogic) + + const immutableColumns: string[] = [] + + function SaveColumnsAsDefault({ isRestricted }: RestrictedComponentProps): JSX.Element { + return ( + + ) + } + const DragHandle = sortableHandle(() => ( + + + + )) + const SelectedColumn = ({ column, disabled }: { column: string; disabled?: boolean }): JSX.Element => { + return ( +
+ {!disabled && } + +
+ + {disabled ? ( + + ) : ( + unselectColumn(column)} + /> + )} + +
+
+ ) + } + + const SortableSelectedColumn = sortableElement(SelectedColumn) + + const SortableSelectedColumnRenderer = ({ index, style, key }: ListRowProps): JSX.Element => { + const disabled = immutableColumns?.includes(columns[index]) + return ( +
+ {disabled && } + {!disabled && ( + + )} +
+ ) + } + + const SortableColumnList = sortableContainer(() => ( +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + + ) + }} + +
+ )) + + const handleSort = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void => { + const newColumns = [...columns] + const [removed] = newColumns.splice(oldIndex, 1) + newColumns.splice(newIndex, 0, removed) + setColumns(newColumns) + } + + return ( + + + + + + + + + + } + > +
+ + +

+ Visible columns ({columns.length}) - Drag to reorder +

+ + + +

Available columns

+
+ + {({ height, width }: { height: number; width: number }) => { + return ( + value && selectColumn(String(value))} + popoverEnabled={false} + selectFirstItem={false} + excludedProperties={{ + [TaxonomicFilterGroupType.EventProperties]: columns, + [TaxonomicFilterGroupType.EventFeatureFlags]: columns, + }} + /> + ) + }} + +
+ +
+ +
+
+ ) +} diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx index 745047772c1cd..2b63773742ff5 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx @@ -1,10 +1,11 @@ -import { actions, kea, path, props, reducers } from 'kea' - +import { actions, kea, listeners, path, props, propsChanged, reducers } from 'kea' import type { columnConfiguratorLogicType } from './columnConfiguratorLogicType' +import { teamLogic } from 'scenes/teamLogic' export interface ColumnConfiguratorLogicProps { key: string columns: string[] + setColumns: (columns: string[]) => void } export const columnConfiguratorLogic = kea([ @@ -13,15 +14,50 @@ export const columnConfiguratorLogic = kea([ actions({ showModal: true, hideModal: true, + selectColumn: (column: string) => ({ column }), + unselectColumn: (column: string) => ({ column }), + resetColumns: (columns: string[]) => ({ columns }), + setColumns: (columns: string[]) => ({ columns }), + toggleSaveAsDefault: true, + save: true, }), - reducers({ + reducers(({ props }) => ({ modalVisible: [ false, { showModal: () => true, hideModal: () => false, - setSelectedColumns: () => false, + setColumns: () => false, + }, + ], + columns: [ + props.columns, + { + setColumns: (_, { columns }) => columns, + selectColumn: (state, { column }) => Array.from(new Set([...state, column])), + unselectColumn: (state, { column }) => state.filter((c) => c !== column), + resetColumns: (_, { columns }) => columns, + }, + ], + saveAsDefault: [ + false, + { + toggleSaveAsDefault: (state) => !state, }, ], + })), + propsChanged(({ actions, props }, oldProps) => { + if (JSON.stringify(props.columns) !== JSON.stringify(oldProps.columns)) { + actions.setColumns(props.columns) + } }), + listeners(({ values, actions, props }) => ({ + save: () => { + props.setColumns(values.columns) + if (values.saveAsDefault) { + teamLogic.findMounted()?.actions.updateCurrentTeam({ live_events_columns: values.columns }) + actions.toggleSaveAsDefault() + } + }, + })), ]) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index b3393e5ad784a..17eb0e135427d 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -75,7 +75,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {showEventFilter && } {showPropertyFilter && } {showExport && } - {showColumnConfigurator && } + {showColumnConfigurator && }
)} ([ { setColumns: (_, { columns }) => columns }, ], })), + propsChanged(({ actions, props }, oldProps) => { + if (JSON.stringify(props.query.columns) !== JSON.stringify(oldProps.query.columns)) { + actions.setColumns(props.query.columns ?? defaultDataTableStringColumns) + } + }), ]) From b2ec7aba44d34a5c590c0053d56914e3764576a8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 10:29:15 +0100 Subject: [PATCH 15/90] actually select them --- .../nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index d5dc0aef861d8..08bb3df02c4f7 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -226,7 +226,7 @@ function ColumnConfiguratorModal(): JSX.Element { TaxonomicFilterGroupType.EventFeatureFlags, ]} value={undefined} - onChange={(_, value) => value && selectColumn(String(value))} + onChange={(_, value) => value && selectColumn(`properties.${value}`)} popoverEnabled={false} selectFirstItem={false} excludedProperties={{ From cb9ad0f09152aa2c509d34e17bb0ac7a309ad9d1 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 15:14:55 +0100 Subject: [PATCH 16/90] actually select them --- frontend/src/queries/nodes/DataTable/DataTable.tsx | 4 ++-- .../nodes/DataTable/{EventActions.tsx => EventRowActions.tsx} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/queries/nodes/DataTable/{EventActions.tsx => EventRowActions.tsx} (97%) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 17eb0e135427d..bb692af8eb737 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -7,7 +7,7 @@ import { EventType } from '~/types' import { EventName } from '~/queries/nodes/EventsNode/EventName' import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' import { EventDetails } from 'scenes/events' -import { EventActions } from '~/queries/nodes/DataTable/EventActions' +import { EventRowActions } from '~/queries/nodes/DataTable/EventRowActions' import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' import { Reload } from '~/queries/nodes/DataNode/Reload' import { LoadNext } from '~/queries/nodes/DataNode/LoadNext' @@ -57,7 +57,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { dataIndex: 'more' as any, title: '', render: function RenderMore(_: any, record: EventType) { - return + return }, }, ] diff --git a/frontend/src/queries/nodes/DataTable/EventActions.tsx b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx similarity index 97% rename from frontend/src/queries/nodes/DataTable/EventActions.tsx rename to frontend/src/queries/nodes/DataTable/EventRowActions.tsx index e843e677816ac..01bcec59b2f99 100644 --- a/frontend/src/queries/nodes/DataTable/EventActions.tsx +++ b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx @@ -10,7 +10,7 @@ interface EventActionProps { event: EventType } -export function EventActions({ event }: EventActionProps): JSX.Element { +export function EventRowActions({ event }: EventActionProps): JSX.Element { let insightParams: Partial | undefined if (event.event === '$pageview') { insightParams = { From 68c28dad8162f350fac42ab246390f6d063fbf49 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 15:15:40 +0100 Subject: [PATCH 17/90] hide on save --- .../DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx index 2b63773742ff5..f89c7d402bf17 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx @@ -27,7 +27,7 @@ export const columnConfiguratorLogic = kea([ { showModal: () => true, hideModal: () => false, - setColumns: () => false, + save: () => false, }, ], columns: [ From 32d8e912a734f84a15e927f12030589e84761ae5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 15:33:58 +0100 Subject: [PATCH 18/90] make the old events table work with the new events format --- frontend/src/scenes/events/EventsTable.tsx | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/scenes/events/EventsTable.tsx b/frontend/src/scenes/events/EventsTable.tsx index 6f262817e1294..ea570764614aa 100644 --- a/frontend/src/scenes/events/EventsTable.tsx +++ b/frontend/src/scenes/events/EventsTable.tsx @@ -279,9 +279,11 @@ export function EventsTable({ if (selectedColumns === 'DEFAULT') { columnsSoFar = [...defaultColumns] } else { - const columnsToBeMapped = !showPersonColumn + let columnsToBeMapped = !showPersonColumn ? selectedColumns.filter((column) => column !== 'person') : selectedColumns + // If user has saved `timestamp`, a column only used in the Data Exploration version of this feature, remove it + columnsToBeMapped = columnsToBeMapped.filter((c) => c !== 'timestamp') columnsSoFar = columnsToBeMapped.map( (e, index): LemonTableColumn => { const defaultColumn = defaultColumns.find((d) => d.key === e) @@ -304,9 +306,16 @@ export function EventsTable({ }, } } else { + // If the user has saved their columns for the new data exploration data table, make them work here + // This code will be removed once we release the new events list feature. + const key = e.startsWith('properties.') + ? e.substring(11) + : e.startsWith('person.properties.') + ? e.substring(18) + : e return { - title: keyMapping['event'][e] ? keyMapping['event'][e].label : e, - key: e, + title: keyMapping['event'][key] ? keyMapping['event'][key].label : key, + key: key, render: function render(_, item: EventsTableRowItem) { const { event } = item if (!event) { @@ -320,13 +329,13 @@ export function EventsTable({ return ( ) } - return + return }, } } From 079bc13b49af3b24c5ce0af4d3ca3005fac7c18d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 15:53:19 +0100 Subject: [PATCH 19/90] configure columns --- .../src/queries/nodes/DataTable/DataTable.tsx | 8 ++++++- .../queries/nodes/DataTable/dataTableLogic.ts | 9 +++++--- frontend/src/scenes/events/EventsScene.tsx | 7 +++---- .../src/scenes/events/eventsSceneLogic.tsx | 21 ++----------------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index bb692af8eb737..6feb3744206bc 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -16,6 +16,8 @@ import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/dataTableLogic' import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' +import { teamLogic } from 'scenes/teamLogic' +import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' interface DataTableProps { query: DataTableNode @@ -40,8 +42,12 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { dataNodeLogic(dataNodeLogicProps) ) - const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}` } + const { currentTeam } = useValues(teamLogic) + const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns + console.log({ defaultColumns }) + const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}`, defaultColumns } const { columns } = useValues(dataTableLogic(dataTableLogicProps)) + console.log({ columns }) const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 4299a587c267a..7d8f58388cba2 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -6,6 +6,7 @@ import { defaultDataTableStringColumns } from './defaults' export interface DataTableLogicProps { key: string query: DataTableNode + defaultColumns?: DataTableStringColumn[] } export const dataTableLogic = kea([ @@ -15,13 +16,15 @@ export const dataTableLogic = kea([ actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), reducers(({ props }) => ({ columns: [ - (props.query.columns || defaultDataTableStringColumns) as DataTableStringColumn[], + (props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns) as DataTableStringColumn[], { setColumns: (_, { columns }) => columns }, ], })), propsChanged(({ actions, props }, oldProps) => { - if (JSON.stringify(props.query.columns) !== JSON.stringify(oldProps.query.columns)) { - actions.setColumns(props.query.columns ?? defaultDataTableStringColumns) + const newColumns = props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns + const oldColumns = oldProps.query.columns ?? oldProps.defaultColumns ?? defaultDataTableStringColumns + if (JSON.stringify(newColumns) !== JSON.stringify(oldColumns)) { + actions.setColumns(newColumns) } }), ]) diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index f362b6e414f6e..88ddbd901ebc7 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -3,7 +3,6 @@ import { eventsSceneLogic } from 'scenes/events/eventsSceneLogic' import { Query } from '~/queries/Query/Query' import { DataTableNode, NodeKind } from '~/queries/schema' import { isDataTableNode } from '~/queries/utils' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' import { objectsEqual } from 'lib/utils' export function EventsScene(): JSX.Element { @@ -12,7 +11,7 @@ export function EventsScene(): JSX.Element { const query: DataTableNode = { kind: NodeKind.DataTableNode, - columns, + columns: columns ?? undefined, source: { kind: NodeKind.EventsNode, properties: properties, @@ -32,8 +31,8 @@ export function EventsScene(): JSX.Element { if (!objectsEqual(newQuery.source.event ?? '', query.source.event ?? '')) { setEventFilter(newQuery.source.event ?? '') } - if (!objectsEqual(newQuery.columns ?? [], query.columns ?? [])) { - setColumns(newQuery.columns ?? defaultDataTableStringColumns) + if (!objectsEqual(newQuery.columns ?? null, query.columns ?? null)) { + setColumns(newQuery.columns ?? null) } } }} diff --git a/frontend/src/scenes/events/eventsSceneLogic.tsx b/frontend/src/scenes/events/eventsSceneLogic.tsx index f9cd972bf4df5..196b995fc42b7 100644 --- a/frontend/src/scenes/events/eventsSceneLogic.tsx +++ b/frontend/src/scenes/events/eventsSceneLogic.tsx @@ -4,7 +4,6 @@ import type { eventsSceneLogicType } from './eventsSceneLogicType' import { AnyPropertyFilter, PropertyFilter } from '~/types' import { actionToUrl, router, urlToAction } from 'kea-router' import equal from 'fast-deep-equal' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' export const eventsSceneLogic = kea([ path(['scenes', 'events', 'eventsSceneLogic']), @@ -12,7 +11,7 @@ export const eventsSceneLogic = kea([ actions({ setProperties: (properties: AnyPropertyFilter[]) => ({ properties }), setEventFilter: (event: string) => ({ event }), - setColumns: (columns: string[]) => ({ columns }), + setColumns: (columns: string[] | null) => ({ columns }), }), reducers({ properties: [ @@ -28,7 +27,7 @@ export const eventsSceneLogic = kea([ }, ], columns: [ - defaultDataTableStringColumns as string[], + null as null | string[], { setColumns: (_, { columns }) => columns, }, @@ -57,17 +56,6 @@ export const eventsSceneLogic = kea([ { replace: true }, ] }, - setColumns: () => { - return [ - router.values.location.pathname, - { - ...router.values.searchParams, - columns: values.columns || undefined, - }, - router.values.hashParams, - { replace: true }, - ] - }, })), urlToAction(({ actions, values }) => ({ @@ -81,11 +69,6 @@ export const eventsSceneLogic = kea([ if (!equal(nextEventFilter, values.eventFilter)) { actions.setEventFilter(nextEventFilter) } - - const nextColumns = searchParams.columns || defaultDataTableStringColumns - if (!equal(nextColumns, values.columns)) { - actions.setColumns(nextColumns) - } }, })), ]) From 509dbff98d9116198d8f1f3c2fecbf40b929d949 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 16:05:46 +0100 Subject: [PATCH 20/90] clean up, add test --- .../ColumnConfigurator/ColumnConfigurator.tsx | 12 +-- .../columnConfiguratorLogic.test.ts | 73 +++++++++++++++++++ .../columnConfiguratorLogic.tsx | 3 +- 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 08bb3df02c4f7..952f5ae93887a 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -69,11 +69,9 @@ function ColumnConfiguratorModal(): JSX.Element { const rowItemHeight = 32 const { modalVisible, columns } = useValues(columnConfiguratorLogic) - const { hideModal, setColumns, resetColumns, selectColumn, unselectColumn, save, toggleSaveAsDefault } = + const { hideModal, setColumns, selectColumn, unselectColumn, save, toggleSaveAsDefault } = useActions(columnConfiguratorLogic) - const immutableColumns: string[] = [] - function SaveColumnsAsDefault({ isRestricted }: RestrictedComponentProps): JSX.Element { return ( { - const disabled = immutableColumns?.includes(columns[index]) return (
- {disabled && } - {!disabled && ( - - )} +
) } @@ -183,7 +177,7 @@ function ColumnConfiguratorModal(): JSX.Element { footer={ - diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts new file mode 100644 index 0000000000000..7885ff11e190c --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts @@ -0,0 +1,73 @@ +import { columnConfiguratorLogic } from './columnConfiguratorLogic' +import { expectLogic } from 'kea-test-utils' +import { initKeaTests } from '~/test/init' +import { teamLogic } from 'scenes/teamLogic' + +describe('columnConfiguratorLogic', () => { + let logic: ReturnType + + const startingColumns = ['a', 'b', 'ant', 'aardvark'] + + beforeEach(() => { + initKeaTests() + logic = columnConfiguratorLogic({ key: 'uniqueKey', columns: startingColumns, setColumns: () => {} }) + logic.mount() + }) + + it('starts with expected defaults', async () => { + await expectLogic(logic).toMatchValues({ + modalVisible: false, + columns: startingColumns, + }) + }) + + it('can show modal', async () => { + await expectLogic(logic, () => logic.actions.showModal()).toMatchValues({ + modalVisible: true, + }) + }) + + it('can hide the modal', async () => { + await expectLogic(logic, () => logic.actions.hideModal()).toMatchValues({ + modalVisible: false, + }) + }) + + it('sets modal to hidden when user has selected and saved columns', async () => { + await expectLogic(logic, () => { + logic.actions.showModal() + logic.actions.setColumns(['a']) + logic.actions.save() + }).toMatchValues({ + modalVisible: false, + }) + }) + + it('cannot duplicate columns', async () => { + await expectLogic(logic, () => { + logic.actions.selectColumn('added') + logic.actions.selectColumn('added') + }).toMatchValues({ + columns: ['a', 'b', 'ant', 'aardvark', 'added'], + }) + }) + + it('sets toggle to save columns as default', async () => { + await expectLogic(logic, () => { + logic.actions.toggleSaveAsDefault() + }).toMatchValues({ + saveAsDefault: true, + }) + }) + + it('saves columns as default', async () => { + await expectLogic(logic, () => { + logic.actions.selectColumn('added') + logic.actions.toggleSaveAsDefault() + logic.actions.save() + }).toDispatchActions([ + teamLogic.actionCreators.updateCurrentTeam({ live_events_columns: ['a', 'b', 'ant', 'aardvark', 'added'] }), + logic.actionCreators.toggleSaveAsDefault(), + ]) + }) +}) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx index f89c7d402bf17..5ddc3109d3a67 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx @@ -16,7 +16,6 @@ export const columnConfiguratorLogic = kea([ hideModal: true, selectColumn: (column: string) => ({ column }), unselectColumn: (column: string) => ({ column }), - resetColumns: (columns: string[]) => ({ columns }), setColumns: (columns: string[]) => ({ columns }), toggleSaveAsDefault: true, save: true, @@ -33,10 +32,10 @@ export const columnConfiguratorLogic = kea([ columns: [ props.columns, { + showModal: () => props.columns, setColumns: (_, { columns }) => columns, selectColumn: (state, { column }) => Array.from(new Set([...state, column])), unselectColumn: (state, { column }) => state.filter((c) => c !== column), - resetColumns: (_, { columns }) => columns, }, ], saveAsDefault: [ From cfb22eb2d11a25c10a2206620decfbe3a5deae31 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 16:30:25 +0100 Subject: [PATCH 21/90] move away from antd --- .../ColumnConfigurator.scss | 73 +++++++++++++++++++ .../ColumnConfigurator/ColumnConfigurator.tsx | 61 +++++++--------- 2 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss new file mode 100644 index 0000000000000..485672eb60383 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss @@ -0,0 +1,73 @@ +.ColumnConfiguratorModal { + .text-blue { + color: var(--primary); + } + + .column-display-item { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0.5rem; + overflow: hidden; + border-radius: var(--radius); + margin: calc(var(--radius) / 2) 0; + + &:hover { + background-color: var(--bg-mid); + } + + &.disabled { + cursor: not-allowed; + color: var(--muted); + background-color: var(--bg-mid); + } + + &.selected { + background-color: var(--primary-bg-hover); + } + } + + &.main-content { + margin-bottom: 0.5rem; + + .lists { + background-color: var(--side); + border-radius: 0.25rem; + padding: 0.5rem; + margin-left: 0 !important; + margin-right: 0 !important; + } + } + + .selected-column-col { + display: flex; + flex-direction: column; + } + + .drag-handle { + cursor: move; + color: var(--default); + font-size: 1.2em; + padding-right: 0.25rem; + + svg { + transform: rotate(90deg) translateX(2px); + } + } +} + +.column-configurator-modal-sortable-container { + z-index: 9999; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0.5rem; + background-color: var(--primary-bg-hover); + + .drag-handle { + color: var(--default); + font-size: 1.2em; + padding-right: 0.25rem; + transform: rotate(90deg) translateX(2px); + } +} diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 952f5ae93887a..76ce655e258d2 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -1,3 +1,4 @@ +import './ColumnConfigurator.scss' import { useActions, useValues, BindLogic } from 'kea' import { LemonButton } from 'lib/components/LemonButton' import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' @@ -15,8 +16,7 @@ import { } from 'react-sortable-hoc' import VirtualizedList, { ListRowProps } from 'react-virtualized/dist/es/List' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import Modal from 'antd/lib/modal/Modal' -import { Button, Col, Row, Space } from 'antd' +import { Col, Row } from 'antd' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { TeamMembershipLevel } from 'lib/constants' @@ -24,6 +24,7 @@ import { useState } from 'react' import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic' import { defaultDataTableStringColumns } from '../defaults' import { DataTableNode } from '~/queries/schema' +import { LemonModal } from 'lib/components/LemonModal' let uniqueNode = 0 @@ -106,7 +107,7 @@ function ColumnConfiguratorModal(): JSX.Element { : column } /> -
+
{disabled ? ( @@ -159,40 +160,27 @@ function ColumnConfiguratorModal(): JSX.Element { } return ( - - - - - - - - - + +
+ + Close + + + Save + + } > -
+

@@ -211,6 +199,9 @@ function ColumnConfiguratorModal(): JSX.Element {
{({ height, width }: { height: number; width: number }) => { + const trimmedProperties = columns.map((c) => + c.replace('person.', '').replace('properties.', '') + ) return ( ) @@ -240,6 +231,6 @@ function ColumnConfiguratorModal(): JSX.Element { scope={RestrictionScope.Project} />
- + ) } From e043ad054035e557057339a82bc1b28394a2f048 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 17:11:08 +0100 Subject: [PATCH 22/90] less ants, move move --- .../ColumnConfigurator.scss | 35 +++++++++++-------- .../ColumnConfigurator/ColumnConfigurator.tsx | 30 ++++++---------- .../columnConfiguratorLogic.tsx | 11 ++++-- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss index 485672eb60383..8fe4b109ea71c 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss @@ -1,9 +1,26 @@ .ColumnConfiguratorModal { - .text-blue { - color: var(--primary); + .Columns { + width: 700px; + display: flex; + column-gap: 1rem; + @media (max-width: 960px) { + display: block; + width: auto; + } + + background-color: var(--side); + border-radius: 0.25rem; + padding: 0.5rem; } - .column-display-item { + .HalfColumn { + width: 50%; + @media (max-width: 960px) { + width: 100%; + } + } + + .SelectedColumn { display: flex; align-items: center; justify-content: flex-start; @@ -27,18 +44,6 @@ } } - &.main-content { - margin-bottom: 0.5rem; - - .lists { - background-color: var(--side); - border-radius: 0.25rem; - padding: 0.5rem; - margin-left: 0 !important; - margin-right: 0 !important; - } - } - .selected-column-col { display: flex; flex-direction: column; diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 76ce655e258d2..9e615595e42eb 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -16,7 +16,6 @@ import { } from 'react-sortable-hoc' import VirtualizedList, { ListRowProps } from 'react-virtualized/dist/es/List' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { Col, Row } from 'antd' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { TeamMembershipLevel } from 'lib/constants' @@ -70,7 +69,7 @@ function ColumnConfiguratorModal(): JSX.Element { const rowItemHeight = 32 const { modalVisible, columns } = useValues(columnConfiguratorLogic) - const { hideModal, setColumns, selectColumn, unselectColumn, save, toggleSaveAsDefault } = + const { hideModal, moveColumn, setColumns, selectColumn, unselectColumn, save, toggleSaveAsDefault } = useActions(columnConfiguratorLogic) function SaveColumnsAsDefault({ isRestricted }: RestrictedComponentProps): JSX.Element { @@ -94,7 +93,7 @@ function ColumnConfiguratorModal(): JSX.Element { const SelectedColumn = ({ column, disabled }: { column: string; disabled?: boolean }): JSX.Element => { return (
{!disabled && } @@ -152,13 +151,6 @@ function ColumnConfiguratorModal(): JSX.Element {
)) - const handleSort = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void => { - const newColumns = [...columns] - const [removed] = newColumns.splice(oldIndex, 1) - newColumns.splice(newIndex, 0, removed) - setColumns(newColumns) - } - return ( } > -
- - +
+
+

Visible columns ({columns.length}) - Drag to reorder

moveColumn(oldIndex, newIndex)} distance={5} useDragHandle lockAxis="y" /> - - +
+

Available columns

-
+
{({ height, width }: { height: number; width: number }) => { const trimmedProperties = columns.map((c) => @@ -223,8 +215,8 @@ function ColumnConfiguratorModal(): JSX.Element { }}
- - +
+
([ props({} as ColumnConfiguratorLogicProps), path(['queries', 'nodes', 'DataTable', 'columnConfiguratorLogic']), + key((props) => props.key), actions({ showModal: true, hideModal: true, selectColumn: (column: string) => ({ column }), unselectColumn: (column: string) => ({ column }), setColumns: (columns: string[]) => ({ columns }), + moveColumn: (oldIndex: number, newIndex: number) => ({ oldIndex, newIndex }), toggleSaveAsDefault: true, save: true, }), @@ -32,10 +34,15 @@ export const columnConfiguratorLogic = kea([ columns: [ props.columns, { - showModal: () => props.columns, setColumns: (_, { columns }) => columns, selectColumn: (state, { column }) => Array.from(new Set([...state, column])), unselectColumn: (state, { column }) => state.filter((c) => c !== column), + moveColumn: (state, { oldIndex, newIndex }) => { + const newColumns = [...state] + const [removed] = newColumns.splice(oldIndex, 1) + newColumns.splice(newIndex, 0, removed) + return newColumns + }, }, ], saveAsDefault: [ From f487f423f2620a7ff06526265c906ba540299953 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 2 Dec 2022 17:21:49 +0100 Subject: [PATCH 23/90] more cleanup --- .../ColumnConfigurator/ColumnConfigurator.tsx | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 9e615595e42eb..3206295c87c9f 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -134,7 +134,7 @@ function ColumnConfiguratorModal(): JSX.Element { } const SortableColumnList = sortableContainer(() => ( -
+
{({ height, width }: { height: number; width: number }) => { return ( @@ -190,29 +190,20 @@ function ColumnConfiguratorModal(): JSX.Element {

Available columns

- {({ height, width }: { height: number; width: number }) => { - const trimmedProperties = columns.map((c) => - c.replace('person.', '').replace('properties.', '') - ) - return ( - value && selectColumn(`properties.${value}`)} - popoverEnabled={false} - selectFirstItem={false} - excludedProperties={{ - [TaxonomicFilterGroupType.EventProperties]: trimmedProperties, - [TaxonomicFilterGroupType.EventFeatureFlags]: trimmedProperties, - }} - /> - ) - }} + {({ height, width }: { height: number; width: number }) => ( + value && selectColumn(`properties.${value}`)} + popoverEnabled={false} + selectFirstItem={false} + /> + )}
From 448a6c5bfe1d44e5603ef094a86cf300744dd8d3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 00:30:26 +0100 Subject: [PATCH 24/90] export --- .../nodes/DataTable/DataTableExport.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 9aec2f19d1348..16dd74e4ae1aa 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -9,15 +9,21 @@ import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/default function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { const exportContext = { - path: api.events.determineListEndpoint({ - event: query.source.event, - properties: query.source.properties, - limit: query.source.limit, - }), + path: api.events.determineListEndpoint( + { + ...(query.source.event ? { event: query.source.event } : {}), + ...(query.source.properties ? { properties: query.source.properties } : {}), + }, + 3500 + ), max_limit: 3500, } + if (onlySelectedColumns) { - exportContext['columns'] = query.columns ?? defaultDataTableStringColumns + exportContext['columns'] = (query.columns ?? defaultDataTableStringColumns)?.map((c) => + // replace "person" with "distinct_id" for export + c === 'person' ? 'distinct_id' : c + ) } triggerExport({ export_format: ExporterFormat.CSV, @@ -30,7 +36,7 @@ interface DataTableExportProps { setQuery?: (node: DataTableNode) => void } -export function DataTableExport({ query }: DataTableExportProps): JSX.Element { +export function DataTableExport({ query }: DataTableExportProps): JSX.Element | null { return ( - + Export current columns , @@ -53,7 +59,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element { placement={'bottomRight'} onConfirm={() => startDownload(query, false)} > - + Export all columns , From 6ff3d474b89fd7ede41a99f42f13dd71670694f3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 00:46:15 +0100 Subject: [PATCH 25/90] show icon, store person properties --- .../components/PropertyFilterButton.tsx | 2 +- .../ColumnConfigurator/ColumnConfigurator.tsx | 46 +++++++++++++------ .../src/queries/nodes/DataTable/DataTable.tsx | 3 +- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx index c356c9fa05b48..beb7c8576e8ff 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx @@ -19,7 +19,7 @@ export interface PropertyFilterButtonProps { style?: React.CSSProperties } -function PropertyFilterIcon({ item }: { item: AnyPropertyFilter }): JSX.Element { +export function PropertyFilterIcon({ item }: { item: AnyPropertyFilter }): JSX.Element { let iconElement = <> switch (item?.type) { case 'event': diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 3206295c87c9f..906fb529a6ce7 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -1,5 +1,5 @@ import './ColumnConfigurator.scss' -import { useActions, useValues, BindLogic } from 'kea' +import { BindLogic, useActions, useValues } from 'kea' import { LemonButton } from 'lib/components/LemonButton' import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' import { IconTuning, SortableDragIcon } from 'lib/components/icons' @@ -24,6 +24,8 @@ import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnC import { defaultDataTableStringColumns } from '../defaults' import { DataTableNode } from '~/queries/schema' import { LemonModal } from 'lib/components/LemonModal' +import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterButton' +import { PropertyFilterType } from '~/types' let uniqueNode = 0 @@ -91,21 +93,31 @@ function ColumnConfiguratorModal(): JSX.Element { )) const SelectedColumn = ({ column, disabled }: { column: string; disabled?: boolean }): JSX.Element => { + let columnType: PropertyFilterType | null = null + let columnKey = column + if (column.startsWith('person.properties')) { + columnType = PropertyFilterType.Person + columnKey = column.replace('person.properties.', '') + } + if (column.startsWith('properties')) { + columnType = PropertyFilterType.Event + columnKey = column.replace('properties.', '') + } + return (
{!disabled && } - + {columnType && ( + + )} +
{disabled ? ( @@ -134,7 +146,7 @@ function ColumnConfiguratorModal(): JSX.Element { } const SortableColumnList = sortableContainer(() => ( -
+
{({ height, width }: { height: number; width: number }) => { return ( @@ -188,7 +200,7 @@ function ColumnConfiguratorModal(): JSX.Element {

Available columns

-
+
{({ height, width }: { height: number; width: number }) => ( value && selectColumn(`properties.${value}`)} + onChange={(group, value) => + value && + selectColumn( + group.type === TaxonomicFilterGroupType.PersonProperties + ? `person.properties.${value}` + : `properties.${value}` + ) + } popoverEnabled={false} selectFirstItem={false} /> diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 6feb3744206bc..8d10f2725d376 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -44,10 +44,9 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const { currentTeam } = useValues(teamLogic) const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns - console.log({ defaultColumns }) + const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}`, defaultColumns } const { columns } = useValues(dataTableLogic(dataTableLogicProps)) - console.log({ columns }) const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ From d34f4a4c10ab68adad712333134e916edf18d22d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 01:13:24 +0100 Subject: [PATCH 26/90] delete old code --- frontend/src/queries/nodes/dataNodeLogic.ts | 35 --------------------- 1 file changed, 35 deletions(-) delete mode 100644 frontend/src/queries/nodes/dataNodeLogic.ts diff --git a/frontend/src/queries/nodes/dataNodeLogic.ts b/frontend/src/queries/nodes/dataNodeLogic.ts deleted file mode 100644 index 1a9002c79d38c..0000000000000 --- a/frontend/src/queries/nodes/dataNodeLogic.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { kea, path, props, key, afterMount, selectors, propsChanged } from 'kea' -import { loaders } from 'kea-loaders' -import type { dataNodeLogicType } from './dataNodeLogicType' -import { DataNode } from '~/queries/schema' -import { query } from '~/queries/query' - -export interface DataNodeLogicProps { - key: string - query: DataNode -} - -export const dataNodeLogic = kea([ - path(['queries', 'nodes', 'dataNodeLogic']), - props({} as DataNodeLogicProps), - key((props) => props.key), - propsChanged(({ actions, props }, oldProps) => { - if (JSON.stringify(props.query) !== JSON.stringify(oldProps.query)) { - actions.loadData() - } - }), - selectors({ query: [() => [(_, props) => props.query], (query) => query] }), - loaders(({ values }) => ({ - response: [ - null as DataNode['response'] | null, - { - loadData: async () => { - return (await query(values.query)) ?? null - }, - }, - ], - })), - afterMount(({ actions }) => { - actions.loadData() - }), -]) From 7d160c3f04f8043a1f9fdaf779bc5299c3d55a1f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 01:18:36 +0100 Subject: [PATCH 27/90] add readme --- frontend/src/queries/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 frontend/src/queries/README.md diff --git a/frontend/src/queries/README.md b/frontend/src/queries/README.md new file mode 100644 index 0000000000000..0474c2743a41e --- /dev/null +++ b/frontend/src/queries/README.md @@ -0,0 +1,17 @@ +# Queries + +- `nodes/` + - The folders in this directory (e.g. `DataNode/`, `DataTable/`, ...) come from `NodeKind` + - The top level component always has the structure `DataTable({ query, setQuery })` + - There are various sub-components for each node kind (e.g. ``, ``) that can be used. Some of them depend on a logic, likely `dataNodeLogic`, being in a `BindLogic` context, so read the source. +- `Query/` + - Generic component that routes internally to the right node. + - `` +- `QueryEditor/` + - Generic JSON editor + - `` +- `examples.ts` - Various examples used in storybook +- `query.ts` - fetch data for any query +- `schema.json` - JSON schema, used for query editor +- `schema.ts` - typescript types for all query nodes +- `utils.ts` - type narrowing utilities From 32c628182ea216151eaaec75daa0ac572ce372fe Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 01:52:23 +0100 Subject: [PATCH 28/90] link properties --- .../src/queries/nodes/DataTable/DataTable.tsx | 2 +- .../queries/nodes/DataTable/renderColumn.tsx | 106 ++++++++++++++++-- frontend/src/queries/schema.ts | 2 + frontend/src/scenes/events/EventsScene.tsx | 1 + 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 8d10f2725d376..e43dd27a723ba 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -53,7 +53,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { dataIndex: key as any, title: renderTitle(key), render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(key, record) + return renderColumn(key, record, query, setQuery) }, })), ...(showActions diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 1c0cca443aec0..b8b1ad0ee6539 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -1,4 +1,4 @@ -import { EventType } from '~/types' +import { AnyPropertyFilter, EventType, PropertyFilterType, PropertyOperator } from '~/types' import { autoCaptureEventToDescription } from 'lib/utils' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { Link } from 'lib/components/Link' @@ -6,16 +6,24 @@ import { TZLabel } from 'lib/components/TZLabel' import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' +import { DataTableNode } from '~/queries/schema' +import { isEventsNode } from '~/queries/utils' +import { combineUrl, router } from 'kea-router' -export function renderColumn(key: string, record: EventType): JSX.Element | string { +export function renderColumn( + key: string, + record: EventType, + query: DataTableNode, + setQuery?: (node: DataTableNode) => void +): JSX.Element | string { if (key === 'event') { if (record.event === '$autocapture') { return autoCaptureEventToDescription(record) } else { const content = - const url = record.properties.$sentry_url - return url ? ( - + const { $sentry_url } = record.properties + return $sentry_url ? ( + {content} ) : ( @@ -25,9 +33,93 @@ export function renderColumn(key: string, record: EventType): JSX.Element | stri } else if (key === 'timestamp') { return } else if (key.startsWith('properties.')) { - return + const propertyKey = key.substring(11) + if (setQuery && isEventsNode(query.source)) { + const newProperty: AnyPropertyFilter = { + key: propertyKey, + value: record.properties[propertyKey], + operator: PropertyOperator.Exact, + type: PropertyFilterType.Event, + } + const matchingProperty = (query.source.properties || []).find( + (p) => p.key === newProperty.key && p.type === newProperty.type + ) + const newProperties = matchingProperty + ? (query.source.properties || []).filter((p) => p !== matchingProperty) + : [...(query.source.properties || []), newProperty] + const newUrl = query.urlProperties + ? combineUrl( + router.values.location.pathname, + { + ...router.values.searchParams, + properties: newProperties, + }, + router.values.hashParams + ).url + : '#' + return ( + { + e.preventDefault() + setQuery({ + ...query, + source: { + ...query.source, + properties: newProperties, + }, + }) + }} + > + + + ) + } + return } else if (key.startsWith('person.properties.')) { - return + const propertyKey = key.substring(18) + if (setQuery && isEventsNode(query.source)) { + const newProperty: AnyPropertyFilter = { + key: propertyKey, + value: record.person?.properties[propertyKey], + operator: PropertyOperator.Exact, + type: PropertyFilterType.Person, + } + const matchingProperty = (query.source.properties || []).find( + (p) => p.key === newProperty.key && p.type === newProperty.type + ) + const newProperties = matchingProperty + ? (query.source.properties || []).filter((p) => p !== matchingProperty) + : [...(query.source.properties || []), newProperty] + const newUrl = query.urlProperties + ? combineUrl( + router.values.location.pathname, + { + ...router.values.searchParams, + properties: newProperties, + }, + router.values.hashParams + ).url + : '#' + return ( + { + e.preventDefault() + setQuery({ + ...query, + source: { + ...query.source, + properties: newProperties, + }, + }) + }} + > + + + ) + } + return } else if (key === 'person') { return ( diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index d6fb75f9177ca..50c16dca14c63 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -116,6 +116,8 @@ export interface DataTableNode extends Node { showColumnConfigurator?: boolean /** Can expand row to show raw event data (default: true) */ expandable?: boolean + /** Link properties via the URL (default: false) */ + urlProperties?: boolean } // Base class should not be used directly diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index 88ddbd901ebc7..2422eb32c6139 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -18,6 +18,7 @@ export function EventsScene(): JSX.Element { event: eventFilter, limit: 100, }, + urlProperties: true, } return ( <> From 691f86ae20b6d2d5fc171d0a6731f292ba5024cb Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 02:19:00 +0100 Subject: [PATCH 29/90] make it look closer to the old page --- .../queries/nodes/DataTable/DataTable.scss | 3 +++ .../src/queries/nodes/DataTable/DataTable.tsx | 22 ++++++++++++++++--- .../nodes/DataTable/DataTableExport.tsx | 13 ++++++++--- .../queries/nodes/DataTable/renderColumn.tsx | 2 ++ 4 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/DataTable.scss diff --git a/frontend/src/queries/nodes/DataTable/DataTable.scss b/frontend/src/queries/nodes/DataTable/DataTable.scss new file mode 100644 index 0000000000000..b7b3e5842d8c7 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/DataTable.scss @@ -0,0 +1,3 @@ +.DataTable td { + max-width: 20rem; +} diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index e43dd27a723ba..db4c9bbcaf2d3 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,3 +1,4 @@ +import './DataTable.scss' import { DataTableNode, EventsNode } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' @@ -18,6 +19,7 @@ import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/d import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' import { teamLogic } from 'scenes/teamLogic' import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { LemonDivider } from 'lib/components/LemonDivider' interface DataTableProps { query: DataTableNode @@ -71,19 +73,32 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const dataSource = (response as null | EventsNode['response'])?.results ?? [] const setQuerySource = (source: EventsNode): void => setQuery?.({ ...query, source }) + const showFilters = showEventFilter || showPropertyFilter + const showTools = showReload || showExport || showColumnConfigurator + return ( - {(showReload || showEventFilter || showPropertyFilter || showExport || showColumnConfigurator) && ( + {showFilters && (
- {showReload && (canLoadNewData ? : )} {showEventFilter && } {showPropertyFilter && } - {showExport && } +
+ )} + {showFilters && showTools ? ( +
+ +
+ ) : null} + {showTools && ( +
+
{showReload && (canLoadNewData ? : )}
{showColumnConfigurator && } + {showExport && }
)} row.id ?? undefined} /> {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && }
diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 16dd74e4ae1aa..2f4eae29b9e8c 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -19,10 +19,17 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void max_limit: 3500, } + const columnMapping = { + url: ['properties.$current_url', 'properties.$screen_name'], + time: 'timestamp', + event: 'event', + source: 'properties.$lib', + person: ['person.distinct_ids.0', 'person.properties.email'], + } + if (onlySelectedColumns) { - exportContext['columns'] = (query.columns ?? defaultDataTableStringColumns)?.map((c) => - // replace "person" with "distinct_id" for export - c === 'person' ? 'distinct_id' : c + exportContext['columns'] = (query.columns ?? defaultDataTableStringColumns)?.flatMap( + (c) => columnMapping[c] || c ) } triggerExport({ diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index b8b1ad0ee6539..680ed11cb6504 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -59,6 +59,7 @@ export function renderColumn( : '#' return ( { e.preventDefault() @@ -103,6 +104,7 @@ export function renderColumn( : '#' return ( { e.preventDefault() From a3f54ce6950d8a13fcf0dbc8736428423b33dad4 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 02:27:22 +0100 Subject: [PATCH 30/90] url --- frontend/src/queries/nodes/DataTable/defaults.ts | 4 ++-- frontend/src/queries/nodes/DataTable/renderColumn.tsx | 5 +++-- frontend/src/queries/nodes/DataTable/renderTitle.tsx | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/defaults.ts b/frontend/src/queries/nodes/DataTable/defaults.ts index ccd8ba08a7bef..c90315ff7c2a3 100644 --- a/frontend/src/queries/nodes/DataTable/defaults.ts +++ b/frontend/src/queries/nodes/DataTable/defaults.ts @@ -3,7 +3,7 @@ import { DataTableStringColumn } from '~/queries/schema' export const defaultDataTableStringColumns: DataTableStringColumn[] = [ 'event', 'person', - 'properties.$current_url', - 'person.properties.email', + 'url', + 'properties.$lib', 'timestamp', ] diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 680ed11cb6504..825223409b4ae 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -32,8 +32,9 @@ export function renderColumn( } } else if (key === 'timestamp') { return - } else if (key.startsWith('properties.')) { - const propertyKey = key.substring(11) + } else if (key.startsWith('properties.') || key === 'url') { + const propertyKey = + key === 'url' ? (record.properties['$screen_name'] ? '$screen_name' : '$current_url') : key.substring(11) if (setQuery && isEventsNode(query.source)) { const newProperty: AnyPropertyFilter = { key: propertyKey, diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index eaca7be9665d7..c65d45c7ec264 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -8,6 +8,8 @@ export function renderTitle(key: string): JSX.Element | string { return 'Event' } else if (key === 'person') { return 'Person' + } else if (key === 'url') { + return 'URL / Screen' } else if (key.startsWith('properties.')) { return } else if (key.startsWith('person.properties.')) { From 8b2b61696744ae4b13bc82a69e8dffa8bc7b4129 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 02:32:30 +0100 Subject: [PATCH 31/90] show buffer warning --- frontend/src/queries/nodes/DataTable/DataTable.tsx | 8 ++++++++ frontend/src/queries/schema.ts | 2 ++ frontend/src/scenes/events/EventsScene.tsx | 1 + 3 files changed, 11 insertions(+) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index db4c9bbcaf2d3..e0c28f28d84c8 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -20,6 +20,7 @@ import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator import { teamLogic } from 'scenes/teamLogic' import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' import { LemonDivider } from 'lib/components/LemonDivider' +import { EventBufferNotice } from 'scenes/events/EventBufferNotice' interface DataTableProps { query: DataTableNode @@ -35,6 +36,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const showExport = query.showExport ?? true const showReload = query.showReload ?? true const showColumnConfigurator = query.showColumnConfigurator ?? true + const showEventsBufferWarning = query.showEventsBufferWarning ?? false const expandable = query.expandable ?? true const [id] = useState(() => uniqueNode++) @@ -97,6 +99,12 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {showExport && }
)} + {showEventsBufferWarning && ( + + )} From 24edb28c769f2af990a220b61d871781567cdc13 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 22:10:13 +0100 Subject: [PATCH 32/90] rename --- frontend/src/queries/nodes/DataTable/renderColumn.tsx | 4 ++-- frontend/src/queries/schema.ts | 2 +- frontend/src/scenes/events/EventsScene.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 825223409b4ae..dc6fec1fd6709 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -48,7 +48,7 @@ export function renderColumn( const newProperties = matchingProperty ? (query.source.properties || []).filter((p) => p !== matchingProperty) : [...(query.source.properties || []), newProperty] - const newUrl = query.urlProperties + const newUrl = query.propertiesViaUrl ? combineUrl( router.values.location.pathname, { @@ -93,7 +93,7 @@ export function renderColumn( const newProperties = matchingProperty ? (query.source.properties || []).filter((p) => p !== matchingProperty) : [...(query.source.properties || []), newProperty] - const newUrl = query.urlProperties + const newUrl = query.propertiesViaUrl ? combineUrl( router.values.location.pathname, { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 78b1cb69934a5..43736d575c958 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -117,7 +117,7 @@ export interface DataTableNode extends Node { /** Can expand row to show raw event data (default: true) */ expandable?: boolean /** Link properties via the URL (default: false) */ - urlProperties?: boolean + propertiesViaUrl?: boolean /** Show warning about live events being buffered max 60 sec (default: false) */ showEventsBufferWarning?: boolean } diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index 8774d815adbb6..bc40565b6c6e0 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -18,7 +18,7 @@ export function EventsScene(): JSX.Element { event: eventFilter, limit: 100, }, - urlProperties: true, + propertiesViaUrl: true, showEventsBufferWarning: true, } return ( From 27484ed89d5a50a6da5c16de97ac6a784d1b2006 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 22:30:24 +0100 Subject: [PATCH 33/90] autoload spinner, fix load order --- .../src/queries/nodes/DataNode/AutoLoad.tsx | 22 ++++++++----- .../src/queries/nodes/DataNode/LoadNext.tsx | 4 +-- .../queries/nodes/DataNode/dataNodeLogic.ts | 33 +++++++++---------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx index b897412a8f317..2a76ca4ffb6b4 100644 --- a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx +++ b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx @@ -2,9 +2,10 @@ import { useActions, useValues } from 'kea' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonSwitch } from 'lib/components/LemonSwitch/LemonSwitch' import { useEffect } from 'react' +import { Spinner } from 'lib/components/Spinner/Spinner' export function AutoLoad(): JSX.Element { - const { autoLoadEnabled } = useValues(dataNodeLogic) + const { autoLoadEnabled, newDataLoading } = useValues(dataNodeLogic) const { startAutoLoad, stopAutoLoad, toggleAutoLoad } = useActions(dataNodeLogic) useEffect(() => { @@ -13,13 +14,16 @@ export function AutoLoad(): JSX.Element { }, []) return ( - +
+ + {newDataLoading ? : null} +
) } diff --git a/frontend/src/queries/nodes/DataNode/LoadNext.tsx b/frontend/src/queries/nodes/DataNode/LoadNext.tsx index 8f08d67e79e46..5959fa1661ab2 100644 --- a/frontend/src/queries/nodes/DataNode/LoadNext.tsx +++ b/frontend/src/queries/nodes/DataNode/LoadNext.tsx @@ -3,11 +3,11 @@ import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonButton } from 'lib/components/LemonButton' export function LoadNext(): JSX.Element { - const { responseLoading } = useValues(dataNodeLogic) + const { nextDataLoading } = useValues(dataNodeLogic) const { loadNextData } = useActions(dataNodeLogic) return ( - + Load more events ) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 936a45b6288c5..1d81563907c4a 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -35,48 +35,47 @@ export const dataNodeLogic = kea([ return (await query(values.query)) ?? null }, loadNewData: async () => { - if (!values.canLoadNewData) { - return + if (!values.canLoadNewData || values.dataLoading) { + return values.response } - const oldResponse = values.response as EventsNode['response'] | null const diffQuery: EventsNode = - oldResponse && oldResponse.results?.length > 0 + values.response && values.response.results?.length > 0 ? { ...values.query, - after: oldResponse.results[0].timestamp, + after: values.response.results[0].timestamp, } : values.query const newResponse = (await query(diffQuery)) ?? null return { - results: [...(newResponse?.results ?? []), ...(oldResponse?.results ?? [])], - next: oldResponse?.next, + results: [...(newResponse?.results ?? []), ...(values.response?.results ?? [])], + next: values.response?.next, } }, loadNextData: async () => { - if (!values.canLoadNextData) { - return + if (!values.canLoadNextData || values.dataLoading) { + return values.response } - const oldResponse = values.response as EventsNode['response'] | null const diffQuery: EventsNode = - oldResponse && oldResponse.results?.length > 0 + values.response && values.response.results?.length > 0 ? { ...values.query, - before: oldResponse.results[oldResponse.results.length - 1].timestamp, + before: values.response.results[values.response.results.length - 1].timestamp, } : values.query const newResponse = (await query(diffQuery)) ?? null return { - results: [...(oldResponse?.results ?? []), ...(newResponse?.results ?? [])], - next: oldResponse?.next, + results: [...(values.response?.results ?? []), ...(newResponse?.results ?? [])], + next: values.response?.next, } }, }, ], })), reducers({ + dataLoading: [false, { loadData: () => true, loadDataSuccess: () => false, loadDataFailure: () => false }], newDataLoading: [ false, - { loadNewData: () => true, loadNewDataSuccess: () => false, loadDataFailure: () => false }, + { loadNewData: () => true, loadNewDataSuccess: () => false, loadNewDataFailure: () => false }, ], nextDataLoading: [ false, @@ -104,8 +103,8 @@ export const dataNodeLogic = kea([ }, ], autoLoadRunning: [ - (s) => [s.autoLoadEnabled, s.autoLoadStarted], - (autoLoadEnabled, autoLoadStarted) => autoLoadEnabled && autoLoadStarted, + (s) => [s.autoLoadEnabled, s.autoLoadStarted, s.dataLoading], + (autoLoadEnabled, autoLoadStarted, dataLoading) => autoLoadEnabled && autoLoadStarted && !dataLoading, ], }), subscriptions(({ actions, cache, values }) => ({ From 79250ab070caaed881b0b3b7ff5fbe857a68ac17 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 22:56:12 +0100 Subject: [PATCH 34/90] add automatic row highlighting --- .../queries/nodes/DataNode/dataNodeLogic.ts | 17 ++++++++++++++++- .../queries/nodes/DataTable/DataTable.scss | 19 +++++++++++++++++-- .../src/queries/nodes/DataTable/DataTable.tsx | 16 +++++++++++++--- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 1d81563907c4a..dac2c084c4fbc 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -26,8 +26,9 @@ export const dataNodeLogic = kea([ startAutoLoad: true, stopAutoLoad: true, toggleAutoLoad: true, + highlightRows: (rowIds: string[], now = new Date().valueOf()) => ({ rowIds, now }), }), - loaders(({ values }) => ({ + loaders(({ actions, values }) => ({ response: [ null as DataNode['response'] | null, { @@ -46,6 +47,7 @@ export const dataNodeLogic = kea([ } : values.query const newResponse = (await query(diffQuery)) ?? null + actions.highlightRows((newResponse?.results ?? []).map((r) => r.id)) return { results: [...(newResponse?.results ?? []), ...(values.response?.results ?? [])], next: values.response?.next, @@ -83,6 +85,19 @@ export const dataNodeLogic = kea([ ], autoLoadEnabled: [false, { toggleAutoLoad: (state) => !state }], autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], + highlightedRows: [ + {} as Record, + { + highlightRows: (state, { rowIds, now }) => { + const newState = { ...state } + for (const rowId of rowIds) { + newState[rowId] = now + } + return newState + }, + loadDataSuccess: () => ({}), + }, + ], }), selectors({ query: [() => [(_, props) => props.query], (query) => query], diff --git a/frontend/src/queries/nodes/DataTable/DataTable.scss b/frontend/src/queries/nodes/DataTable/DataTable.scss index b7b3e5842d8c7..c0745196d4e2a 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.scss +++ b/frontend/src/queries/nodes/DataTable/DataTable.scss @@ -1,3 +1,18 @@ -.DataTable td { - max-width: 20rem; +.DataTable { + td { + max-width: 20rem; + } + + @keyframes DataTable--highlight { + 0% { + background-color: var(--yellow-lightest); + } + 100% { + background-color: initial; + } + } + + .DataTable__row--highlight_once { + animation: DataTable--highlight 2000ms ease-out; + } } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index e0c28f28d84c8..6c25bfb32df16 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -21,6 +21,7 @@ import { teamLogic } from 'scenes/teamLogic' import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' +import clsx from 'clsx' interface DataTableProps { query: DataTableNode @@ -42,9 +43,15 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const [id] = useState(() => uniqueNode++) const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } - const { response, responseLoading, canLoadNextData, canLoadNewData, nextDataLoading, newDataLoading } = useValues( - dataNodeLogic(dataNodeLogicProps) - ) + const { + response, + responseLoading, + canLoadNextData, + canLoadNewData, + nextDataLoading, + newDataLoading, + highlightedRows, + } = useValues(dataNodeLogic(dataNodeLogicProps)) const { currentTeam } = useValues(teamLogic) const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns @@ -122,6 +129,9 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { : undefined } rowKey={(row) => row.id ?? undefined} + rowClassName={(row) => + clsx('DataTable__row', { 'DataTable__row--highlight_once': highlightedRows[row?.id] }) + } /> {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } From 6c552fe2ca5f16a6ca20bebf1d21f4e2142a8029 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 3 Dec 2022 23:54:57 +0100 Subject: [PATCH 35/90] scope down --- .../PropertyFilters/components/PropertyFilterButton.tsx | 8 ++++---- .../DataTable/ColumnConfigurator/ColumnConfigurator.tsx | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx index beb7c8576e8ff..11202edf44c1b 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx @@ -1,6 +1,6 @@ import './PropertyFilterButton.scss' import { Button } from 'antd' -import { AnyPropertyFilter } from '~/types' +import { AnyPropertyFilter, PropertyFilterType } from '~/types' import { CloseButton } from 'lib/components/CloseButton' import { IconCohort, IconPerson, UnverifiedEvent } from 'lib/components/icons' import { Tooltip } from 'lib/components/Tooltip' @@ -19,9 +19,9 @@ export interface PropertyFilterButtonProps { style?: React.CSSProperties } -export function PropertyFilterIcon({ item }: { item: AnyPropertyFilter }): JSX.Element { +export function PropertyFilterIcon({ type }: { type?: PropertyFilterType }): JSX.Element { let iconElement = <> - switch (item?.type) { + switch (type) { case 'event': iconElement = ( @@ -69,7 +69,7 @@ export const PropertyFilterButton = React.forwardRef - + {midEllipsis(label, 32)} diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 906fb529a6ce7..02f4a45ab0df7 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -110,13 +110,7 @@ function ColumnConfiguratorModal(): JSX.Element { style={{ height: `${rowItemHeight}px` }} > {!disabled && } - {columnType && ( - - )} + {columnType && }
From bdf36e2fab02d27dace13e814e52a0ca0a5aa129 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 00:44:17 +0100 Subject: [PATCH 36/90] comment --- frontend/src/lib/components/ResizableTable/TableConfig.tsx | 2 +- frontend/src/queries/nodes/DataNode/AutoLoad.tsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/ResizableTable/TableConfig.tsx b/frontend/src/lib/components/ResizableTable/TableConfig.tsx index 4a84860ba070e..0266f460b2941 100644 --- a/frontend/src/lib/components/ResizableTable/TableConfig.tsx +++ b/frontend/src/lib/components/ResizableTable/TableConfig.tsx @@ -94,7 +94,7 @@ function ColumnConfigurator({ immutableColumns, defaultColumns }: TableConfigPro value={ column.startsWith('properties.') ? column.substring(11) - : column.startsWith('person.properties') + : column.startsWith('person.properties.') ? column.substring(18) : column } diff --git a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx index 2a76ca4ffb6b4..6ac9606d7ba49 100644 --- a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx +++ b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx @@ -8,6 +8,9 @@ export function AutoLoad(): JSX.Element { const { autoLoadEnabled, newDataLoading } = useValues(dataNodeLogic) const { startAutoLoad, stopAutoLoad, toggleAutoLoad } = useActions(dataNodeLogic) + // Reload data only when this AutoLoad component is mounted. + // This avoids needless reloading in the background, as logics might be kept around, + // even if not visually present. useEffect(() => { startAutoLoad() return () => stopAutoLoad() From 66e5221e188299e2e5c16dbab52a4f7f95ad1d5b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 01:04:58 +0100 Subject: [PATCH 37/90] more comments --- .../DataTable/ColumnConfigurator/ColumnConfigurator.tsx | 8 ++++---- frontend/src/queries/nodes/DataTable/renderTitle.tsx | 1 + frontend/src/scenes/events/Events.tsx | 6 ++++-- frontend/src/scenes/events/EventsTable.tsx | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 02f4a45ab0df7..a5f5562bbfbf1 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -95,13 +95,13 @@ function ColumnConfiguratorModal(): JSX.Element { const SelectedColumn = ({ column, disabled }: { column: string; disabled?: boolean }): JSX.Element => { let columnType: PropertyFilterType | null = null let columnKey = column - if (column.startsWith('person.properties')) { + if (column.startsWith('person.properties.')) { columnType = PropertyFilterType.Person - columnKey = column.replace('person.properties.', '') + columnKey = column.substring(18) } - if (column.startsWith('properties')) { + if (column.startsWith('properties.')) { columnType = PropertyFilterType.Event - columnKey = column.replace('properties.', '') + columnKey = column.substring(11) } return ( diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index c65d45c7ec264..0dc573140ce98 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -13,6 +13,7 @@ export function renderTitle(key: string): JSX.Element | string { } else if (key.startsWith('properties.')) { return } else if (key.startsWith('person.properties.')) { + // NOTE: type=Event is not a mistake, even if it's a person property. Don't ask, won't fix. return } else { return String(key) diff --git a/frontend/src/scenes/events/Events.tsx b/frontend/src/scenes/events/Events.tsx index 535f84cc19ef5..ea2939ec8d6f7 100644 --- a/frontend/src/scenes/events/Events.tsx +++ b/frontend/src/scenes/events/Events.tsx @@ -8,18 +8,20 @@ import { EventsScene } from 'scenes/events/EventsScene' export const scene: SceneExport = { component: Events, + // NOTE: Removing the lines below because turbo mode messes up having two separate versions of this scene. + // It's a small price to pay. Put this back when the flag is removed. // logic: eventsTableLogic, // paramsToProps: ({ params: { fixedFilters } }) => ({ fixedFilters, key: 'EventsTable', sceneUrl: urls.events() }), } export function Events(): JSX.Element { const { featureFlags } = useValues(featureFlagLogic) - const useDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] return ( <>
- {useDataExploration ? : } + {featureDataExploration ? : } ) } diff --git a/frontend/src/scenes/events/EventsTable.tsx b/frontend/src/scenes/events/EventsTable.tsx index ea570764614aa..f7f0cc866a5f2 100644 --- a/frontend/src/scenes/events/EventsTable.tsx +++ b/frontend/src/scenes/events/EventsTable.tsx @@ -282,7 +282,7 @@ export function EventsTable({ let columnsToBeMapped = !showPersonColumn ? selectedColumns.filter((column) => column !== 'person') : selectedColumns - // If user has saved `timestamp`, a column only used in the Data Exploration version of this feature, remove it + // If user has saved `timestamp`, a column only used in the Data Exploration flagged version of this feature, remove it columnsToBeMapped = columnsToBeMapped.filter((c) => c !== 'timestamp') columnsSoFar = columnsToBeMapped.map( (e, index): LemonTableColumn => { @@ -307,7 +307,7 @@ export function EventsTable({ } } else { // If the user has saved their columns for the new data exploration data table, make them work here - // This code will be removed once we release the new events list feature. + // This entire file will be removed once we release the new events list feature. const key = e.startsWith('properties.') ? e.substring(11) : e.startsWith('person.properties.') From 95d63b3981c5e65e8467b84ee3e440b2a5902dbb Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 01:24:03 +0100 Subject: [PATCH 38/90] add link to session player --- .../src/queries/nodes/DataTable/DataTable.tsx | 2 ++ .../nodes/DataTable/EventRowActions.tsx | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 6c25bfb32df16..a18a455a275ca 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -22,6 +22,7 @@ import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/default import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' +import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' interface DataTableProps { query: DataTableNode @@ -134,6 +135,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { } /> {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } + ) diff --git a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx index 01bcec59b2f99..258abbf2c53ff 100644 --- a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx +++ b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx @@ -5,12 +5,17 @@ import { createActionFromEvent } from 'scenes/events/createActionFromEvent' import { urls } from 'scenes/urls' import { getCurrentTeamId } from 'lib/utils/logics' import { teamLogic } from 'scenes/teamLogic' +import { IconPlayCircle } from 'lib/components/icons' +import { useActions } from 'kea' +import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' interface EventActionProps { event: EventType } export function EventRowActions({ event }: EventActionProps): JSX.Element { + const { openSessionPlayer } = useActions(sessionPlayerModalLogic) + let insightParams: Partial | undefined if (event.event === '$pageview') { insightParams = { @@ -73,7 +78,26 @@ export function EventRowActions({ event }: EventActionProps): JSX.Element { Create action from event )} - {/* TODO: add link to session recording modal */} + {!!event.properties.$session_id && ( + { + e.preventDefault() + if (event.properties.$session_id) { + openSessionPlayer({ + id: event.properties.$session_id, + }) + } + }} + fullWidth + sideIcon={} + data-attr="events-table-usage" + > + View recording + + )} {insightParams && ( Date: Sun, 4 Dec 2022 01:57:26 +0100 Subject: [PATCH 39/90] move defaults to logic --- .../src/queries/nodes/DataTable/DataTable.tsx | 22 ++++++++++--------- .../queries/nodes/DataTable/dataTableLogic.ts | 20 ++++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index a18a455a275ca..1fae5ae61a883 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -32,15 +32,6 @@ interface DataTableProps { let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { - const showPropertyFilter = query.showPropertyFilter ?? true - const showEventFilter = query.showEventFilter ?? true - const showActions = query.showActions ?? true - const showExport = query.showExport ?? true - const showReload = query.showReload ?? true - const showColumnConfigurator = query.showColumnConfigurator ?? true - const showEventsBufferWarning = query.showEventsBufferWarning ?? false - const expandable = query.expandable ?? true - const [id] = useState(() => uniqueNode++) const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } @@ -58,7 +49,18 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}`, defaultColumns } - const { columns } = useValues(dataTableLogic(dataTableLogicProps)) + const { columns, queryWithDefaults } = useValues(dataTableLogic(dataTableLogicProps)) + + const { + showActions, + showEventFilter, + showPropertyFilter, + showReload, + showExport, + showColumnConfigurator, + showEventsBufferWarning, + expandable, + } = queryWithDefaults const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 7d8f58388cba2..f05d82c76ab92 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -1,4 +1,4 @@ -import { actions, kea, key, path, props, propsChanged, reducers } from 'kea' +import { actions, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' import type { dataTableLogicType } from './dataTableLogicType' import { DataTableNode, DataTableStringColumn } from '~/queries/schema' import { defaultDataTableStringColumns } from './defaults' @@ -20,6 +20,24 @@ export const dataTableLogic = kea([ { setColumns: (_, { columns }) => columns }, ], })), + selectors({ + queryWithDefaults: [ + (s) => [(_, props) => props.query, s.columns], + (query: DataTableNode, columns): Required => ({ + ...query, + columns: columns, + showPropertyFilter: query.showPropertyFilter ?? true, + showEventFilter: query.showEventFilter ?? true, + showActions: query.showActions ?? true, + showExport: query.showExport ?? true, + showReload: query.showReload ?? true, + showColumnConfigurator: query.showColumnConfigurator ?? true, + showEventsBufferWarning: query.showEventsBufferWarning ?? false, + expandable: query.expandable ?? true, + propertiesViaUrl: query.propertiesViaUrl ?? false, + }), + ], + }), propsChanged(({ actions, props }, oldProps) => { const newColumns = props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns const oldColumns = oldProps.query.columns ?? oldProps.defaultColumns ?? defaultDataTableStringColumns From b817b2c461f20920deab149eee4443e4f30604c6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 01:57:37 +0100 Subject: [PATCH 40/90] enable autoload --- frontend/src/queries/nodes/DataNode/AutoLoad.tsx | 4 ++-- frontend/src/queries/nodes/DataNode/dataNodeLogic.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx index 6ac9606d7ba49..39b14c1cf18c5 100644 --- a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx +++ b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx @@ -5,7 +5,7 @@ import { useEffect } from 'react' import { Spinner } from 'lib/components/Spinner/Spinner' export function AutoLoad(): JSX.Element { - const { autoLoadEnabled, newDataLoading } = useValues(dataNodeLogic) + const { autoLoadToggled, newDataLoading } = useValues(dataNodeLogic) const { startAutoLoad, stopAutoLoad, toggleAutoLoad } = useActions(dataNodeLogic) // Reload data only when this AutoLoad component is mounted. @@ -23,7 +23,7 @@ export function AutoLoad(): JSX.Element { data-attr="live-events-refresh-toggle" id="autoload-switch" label="Automatically load new events" - checked={autoLoadEnabled} + checked={autoLoadToggled} onChange={toggleAutoLoad} /> {newDataLoading ? : null} diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index dac2c084c4fbc..0c0a3edc2aa2c 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -83,7 +83,7 @@ export const dataNodeLogic = kea([ false, { loadNextData: () => true, loadNextDataSuccess: () => false, loadNextDataFailure: () => false }, ], - autoLoadEnabled: [false, { toggleAutoLoad: (state) => !state }], + autoLoadToggled: [false, { toggleAutoLoad: (state) => !state }], autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], highlightedRows: [ {} as Record, @@ -118,8 +118,8 @@ export const dataNodeLogic = kea([ }, ], autoLoadRunning: [ - (s) => [s.autoLoadEnabled, s.autoLoadStarted, s.dataLoading], - (autoLoadEnabled, autoLoadStarted, dataLoading) => autoLoadEnabled && autoLoadStarted && !dataLoading, + (s) => [s.autoLoadToggled, s.autoLoadStarted, s.dataLoading], + (autoLoadToggled, autoLoadStarted, dataLoading) => autoLoadToggled && autoLoadStarted && !dataLoading, ], }), subscriptions(({ actions, cache, values }) => ({ From 6dc307907b4853811ad1478996b5544d9aba2a08 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 11:50:21 +0100 Subject: [PATCH 41/90] sanitize old columns --- .../queries/nodes/DataTable/dataTableLogic.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index f05d82c76ab92..a9e392d4ecad0 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -9,18 +9,33 @@ export interface DataTableLogicProps { defaultColumns?: DataTableStringColumn[] } +const topLevelColumns = ['event', 'timestamp', 'id', 'distinct_id'] + export const dataTableLogic = kea([ props({} as DataTableLogicProps), key((props) => props.key), path(['queries', 'nodes', 'DataTable', 'dataTableLogic']), actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), reducers(({ props }) => ({ - columns: [ + storedColumns: [ (props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns) as DataTableStringColumn[], { setColumns: (_, { columns }) => columns }, ], })), selectors({ + columns: [ + (s) => [s.storedColumns], + (storedColumns) => { + // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' + return storedColumns.map((column) => { + if (topLevelColumns.includes(column) || column.includes('properties.')) { + return column + } else { + return `properties.${column}` + } + }) + }, + ], queryWithDefaults: [ (s) => [(_, props) => props.query, s.columns], (query: DataTableNode, columns): Required => ({ From 67c0202b0bcd30613718292580f5886de5adb9a5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 12:29:29 +0100 Subject: [PATCH 42/90] cleanup --- frontend/src/queries/nodes/DataTable/dataTableLogic.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index a9e392d4ecad0..f1f55869ace63 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -9,8 +9,6 @@ export interface DataTableLogicProps { defaultColumns?: DataTableStringColumn[] } -const topLevelColumns = ['event', 'timestamp', 'id', 'distinct_id'] - export const dataTableLogic = kea([ props({} as DataTableLogicProps), key((props) => props.key), @@ -27,8 +25,9 @@ export const dataTableLogic = kea([ (s) => [s.storedColumns], (storedColumns) => { // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' + const topLevelFields = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] return storedColumns.map((column) => { - if (topLevelColumns.includes(column) || column.includes('properties.')) { + if (topLevelFields.includes(column) || column.includes('properties.')) { return column } else { return `properties.${column}` From dd048e992f6a46f9684020fd559ae18ad66e9daa Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 21:59:14 +0100 Subject: [PATCH 43/90] refactor --- .../src/queries/nodes/DataTable/DataTable.tsx | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 1fae5ae61a883..8c40ea81294c1 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -91,53 +91,50 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { return ( - {showFilters && ( -
- {showEventFilter && } - {showPropertyFilter && } -
- )} - {showFilters && showTools ? ( -
- -
- ) : null} - {showTools && ( -
-
{showReload && (canLoadNewData ? : )}
- {showColumnConfigurator && } - {showExport && } -
- )} - {showEventsBufferWarning && ( - + {showFilters && ( +
+ {showEventFilter && } + {showPropertyFilter && ( + + )} +
+ )} + {showFilters && showTools && } + {showTools && ( +
+
{showReload && (canLoadNewData ? : )}
+ {showColumnConfigurator && } + {showExport && } +
+ )} + {showEventsBufferWarning && ( + + )} + + }, + rowExpandable: () => true, + noIndent: true, + } + : undefined + } + rowKey={(row) => row.id ?? undefined} + rowClassName={(row) => + clsx('DataTable__row', { 'DataTable__row--highlight_once': highlightedRows[row?.id] }) + } /> - )} - - }, - rowExpandable: () => true, - noIndent: true, - } - : undefined - } - rowKey={(row) => row.id ?? undefined} - rowClassName={(row) => - clsx('DataTable__row', { 'DataTable__row--highlight_once': highlightedRows[row?.id] }) - } - /> - {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } - + {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } + +
) From b0c11288eb69fd62c1c818debe89d82b5ff06741 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 22:11:54 +0100 Subject: [PATCH 44/90] key cleanup --- frontend/src/queries/nodes/DataNode/DataNode.tsx | 4 ++-- frontend/src/queries/nodes/DataTable/DataTable.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index 4f703fc268c28..ac5cb3cc1d8ea 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -10,11 +10,11 @@ interface DataNodeProps { query: DataNodeType } -let i = 0 +let uniqueNode = 0 /** Default renderer for data nodes. Display the JSON in a Monaco editor. */ export function DataNode(props: DataNodeProps): JSX.Element { - const [key] = useState(() => String(i++)) + const [key] = useState(() => `DataNode.${uniqueNode++}`) const logic = dataNodeLogic({ ...props, key }) const { response, responseLoading } = useValues(logic) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 8c40ea81294c1..a5889a1719266 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -32,9 +32,9 @@ interface DataTableProps { let uniqueNode = 0 export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { - const [id] = useState(() => uniqueNode++) + const [key] = useState(() => `DataTable.${uniqueNode++}`) - const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key: `DataTable.${id}` } + const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key } const { response, responseLoading, @@ -48,7 +48,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const { currentTeam } = useValues(teamLogic) const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns - const dataTableLogicProps: DataTableLogicProps = { query: query, key: `DataTable.${id}`, defaultColumns } + const dataTableLogicProps: DataTableLogicProps = { query, key, defaultColumns } const { columns, queryWithDefaults } = useValues(dataTableLogic(dataTableLogicProps)) const { From 91a93274396a772de0a79b471ef8bc26f63a3cb5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 22:34:03 +0100 Subject: [PATCH 45/90] kea localstorage with global key --- frontend/src/queries/nodes/DataNode/dataNodeLogic.ts | 11 ++++++++--- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 0c0a3edc2aa2c..2baa845e96bb7 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -73,7 +73,7 @@ export const dataNodeLogic = kea([ }, ], })), - reducers({ + reducers(({ props }) => ({ dataLoading: [false, { loadData: () => true, loadDataSuccess: () => false, loadDataFailure: () => false }], newDataLoading: [ false, @@ -83,7 +83,12 @@ export const dataNodeLogic = kea([ false, { loadNextData: () => true, loadNextDataSuccess: () => false, loadNextDataFailure: () => false }, ], - autoLoadToggled: [false, { toggleAutoLoad: (state) => !state }], + autoLoadToggled: [ + false, + // store the autoload toggle's state in localstorage, separately for each data node kind + { persist: true, storageKey: `queries.nodes.dataNodeLogic..autoLoadToggled.${props.query.kind}` }, + { toggleAutoLoad: (state) => !state }, + ], autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], highlightedRows: [ {} as Record, @@ -98,7 +103,7 @@ export const dataNodeLogic = kea([ loadDataSuccess: () => ({}), }, ], - }), + })), selectors({ query: [() => [(_, props) => props.query], (query) => query], canLoadNewData: [ diff --git a/package.json b/package.json index 6be64f30330d0..3eb213ce78f7a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "kea": "^3.0.4", "kea-forms": "^3.0.3", "kea-loaders": "^3.0.0", - "kea-localstorage": "^3.0.0", + "kea-localstorage": "^3.1.0", "kea-router": "^3.1.3", "kea-subscriptions": "^3.0.0", "kea-waitfor": "^0.2.1", diff --git a/yarn.lock b/yarn.lock index 7b8f344065334..c29bb08c56856 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13319,10 +13319,10 @@ kea-loaders@^3.0.0: resolved "https://registry.yarnpkg.com/kea-loaders/-/kea-loaders-3.0.0.tgz#bf6e5b853ac035bf0055f6379a750a7afdb1b3ad" integrity sha512-YQWXMthnfsxRGSI+pXehgfPWE/hjck9mD2oGlQv86wE6MWo/gGn1lT/gAyF80s9YHOLMOL43Nei5py3K6vwBbg== -kea-localstorage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/kea-localstorage/-/kea-localstorage-3.0.0.tgz#eae89f0ff4f90006efecee02ac2ef19224193091" - integrity sha512-5g/7Sb5HjXLVTTYB9aUgizISUmnQZHqFm7ab+K3sLLh6gp3UEaKJMs5yq2rFPAfr7QR8OpMkfhKA0Ykg5UaOMQ== +kea-localstorage@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/kea-localstorage/-/kea-localstorage-3.1.0.tgz#bd25e60d26ab5920c3958b2c040164e86510bc09" + integrity sha512-OFByJwu88Ro9C9A1bqY5n8v1JYl6ArXX1zC75qpN8HorrReoM9Kf0gZdVspyExsRuX5en9ga64vsk54+jW4Kvw== kea-router@^3.1.3: version "3.1.3" From 4fc3083c8ddc9e59c9182bec9a3bdc781f908d13 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 23:25:44 +0100 Subject: [PATCH 46/90] query editor new look --- frontend/src/queries/Query/Query.stories.tsx | 8 +++-- .../src/queries/QueryEditor/QueryEditor.tsx | 35 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/frontend/src/queries/Query/Query.stories.tsx b/frontend/src/queries/Query/Query.stories.tsx index e825848f7c563..e5bf828fb01b4 100644 --- a/frontend/src/queries/Query/Query.stories.tsx +++ b/frontend/src/queries/Query/Query.stories.tsx @@ -22,10 +22,12 @@ const BasicTemplate: ComponentStory = (props: QueryProps) => { const [queryString, setQueryString] = useState(JSON.stringify(props.query)) return ( -
+ <> - -
+
+ +
+ ) } diff --git a/frontend/src/queries/QueryEditor/QueryEditor.tsx b/frontend/src/queries/QueryEditor/QueryEditor.tsx index 4083411b144c9..a82cca3a85339 100644 --- a/frontend/src/queries/QueryEditor/QueryEditor.tsx +++ b/frontend/src/queries/QueryEditor/QueryEditor.tsx @@ -8,6 +8,7 @@ import { queryEditorLogic } from '~/queries/QueryEditor/queryEditorLogic' export interface QueryEditorProps { query: string setQuery?: (query: string) => void + height?: number } let i = 0 @@ -34,30 +35,30 @@ export function QueryEditor(props: QueryEditorProps): JSX.Element { }, [monaco]) return ( -
+
setQueryInput(v ?? '')} - height={300} + height={props.height ?? 300} /> -
- saveQuery()} - type="primary" - status={error ? 'danger' : 'primary'} - disabled={!props.setQuery || !!error || !inputChanged} - > - Update - - {error ? ( -
- Error parsing JSON: {error} -
- ) : null} -
+ {error ? ( +
+ Error parsing JSON: {error} +
+ ) : null} + saveQuery()} + type="primary" + status={error ? 'danger' : 'muted-alt'} + disabled={!props.setQuery || !!error || !inputChanged} + fullWidth + center + > + Update +
) } From feb6273d8c27b30d7c9bd8e14c9fa1af428bd451 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 4 Dec 2022 23:26:05 +0100 Subject: [PATCH 47/90] empty datatable stories --- .../nodes/DataTable/DataTable.examples.ts | 9 +++++ .../nodes/DataTable/DataTable.stories.tsx | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 frontend/src/queries/nodes/DataTable/DataTable.examples.ts create mode 100644 frontend/src/queries/nodes/DataTable/DataTable.stories.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts new file mode 100644 index 0000000000000..ffe462055302e --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -0,0 +1,9 @@ +import { DataTableNode, NodeKind } from '~/queries/schema' + +const NoColumns: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + columns: undefined, +} + +export const examples = { NoColumns } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx new file mode 100644 index 0000000000000..94795b5cab0b1 --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -0,0 +1,35 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react' +import { Query, QueryProps } from '~/queries/Query/Query' +import { useState } from 'react' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { examples } from './DataTable.examples' + +export default { + title: 'Queries/DataTable', + component: Query, + parameters: { + chromatic: { disableSnapshot: false }, + layout: 'fullscreen', + options: { showPanel: false }, + viewMode: 'story', + }, + argTypes: { + query: { defaultValue: {} }, + }, +} as ComponentMeta + +const BasicTemplate: ComponentStory = (props: QueryProps) => { + const [queryString, setQueryString] = useState(JSON.stringify(props.query)) + + return ( + <> + +
+ +
+ + ) +} + +export const NoColumns = BasicTemplate.bind({}) +NoColumns.args = { query: examples['NoColumns'] } From a5bb236009e98e116e687617e5a4f00346745966 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 00:31:58 +0100 Subject: [PATCH 48/90] query edit modal --- .../src/queries/QueryEditor/QueryEditor.tsx | 30 +++++++++++------- .../src/queries/nodes/DataTable/DataTable.tsx | 4 ++- .../queries/nodes/Node/QueryEditorModal.tsx | 31 +++++++++++++++++++ frontend/src/styles/vars.scss | 2 +- 4 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 frontend/src/queries/nodes/Node/QueryEditorModal.tsx diff --git a/frontend/src/queries/QueryEditor/QueryEditor.tsx b/frontend/src/queries/QueryEditor/QueryEditor.tsx index a82cca3a85339..4447e96bdb2ef 100644 --- a/frontend/src/queries/QueryEditor/QueryEditor.tsx +++ b/frontend/src/queries/QueryEditor/QueryEditor.tsx @@ -4,11 +4,13 @@ import { useEffect, useState } from 'react' import schema from '~/queries/schema.json' import { LemonButton } from 'lib/components/LemonButton' import { queryEditorLogic } from '~/queries/QueryEditor/queryEditorLogic' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import clsx from 'clsx' export interface QueryEditorProps { query: string setQuery?: (query: string) => void - height?: number + className?: string } let i = 0 @@ -35,15 +37,21 @@ export function QueryEditor(props: QueryEditorProps): JSX.Element { }, [monaco]) return ( -
- setQueryInput(v ?? '')} - height={props.height ?? 300} - /> +
+
+ + {({ height }) => ( + setQueryInput(v ?? '')} + height={height} + /> + )} + +
{error ? (
Error parsing JSON: {error} @@ -57,7 +65,7 @@ export function QueryEditor(props: QueryEditorProps): JSX.Element { fullWidth center > - Update + {!props.setQuery ? 'No permission to update' : 'Update'}
) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index a5889a1719266..d7cbf52e56608 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode } from '~/queries/schema' +import { DataTableNode, EventsNode, Node } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -23,6 +23,7 @@ import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' +import { QueryEditorModal } from '~/queries/nodes/Node/QueryEditorModal' interface DataTableProps { query: DataTableNode @@ -106,6 +107,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element {
{showReload && (canLoadNewData ? : )}
{showColumnConfigurator && } {showExport && } + void} />
)} {showEventsBufferWarning && ( diff --git a/frontend/src/queries/nodes/Node/QueryEditorModal.tsx b/frontend/src/queries/nodes/Node/QueryEditorModal.tsx new file mode 100644 index 0000000000000..f8bd0fd26445d --- /dev/null +++ b/frontend/src/queries/nodes/Node/QueryEditorModal.tsx @@ -0,0 +1,31 @@ +import { LemonModal } from 'lib/components/LemonModal' +import { useState } from 'react' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { IconEvent } from 'lib/components/icons' +import { LemonButton } from 'lib/components/LemonButton' +import { Node } from '~/queries/schema' + +export interface QueryEditorModalProps { + query: Node + setQuery?: (query: Node) => void +} + +export function QueryEditorModal({ query, setQuery }: QueryEditorModalProps): JSX.Element { + const [open, setOpen] = useState(false) + + return ( + <> + setOpen(true)}> + + + setOpen(false)} simple title={''} width={880}> + + setQuery(JSON.parse(query)) : undefined} + /> + + + + ) +} diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss index 554fddb56e448..1ac81093ef113 100644 --- a/frontend/src/styles/vars.scss +++ b/frontend/src/styles/vars.scss @@ -12,7 +12,7 @@ $screens: ( 'xxl': $xxl, ); -$spaces: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 100, 120; +$spaces: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 40, 60, 80, 100, 120; $leadings: 3, 4, 5, 6, 7, 8, 9, 10; $sides: 'top', 'right', 'bottom', 'left'; // CSS cursors from https://tailwindcss.com/docs/cursor From befb97e4aa9b5e4153b12ab8f07c37eb2ba6d3fd Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 01:03:03 +0100 Subject: [PATCH 49/90] inline editor --- .../src/queries/nodes/DataTable/DataTable.tsx | 20 ++++- .../queries/nodes/DataTable/dataTableLogic.ts | 10 +-- .../src/queries/nodes/Node/InlineEditor.tsx | 39 ++++++++++ .../queries/nodes/Node/QueryEditorModal.tsx | 31 -------- frontend/src/queries/schema.json | 78 +++++++------------ frontend/src/scenes/events/EventsScene.tsx | 37 +++++---- 6 files changed, 111 insertions(+), 104 deletions(-) create mode 100644 frontend/src/queries/nodes/Node/InlineEditor.tsx delete mode 100644 frontend/src/queries/nodes/Node/QueryEditorModal.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index d7cbf52e56608..afb984d2013b6 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -23,7 +23,7 @@ import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' -import { QueryEditorModal } from '~/queries/nodes/Node/QueryEditorModal' +import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' interface DataTableProps { query: DataTableNode @@ -88,17 +88,24 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const showFilters = showEventFilter || showPropertyFilter const showTools = showReload || showExport || showColumnConfigurator + const inlineRow = showFilters ? 1 : showTools ? 2 : 0 return ( -
+
{showFilters && (
{showEventFilter && } {showPropertyFilter && ( )} + {inlineRow === 1 ? ( + <> +
+ void} /> + + ) : null}
)} {showFilters && showTools && } @@ -107,12 +114,19 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element {
{showReload && (canLoadNewData ? : )}
{showColumnConfigurator && } {showExport && } - void} /> + {inlineRow === 2 ? ( + void} /> + ) : null}
)} {showEventsBufferWarning && ( )} + {inlineRow === 0 ? ( +
+ void} /> +
+ ) : null} ([ (query: DataTableNode, columns): Required => ({ ...query, columns: columns, - showPropertyFilter: query.showPropertyFilter ?? true, - showEventFilter: query.showEventFilter ?? true, + showPropertyFilter: query.showPropertyFilter ?? false, + showEventFilter: query.showEventFilter ?? false, showActions: query.showActions ?? true, - showExport: query.showExport ?? true, - showReload: query.showReload ?? true, - showColumnConfigurator: query.showColumnConfigurator ?? true, + showExport: query.showExport ?? false, + showReload: query.showReload ?? false, + showColumnConfigurator: query.showColumnConfigurator ?? false, showEventsBufferWarning: query.showEventsBufferWarning ?? false, expandable: query.expandable ?? true, propertiesViaUrl: query.propertiesViaUrl ?? false, diff --git a/frontend/src/queries/nodes/Node/InlineEditor.tsx b/frontend/src/queries/nodes/Node/InlineEditor.tsx new file mode 100644 index 0000000000000..1092c1ab5e54c --- /dev/null +++ b/frontend/src/queries/nodes/Node/InlineEditor.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { IconEvent } from 'lib/components/icons' +import { LemonButton } from 'lib/components/LemonButton' +import { Node } from '~/queries/schema' +import { Drawer } from 'lib/components/Drawer' +import { urls } from 'scenes/urls' + +export interface InlineEditorProps { + query: Node + setQuery?: (query: Node) => void +} + +export function InlineEditor({ query, setQuery }: InlineEditorProps): JSX.Element { + const [open, setOpen] = useState(false) + + return ( + <> + setOpen(true)}> + + + setOpen(false)} + width="60vw" + title={ + <> + Open in Query Builder + + } + > + setQuery(JSON.parse(query)) : undefined} + /> + + + ) +} diff --git a/frontend/src/queries/nodes/Node/QueryEditorModal.tsx b/frontend/src/queries/nodes/Node/QueryEditorModal.tsx deleted file mode 100644 index f8bd0fd26445d..0000000000000 --- a/frontend/src/queries/nodes/Node/QueryEditorModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { LemonModal } from 'lib/components/LemonModal' -import { useState } from 'react' -import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' -import { IconEvent } from 'lib/components/icons' -import { LemonButton } from 'lib/components/LemonButton' -import { Node } from '~/queries/schema' - -export interface QueryEditorModalProps { - query: Node - setQuery?: (query: Node) => void -} - -export function QueryEditorModal({ query, setQuery }: QueryEditorModalProps): JSX.Element { - const [open, setOpen] = useState(false) - - return ( - <> - setOpen(true)}> - - - setOpen(false)} simple title={''} width={880}> - - setQuery(JSON.parse(query)) : undefined} - /> - - - - ) -} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 4261704199875..b394acb0e9b9f 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1247,38 +1247,15 @@ ], "type": "string" }, - "DataTableColumn": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/PropertyFilterType" - } - }, - "required": ["type", "key"], - "type": "object" - }, "DataTableNode": { "additionalProperties": false, "properties": { "columns": { - "anyOf": [ - { - "items": { - "$ref": "#/definitions/DataTableColumn" - }, - "type": "array" - }, - { - "items": { - "$ref": "#/definitions/DataTableStringColumn" - }, - "type": "array" - } - ], - "description": "Columns shown in the table" + "description": "Columns shown in the table", + "items": { + "$ref": "#/definitions/DataTableStringColumn" + }, + "type": "array" }, "expandable": { "description": "Can expand row to show raw event data (default: true)", @@ -1288,16 +1265,28 @@ "const": "DataTableNode", "type": "string" }, + "propertiesViaUrl": { + "description": "Link properties via the URL (default: false)", + "type": "boolean" + }, + "showActions": { + "description": "Show the kebab menu at the end of the row", + "type": "boolean" + }, + "showColumnConfigurator": { + "description": "Show a button to configure the table's columns", + "type": "boolean" + }, "showEventFilter": { "description": "Include an event filter above the table (default: true)", "type": "boolean" }, - "showExport": { - "description": "Show the export button", + "showEventsBufferWarning": { + "description": "Show warning about live events being buffered max 60 sec (default: false)", "type": "boolean" }, - "showMore": { - "description": "Show the \"...\" menu at the end of the row", + "showExport": { + "description": "Show the export button", "type": "boolean" }, "showPropertyFilter": { @@ -1317,15 +1306,7 @@ "type": "object" }, "DataTableStringColumn": { - "anyOf": [ - { - "$ref": "#/definitions/PropertyColumnString" - }, - { - "const": "person", - "type": "string" - } - ] + "type": "string" }, "DateRange": { "additionalProperties": false, @@ -1440,6 +1421,14 @@ "EventsNode": { "additionalProperties": false, "properties": { + "after": { + "description": "Only fetch events that happened after this timestamp", + "type": "string" + }, + "before": { + "description": "Only fetch events that happened before this timestamp", + "type": "string" + }, "custom_name": { "type": "string" }, @@ -1889,13 +1878,6 @@ "required": ["kind"], "type": "object" }, - "PropertyColumnString": { - "type": "string" - }, - "PropertyFilterType": { - "enum": ["meta", "event", "person", "element", "feature", "session", "cohort", "recording", "group"], - "type": "string" - }, "PropertyFilterValue": { "anyOf": [ { diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index bc40565b6c6e0..e1b0c05ec6fb3 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -20,25 +20,28 @@ export function EventsScene(): JSX.Element { }, propertiesViaUrl: true, showEventsBufferWarning: true, + showColumnConfigurator: true, + showEventFilter: true, + showExport: true, + showPropertyFilter: true, + showReload: true, } return ( - <> - { - if (isDataTableNode(newQuery)) { - if (!objectsEqual(newQuery.source.properties ?? [], query.source.properties ?? [])) { - setProperties(newQuery.source.properties ?? []) - } - if (!objectsEqual(newQuery.source.event ?? '', query.source.event ?? '')) { - setEventFilter(newQuery.source.event ?? '') - } - if (!objectsEqual(newQuery.columns ?? null, query.columns ?? null)) { - setColumns(newQuery.columns ?? null) - } + { + if (isDataTableNode(newQuery)) { + if (!objectsEqual(newQuery.source.properties ?? [], query.source.properties ?? [])) { + setProperties(newQuery.source.properties ?? []) } - }} - /> - + if (!objectsEqual(newQuery.source.event ?? '', query.source.event ?? '')) { + setEventFilter(newQuery.source.event ?? '') + } + if (!objectsEqual(newQuery.columns ?? null, query.columns ?? null)) { + setColumns(newQuery.columns ?? null) + } + } + }} + /> ) } From 4740db0c2d4bf91e33a4b1847f8fcce4118016a0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 01:41:13 +0100 Subject: [PATCH 50/90] times of plenty --- frontend/src/lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1dca8ef7df7f4..afd3bd4cb28b5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -555,7 +555,7 @@ const api = { }, async list( filters: EventsListQueryParams, - limit: number = 10, + limit: number = 100, teamId: TeamType['id'] = getCurrentTeamId() ): Promise> { const params: EventsListQueryParams = { ...filters, limit, orderBy: ['-timestamp'] } @@ -563,7 +563,7 @@ const api = { }, determineListEndpoint( filters: EventsListQueryParams, - limit: number = 10, + limit: number = 100, teamId: TeamType['id'] = getCurrentTeamId() ): string { const params: EventsListQueryParams = { ...filters, limit, orderBy: ['-timestamp'] } From f483e1952f15dcf3933938e48d2d09ddf800df1c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 01:44:00 +0100 Subject: [PATCH 51/90] add stories --- frontend/src/lib/api.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index 27c760852db22..2f9c707d2b34b 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -70,7 +70,7 @@ export const MOCK_DEFAULT_TEAM: TeamType = { access_control: true, has_group_types: true, primary_dashboard: 1, - live_events_columns: ['event', 'person'], + live_events_columns: null, } export const MOCK_DEFAULT_ORGANIZATION: OrganizationType = { From bf4cbdead7dc7e1b3e843fb7ccbc52ba588cc1e0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 01:44:04 +0100 Subject: [PATCH 52/90] add stories --- .../nodes/DataTable/DataTable.examples.ts | 58 +- .../nodes/DataTable/DataTable.stories.tsx | 28 +- .../src/queries/nodes/DataTable/DataTable.tsx | 9 +- .../nodes/DataTable/__mocks__/EventsNode.json | 870 ++++++++++++++++++ 4 files changed, 957 insertions(+), 8 deletions(-) create mode 100644 frontend/src/queries/nodes/DataTable/__mocks__/EventsNode.json diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index ffe462055302e..3ad0fc8e98f48 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -1,9 +1,61 @@ import { DataTableNode, NodeKind } from '~/queries/schema' -const NoColumns: DataTableNode = { +const AllDefaults: DataTableNode = { kind: NodeKind.DataTableNode, source: { kind: NodeKind.EventsNode }, - columns: undefined, } -export const examples = { NoColumns } +const Minimalist: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + showActions: false, + expandable: false, +} + +const ManyColumns: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + columns: [ + 'id', + 'event', + 'timestamp', + 'url', + 'person', + 'properties.$current_url', + 'properties.$browser', + 'properties.$browser_version', + 'properties.$lib', + 'person.properties.email', + ], +} + +const ShowFilters: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], + showEventFilter: true, + showPropertyFilter: true, +} + +const ShowTools: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], + showExport: true, + showReload: true, + showColumnConfigurator: true, +} + +const ShowAllTheThings: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.EventsNode }, + columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], + showExport: true, + showReload: true, + showColumnConfigurator: true, + showEventFilter: true, + showPropertyFilter: true, + showEventsBufferWarning: true, +} + +export const examples = { AllDefaults, Minimalist, ManyColumns, ShowFilters, ShowTools, ShowAllTheThings } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx index 94795b5cab0b1..c4b9fe39bc2e5 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -3,6 +3,8 @@ import { Query, QueryProps } from '~/queries/Query/Query' import { useState } from 'react' import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' import { examples } from './DataTable.examples' +import { mswDecorator } from '~/mocks/browser' +import events from './__mocks__/EventsNode.json' export default { title: 'Queries/DataTable', @@ -16,6 +18,13 @@ export default { argTypes: { query: { defaultValue: {} }, }, + decorators: [ + mswDecorator({ + get: { + '/api/projects/:projectId/events': events, + }, + }), + ], } as ComponentMeta const BasicTemplate: ComponentStory = (props: QueryProps) => { @@ -31,5 +40,20 @@ const BasicTemplate: ComponentStory = (props: QueryProps) => { ) } -export const NoColumns = BasicTemplate.bind({}) -NoColumns.args = { query: examples['NoColumns'] } +export const AllDefaults = BasicTemplate.bind({}) +AllDefaults.args = { query: examples['AllDefaults'] } + +export const Minimalist = BasicTemplate.bind({}) +Minimalist.args = { query: examples['Minimalist'] } + +export const ManyColumns = BasicTemplate.bind({}) +ManyColumns.args = { query: examples['ManyColumns'] } + +export const ShowFilters = BasicTemplate.bind({}) +ShowFilters.args = { query: examples['ShowFilters'] } + +export const ShowTools = BasicTemplate.bind({}) +ShowTools.args = { query: examples['ShowTools'] } + +export const ShowAllTheThings = BasicTemplate.bind({}) +ShowAllTheThings.args = { query: examples['ShowAllTheThings'] } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index afb984d2013b6..2c5fdae8cfd23 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -103,7 +103,10 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {inlineRow === 1 ? ( <>
- void} /> + void} + /> ) : null}
@@ -115,7 +118,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { {showColumnConfigurator && } {showExport && } {inlineRow === 2 ? ( - void} /> + void} /> ) : null}
)} @@ -124,7 +127,7 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { )} {inlineRow === 0 ? (
- void} /> + void} />
) : null} Date: Mon, 5 Dec 2022 01:47:22 +0100 Subject: [PATCH 53/90] add more stories --- .../nodes/DataTable/DataTable.examples.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index 3ad0fc8e98f48..5ee238ddf6cbc 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -1,4 +1,5 @@ import { DataTableNode, NodeKind } from '~/queries/schema' +import { PropertyFilterType, PropertyOperator } from '~/types' const AllDefaults: DataTableNode = { kind: NodeKind.DataTableNode, @@ -31,7 +32,19 @@ const ManyColumns: DataTableNode = { const ShowFilters: DataTableNode = { kind: NodeKind.DataTableNode, - source: { kind: NodeKind.EventsNode }, + source: { + kind: NodeKind.EventsNode, + properties: [ + { + key: '$browser', + value: ['Chrome'], + operator: PropertyOperator.Exact, + type: PropertyFilterType.Event, + }, + ], + event: '', + limit: 100, + }, columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], showEventFilter: true, showPropertyFilter: true, @@ -48,7 +61,19 @@ const ShowTools: DataTableNode = { const ShowAllTheThings: DataTableNode = { kind: NodeKind.DataTableNode, - source: { kind: NodeKind.EventsNode }, + source: { + kind: NodeKind.EventsNode, + properties: [ + { + key: '$browser', + value: ['Chrome'], + operator: PropertyOperator.Exact, + type: PropertyFilterType.Event, + }, + ], + event: '', + limit: 100, + }, columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], showExport: true, showReload: true, From f2650e463ea6e6d86f1869b3a9f72124cdb31552 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 02:07:10 +0100 Subject: [PATCH 54/90] friendly hint --- frontend/src/scenes/events/EventsScene.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index e1b0c05ec6fb3..c9e34dee2a80d 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -40,6 +40,8 @@ export function EventsScene(): JSX.Element { if (!objectsEqual(newQuery.columns ?? null, query.columns ?? null)) { setColumns(newQuery.columns ?? null) } + // If anything else was changed in the query (nice try, hacker), we won't save it. + // Use the query editor to play around. } }} /> From 686b8e619b419dabb3557e7b86c250acf4d86335 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 02:13:25 +0100 Subject: [PATCH 55/90] update snapshot --- .../__snapshots__/verifiedDomainsLogic.test.ts.snap | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap b/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap index 1866a0410dca1..5156ac9496c00 100644 --- a/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap +++ b/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap @@ -56,10 +56,7 @@ exports[`verifiedDomainsLogic values has proper defaults 1`] = ` "id": 997, "ingested_event": true, "is_demo": false, - "live_events_columns": [ - "event", - "person", - ], + "live_events_columns": null, "name": "MockHog App + Marketing", "organization": "ABCD", "path_cleaning_filters": [], From 467b54ca04c08f99433021f9d3b5b68828a4a3e3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 09:19:24 +0100 Subject: [PATCH 56/90] replace a few more events tables --- frontend/src/queries/Query/Query.tsx | 8 ++-- .../queries/nodes/DataNode/dataNodeLogic.ts | 9 ++++- frontend/src/queries/query.ts | 3 +- frontend/src/queries/schema.ts | 5 +++ frontend/src/scenes/actions/Action.tsx | 33 ++++++++++++---- .../definition/DefinitionView.tsx | 37 +++++++++++++----- .../src/scenes/feature-flags/FeatureFlag.tsx | 21 +++++++++- frontend/src/scenes/groups/Group.tsx | 39 +++++++++++++++---- 8 files changed, 124 insertions(+), 31 deletions(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 02b8380a1b549..4c0feeaf5d899 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,14 +1,14 @@ import { isDataNode, isDataTableNode, isLegacyQuery, isInsightQueryNode } from '../utils' import { DataTable } from '~/queries/nodes/DataTable/DataTable' import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { Node } from '~/queries/schema' +import { Node, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' -export interface QueryProps { - query: Node | string - setQuery?: (node: Node) => void +export interface QueryProps { + query: T | string + setQuery?: (node: T) => void } export function Query(props: QueryProps): JSX.Element { diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 2baa845e96bb7..f6f8551a708c0 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -86,7 +86,14 @@ export const dataNodeLogic = kea([ autoLoadToggled: [ false, // store the autoload toggle's state in localstorage, separately for each data node kind - { persist: true, storageKey: `queries.nodes.dataNodeLogic..autoLoadToggled.${props.query.kind}` }, + { + persist: true, + storageKey: [ + 'queries.nodes.dataNodeLogic..autoLoadToggled', + props.query.kind, + isEventsNode(props.query) && props.query.actionId ? 'action' : '', + ].join('.'), + }, { toggleAutoLoad: (state) => !state }, ], autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 3f6c94377d1b3..16df5d2058460 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -22,8 +22,9 @@ export async function query( if (isEventsNode(query)) { return await api.events.list( { - properties: query.properties, + properties: [...(query.fixedProperties || []), ...(query.properties || [])], ...(query.event ? { event: query.event } : {}), + ...(query.actionId ? { action_id: query.actionId } : {}), before: query.before, after: query.after, }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 43736d575c958..c7ba6dc669b81 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -72,13 +72,18 @@ export interface EntityNode extends DataNode { math?: BaseMathType | PropertyMathType | CountPerActorMathType math_property?: string math_group_type_index?: 0 | 1 | 2 | 3 | 4 + /** Properties configurable in the interface */ properties?: AnyPropertyFilter[] + /** Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person) */ + fixedProperties?: AnyPropertyFilter[] } export interface EventsNode extends EntityNode { kind: NodeKind.EventsNode event?: string limit?: number + /** Show events matching a given action */ + actionId?: number /** Only fetch events that happened before this timestamp */ before?: string /** Only fetch events that happened after this timestamp */ diff --git a/frontend/src/scenes/actions/Action.tsx b/frontend/src/scenes/actions/Action.tsx index 25c330ba15871..a705a58e009b1 100644 --- a/frontend/src/scenes/actions/Action.tsx +++ b/frontend/src/scenes/actions/Action.tsx @@ -8,6 +8,10 @@ import { dayjs } from 'lib/dayjs' import { Spinner } from 'lib/components/Spinner/Spinner' import { SceneExport } from 'scenes/sceneTypes' import { actionLogic, ActionLogicProps } from 'scenes/actions/actionLogic' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export const scene: SceneExport = { logic: actionLogic, @@ -23,6 +27,9 @@ export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element { const { action, isComplete } = useValues(actionLogic) const { loadAction } = useActions(actionLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + return ( <> {(!id || action) && ( @@ -53,13 +60,25 @@ export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element { )}

- + {featureDataExploration ? ( + + ) : ( + + )}
) : (
diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx index f7cb92aa6b79e..b0e0fc0957d2d 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx @@ -27,6 +27,10 @@ import { NotFound } from 'lib/components/NotFound' import { IconPlayCircle } from 'lib/components/icons' import { combineUrl } from 'kea-router/lib/utils' import { urls } from 'scenes/urls' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' export const scene: SceneExport = { component: DefinitionView, @@ -50,6 +54,8 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { } = useValues(logic) const { setPageMode } = useActions(logic) const { hasAvailableFeature } = useValues(userLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] if (definitionLoading) { return @@ -58,6 +64,7 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { if (definitionMissing) { return } + return (
{mode === DefinitionPageMode.Edit ? ( @@ -201,15 +208,27 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { This is the list of recent events that match this definition.

- + {featureDataExploration ? ( + + ) : ( + + )}
)} diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 23300ea007f66..63eef0cd23b86 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -50,6 +50,8 @@ import { isPropertyFilterWithOperator } from 'lib/components/PropertyFilters/uti import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' import { ResourcePermission } from 'scenes/ResourcePermissionModal' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' export const scene: SceneExport = { component: FeatureFlag, @@ -429,7 +431,24 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { } function ExposureTab({ id, featureFlagKey }: { id: string; featureFlagKey: string }): JSX.Element { - return ( + const { featureFlags } = useValues(enabledFeaturesLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + + return featureDataExploration ? ( + + ) : ( : @@ -70,14 +76,31 @@ export function Group(): JSX.Element { Events} key={PersonsTabType.EVENTS}> - + {featureDataExploration ? ( + + ) : ( + + )} Date: Mon, 5 Dec 2022 09:48:17 +0100 Subject: [PATCH 57/90] add custom columns, add web performance table --- frontend/src/queries/Query/Query.tsx | 10 +- .../src/queries/nodes/DataTable/DataTable.tsx | 10 +- .../queries/nodes/DataTable/dataTableLogic.ts | 7 +- .../queries/nodes/DataTable/renderColumn.tsx | 8 +- .../queries/nodes/DataTable/renderTitle.tsx | 5 +- frontend/src/queries/schema.json | 20 +++ frontend/src/queries/schema.ts | 3 + frontend/src/scenes/events/EventsTable.tsx | 8 +- .../src/scenes/performance/WebPerformance.tsx | 134 ++++++++++++------ 9 files changed, 148 insertions(+), 57 deletions(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 4c0feeaf5d899..de9b1d956cea4 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,18 +1,22 @@ import { isDataNode, isDataTableNode, isLegacyQuery, isInsightQueryNode } from '../utils' import { DataTable } from '~/queries/nodes/DataTable/DataTable' import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { Node, QuerySchema } from '~/queries/schema' +import { Node, QueryCustom, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' export interface QueryProps { + /** The query to render */ query: T | string + /** Set this if the user can update the query */ setQuery?: (node: T) => void + /** Custom components passed down to query nodes (e.g. custom table columns) */ + custom?: QueryCustom } export function Query(props: QueryProps): JSX.Element { - const { query, setQuery } = props + const { query, setQuery, custom } = props if (typeof query === 'string') { try { return @@ -24,7 +28,7 @@ export function Query(props: QueryProps): JSX.Element { if (isLegacyQuery(query)) { component = } else if (isDataTableNode(query)) { - component = + component = } else if (isDataNode(query)) { component = } else if (isInsightQueryNode(query)) { diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 2c5fdae8cfd23..17f1fe5195188 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode, Node } from '~/queries/schema' +import { DataTableNode, EventsNode, Node, QueryCustom } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -28,11 +28,13 @@ import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' interface DataTableProps { query: DataTableNode setQuery?: (node: DataTableNode) => void + /** Custom table columns */ + custom?: QueryCustom } let uniqueNode = 0 -export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { +export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Element { const [key] = useState(() => `DataTable.${uniqueNode++}`) const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key } @@ -66,9 +68,9 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ dataIndex: key as any, - title: renderTitle(key), + title: renderTitle(key, custom), render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(key, record, query, setQuery) + return renderColumn(key, record, query, setQuery, custom) }, })), ...(showActions diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index f8c7da71e2459..6fde375a7122a 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -27,7 +27,12 @@ export const dataTableLogic = kea([ // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' const topLevelFields = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] return storedColumns.map((column) => { - if (topLevelFields.includes(column) || column.includes('properties.')) { + if ( + topLevelFields.includes(column) || + column.startsWith('person.properties.') || + column.startsWith('properties.') || + column.startsWith('custom.') + ) { return column } else { return `properties.${column}` diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index dc6fec1fd6709..c81cbfd727a26 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -6,7 +6,7 @@ import { TZLabel } from 'lib/components/TZLabel' import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' -import { DataTableNode } from '~/queries/schema' +import { DataTableNode, QueryCustom } from '~/queries/schema' import { isEventsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' @@ -14,7 +14,8 @@ export function renderColumn( key: string, record: EventType, query: DataTableNode, - setQuery?: (node: DataTableNode) => void + setQuery?: (node: DataTableNode) => void, + custom?: QueryCustom ): JSX.Element | string { if (key === 'event') { if (record.event === '$autocapture') { @@ -129,6 +130,9 @@ export function renderColumn( ) + } else if (key.startsWith('custom.')) { + const Component = custom?.[key.substring(7)]?.render + return Component ? : '' } else { return String(record[key]) } diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index 0dc573140ce98..9904a3174ea83 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -1,7 +1,8 @@ import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { QueryCustom } from '~/queries/schema' -export function renderTitle(key: string): JSX.Element | string { +export function renderTitle(key: string, custom?: QueryCustom): JSX.Element | string { if (key === 'timestamp') { return 'Time' } else if (key === 'event') { @@ -12,6 +13,8 @@ export function renderTitle(key: string): JSX.Element | string { return 'URL / Screen' } else if (key.startsWith('properties.')) { return + } else if (key.startsWith('custom.')) { + return custom?.[key.substring(7)]?.title ?? key.substring(7).replace('_', ' ') } else if (key.startsWith('person.properties.')) { // NOTE: type=Event is not a mistake, even if it's a person property. Don't ask, won't fix. return diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index b394acb0e9b9f..bc0107bb04b18 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -8,6 +8,13 @@ "custom_name": { "type": "string" }, + "fixedProperties": { + "description": "Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person)", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, "id": { "type": "number" }, @@ -39,6 +46,7 @@ "type": "string" }, "properties": { + "description": "Properties configurable in the interface", "items": { "$ref": "#/definitions/AnyPropertyFilter" }, @@ -1421,6 +1429,10 @@ "EventsNode": { "additionalProperties": false, "properties": { + "actionId": { + "description": "Show events matching a given action", + "type": "number" + }, "after": { "description": "Only fetch events that happened after this timestamp", "type": "string" @@ -1435,6 +1447,13 @@ "event": { "type": "string" }, + "fixedProperties": { + "description": "Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person)", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, "kind": { "const": "EventsNode", "type": "string" @@ -1466,6 +1485,7 @@ "type": "string" }, "properties": { + "description": "Properties configurable in the interface", "items": { "$ref": "#/definitions/AnyPropertyFilter" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index c7ba6dc669b81..f430aaf6e02cf 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -225,3 +225,6 @@ export interface BreakdownFilter { breakdown_group_type_index?: number | null aggregation_group_type_index?: number | undefined // Groups aggregation } + +/** Pass custom templates and other metadata to queries. Used for e.g. custom columns in the DataTable. */ +export type QueryCustom = Record JSX.Element }> diff --git a/frontend/src/scenes/events/EventsTable.tsx b/frontend/src/scenes/events/EventsTable.tsx index f7f0cc866a5f2..32153bf68a77c 100644 --- a/frontend/src/scenes/events/EventsTable.tsx +++ b/frontend/src/scenes/events/EventsTable.tsx @@ -472,11 +472,11 @@ export function EventsTable({ person: ['person.distinct_ids.0', 'person.properties.email'], } - return (selectedColumns === 'DEFAULT' ? defaultColumns.map((e) => e.key || '') : selectedColumns).flatMap( - (x) => { + return (selectedColumns === 'DEFAULT' ? defaultColumns.map((e) => e.key || '') : selectedColumns) + .flatMap((x) => { return columnMapping[x] || `properties.${x}` - } - ) + }) + .filter((c) => !c.startsWith('custom.')) }, [defaultColumns, selectedColumns]) return ( diff --git a/frontend/src/scenes/performance/WebPerformance.tsx b/frontend/src/scenes/performance/WebPerformance.tsx index d346309dee09c..84d869af3c83c 100644 --- a/frontend/src/scenes/performance/WebPerformance.tsx +++ b/frontend/src/scenes/performance/WebPerformance.tsx @@ -10,6 +10,10 @@ import { useActions, useValues } from 'kea' import { WebPerformanceWaterfallChart } from 'scenes/performance/WebPerformanceWaterfallChart' import { IconPlay } from 'lib/components/icons' import { LemonButton } from '@posthog/lemon-ui' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { Query } from '~/queries/Query/Query' +import { EventsNode, NodeKind } from '~/queries/schema' /* * link to SessionRecording from table and chart @@ -27,53 +31,99 @@ export const webPerformancePropertyFilters: AnyPropertyFilter[] = [ const EventsWithPerformanceTable = (): JSX.Element => { const { setEventToDisplay } = useActions(webPerformanceLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] return ( <>
- - } - type="secondary" - size="small" - onClick={() => { - setEventToDisplay(event) - }} - > - View waterfall chart - -
- ) + {featureDataExploration ? ( + + columns: ['properties.$current_url', 'properties.$lib', 'timestamp', 'custom.waterfallButton'], + showReload: true, + showColumnConfigurator: false, + showExport: true, + showEventFilter: false, + showPropertyFilter: true, + showActions: false, + expandable: false, + }} + custom={{ + waterfallButton: { + title: '', + render: function RenderWaterfallButton({ + record: event, + }: { + record: Required['response']['results'][0] + }) { + return ( +
+ } + type="secondary" + size="small" + onClick={() => setEventToDisplay(event)} + > + View waterfall chart + +
+ ) + }, + }, + }} + /> + ) : ( + + } + type="secondary" + size="small" + onClick={() => { + setEventToDisplay(event) + }} + > + View waterfall chart + +
+ ) + }, + }, + ]} + /> + )} ) } From 9a2b18a02d462a897f1e0b953d10835ef32e9aae Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 09:56:20 +0100 Subject: [PATCH 58/90] set query locally --- frontend/src/queries/Query/Query.tsx | 9 ++++++++- frontend/src/scenes/groups/Group.tsx | 1 + frontend/src/scenes/performance/WebPerformance.tsx | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index de9b1d956cea4..2b1506d01c7e2 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -5,18 +5,25 @@ import { Node, QueryCustom, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' +import { useState } from 'react' export interface QueryProps { /** The query to render */ query: T | string /** Set this if the user can update the query */ setQuery?: (node: T) => void + /** Can the query can still be edited locally if there is `setQuery` */ + setQueryLocally?: boolean /** Custom components passed down to query nodes (e.g. custom table columns) */ custom?: QueryCustom } export function Query(props: QueryProps): JSX.Element { - const { query, setQuery, custom } = props + const { query: globalQuery, setQuery: globalSetQuery, setQueryLocally, custom } = props + const [localQuery, localSetQuery] = useState(globalQuery) + const query = setQueryLocally ? localQuery : globalQuery + const setQuery = setQueryLocally ? localSetQuery : globalSetQuery + if (typeof query === 'string') { try { return diff --git a/frontend/src/scenes/groups/Group.tsx b/frontend/src/scenes/groups/Group.tsx index 721bb34497f0b..f47fbfa9871ea 100644 --- a/frontend/src/scenes/groups/Group.tsx +++ b/frontend/src/scenes/groups/Group.tsx @@ -90,6 +90,7 @@ export function Group(): JSX.Element { showEventFilter: true, showPropertyFilter: true, }} + setQueryLocally /> ) : ( { showActions: false, expandable: false, }} + setQueryLocally custom={{ waterfallButton: { title: '', From 7f287856ffeefc0b7efd69b4a815defa85175de0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 12:24:45 +0100 Subject: [PATCH 59/90] persons page --- .../queries/nodes/DataNode/dataNodeLogic.ts | 10 +++--- frontend/src/queries/query.ts | 1 + frontend/src/queries/schema.json | 4 +++ frontend/src/queries/schema.ts | 2 ++ frontend/src/scenes/persons/Person.tsx | 36 +++++++++++++++---- frontend/src/types.ts | 2 +- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index f6f8551a708c0..a41921f0a4446 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -5,6 +5,7 @@ import { DataNode, EventsNode } from '~/queries/schema' import { query } from '~/queries/query' import { isEventsNode } from '~/queries/utils' import { subscriptions } from 'kea-subscriptions' +import clsx from 'clsx' export interface DataNodeLogicProps { key: string @@ -88,11 +89,10 @@ export const dataNodeLogic = kea([ // store the autoload toggle's state in localstorage, separately for each data node kind { persist: true, - storageKey: [ - 'queries.nodes.dataNodeLogic..autoLoadToggled', - props.query.kind, - isEventsNode(props.query) && props.query.actionId ? 'action' : '', - ].join('.'), + storageKey: clsx('queries.nodes.dataNodeLogic..autoLoadToggled', props.query.kind, { + action: isEventsNode(props.query) && props.query.actionId, + person: isEventsNode(props.query) && props.query.personId, + }), }, { toggleAutoLoad: (state) => !state }, ], diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 16df5d2058460..66100c9dec657 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -25,6 +25,7 @@ export async function query( properties: [...(query.fixedProperties || []), ...(query.properties || [])], ...(query.event ? { event: query.event } : {}), ...(query.actionId ? { action_id: query.actionId } : {}), + ...(query.personId ? { person_id: query.personId } : {}), before: query.before, after: query.after, }, diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index bc0107bb04b18..5c6315e1ee4d1 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1484,6 +1484,10 @@ "name": { "type": "string" }, + "personId": { + "description": "Show events for a given person", + "type": "string" + }, "properties": { "description": "Properties configurable in the interface", "items": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index f430aaf6e02cf..52426868133d3 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -84,6 +84,8 @@ export interface EventsNode extends EntityNode { limit?: number /** Show events matching a given action */ actionId?: number + /** Show events for a given person */ + personId?: string /** Only fetch events that happened before this timestamp */ before?: string /** Only fetch events that happened after this timestamp */ diff --git a/frontend/src/scenes/persons/Person.tsx b/frontend/src/scenes/persons/Person.tsx index 6b9127b80413e..ca9769e3e6ba5 100644 --- a/frontend/src/scenes/persons/Person.tsx +++ b/frontend/src/scenes/persons/Person.tsx @@ -27,6 +27,10 @@ import { SpinnerOverlay } from 'lib/components/Spinner/Spinner' import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' import { NotFound } from 'lib/components/NotFound' import { RelatedFeatureFlags } from './RelatedFeatureFlags' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' const { TabPane } = Tabs @@ -97,6 +101,8 @@ export function Person(): JSX.Element | null { } = useActions(personsLogic) const { groupsEnabled } = useValues(groupsAccessLogic) const { currentTeam } = useValues(teamLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] if (!person) { return personLoading ? : @@ -157,12 +163,30 @@ export function Person(): JSX.Element | null { /> Events} key={PersonsTabType.EVENTS}> - + {featureDataExploration ? ( + + ) : ( + + )} Recordings} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index c5b2665105622..946407e997e6a 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -640,7 +640,7 @@ export interface FunnelStepRangeEntityFilter { export type EntityFilterTypes = EntityFilter | ActionFilter | null export interface PersonType { - id?: number + id?: string uuid?: string name?: string distinct_ids: string[] From 0ec6f685debe60fcff4928c8e713942adb7c48b2 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 12:39:48 +0100 Subject: [PATCH 60/90] fix person id types --- frontend/src/lib/api.ts | 4 ++-- .../ActivityLog/__mocks__/activityLogMocks.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index afd3bd4cb28b5..daa591a3380f3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -816,7 +816,7 @@ const api = { async update(id: number, person: Partial): Promise { return new ApiRequest().person(id).update({ data: person }) }, - async updateProperty(id: number, property: string, value: any): Promise { + async updateProperty(id: string, property: string, value: any): Promise { return new ApiRequest() .person(id) .withAction('update_property') @@ -827,7 +827,7 @@ const api = { }, }) }, - async deleteProperty(id: number, property: string): Promise { + async deleteProperty(id: string, property: string): Promise { return new ApiRequest() .person(id) .withAction('delete_property') diff --git a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts index dab8e2ab4b1c9..daf09e69cc983 100644 --- a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts +++ b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts @@ -427,7 +427,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ type: 'Person', source: [ { - id: 502746582, + id: '502746582', name: '1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4', uuid: '01819231-75a0-0000-467e-a4be57b44a37', created_at: '2022-06-23T20:12:03.828000Z', @@ -462,7 +462,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4'], }, { - id: 502725471, + id: '502725471', name: '1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7', uuid: '01819220-aa0b-0000-6992-fad9de0ea4dc', created_at: '2022-06-23T19:53:43.137000Z', @@ -497,7 +497,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7'], }, { - id: 502715718, + id: '502715718', name: '18192189287517-0e66a695611002-3297640-75300-18192189288790', uuid: '01819218-92f2-0000-7132-90a079001dc9', created_at: '2022-06-23T19:44:52.944000Z', @@ -532,7 +532,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['18192189287517-0e66a695611002-3297640-75300-18192189288790'], }, { - id: 502696118, + id: '502696118', name: '1819208c6ed32c-0e427ef09bbae-3297640-75300-1819208c6ee74b', uuid: '01819208-c74f-0000-75e7-7d3dc16da26b', created_at: '2022-06-23T19:27:37.771000Z', @@ -568,7 +568,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ }, ], target: { - id: 502792727, + id: '502792727', name: '1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7', uuid: '01819256-1d25-0000-4ed7-ea437589ada7', created_at: '2022-06-23T20:52:06.053733Z', From f547132bc94452c1b1b99605e00e79a581983e49 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 15:17:40 +0100 Subject: [PATCH 61/90] basic persons table --- frontend/src/queries/examples.ts | 34 +++++++++++++ .../src/queries/nodes/DataTable/DataTable.tsx | 23 ++++++--- .../nodes/DataTable/DataTableExport.tsx | 23 +++++---- .../queries/nodes/DataTable/dataTableLogic.ts | 6 ++- .../queries/nodes/DataTable/renderColumn.tsx | 46 ++++++++++------- .../queries/nodes/DataTable/renderTitle.tsx | 2 + .../PersonsNode/PersonPropertyFilters.tsx | 26 ++++++++++ frontend/src/queries/query.ts | 41 ++++++++++----- frontend/src/queries/schema.json | 50 ++++++++++++++++++- frontend/src/queries/schema.ts | 15 +++++- frontend/src/queries/utils.ts | 9 +++- frontend/src/scenes/events/EventsScene.tsx | 4 +- 12 files changed, 222 insertions(+), 57 deletions(-) create mode 100644 frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 3f2356c5ffa23..91e8a201b6a0b 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -12,6 +12,7 @@ import { PathsQuery, StickinessQuery, LifecycleQuery, + PersonsNode, } from '~/queries/schema' import { ChartDisplayType, @@ -39,6 +40,35 @@ const EventsTable: DataTableNode = { columns: defaultDataTableStringColumns, source: Events, } +const EventsTableFull: DataTableNode = { + ...EventsTable, + showPropertyFilter: true, + showEventFilter: true, + showExport: true, + showReload: true, + showColumnConfigurator: true, + showEventsBufferWarning: true, +} + +const Persons: PersonsNode = { + kind: NodeKind.PersonsNode, + properties: [ + { type: PropertyFilterType.Person, key: '$browser', operator: PropertyOperator.Exact, value: 'Chrome' }, + ], +} + +const PersonsTable: DataTableNode = { + kind: NodeKind.DataTableNode, + columns: ['id', 'person', 'properties.$geoip_country_name', 'properties.$browser', 'created_at'], + source: Persons, +} + +const PersonsTableFull: DataTableNode = { + ...PersonsTable, + showPropertyFilter: true, + showExport: true, + showReload: true, +} const LegacyTrendsQuery: LegacyQuery = { kind: NodeKind.LegacyQuery, @@ -196,6 +226,10 @@ const InsightLifecycleQuery: LifecycleQuery = { export const examples: Record = { Events, EventsTable, + EventsTableFull, + Persons, + PersonsTable, + PersonsTableFull, LegacyTrendsQuery, InsightTrendsQuery, InsightFunnelsQuery, diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 17f1fe5195188..fa232d2f23d22 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode, Node, QueryCustom } from '~/queries/schema' +import { DataTableNode, EventsNode, Node, PersonsNode, QueryCustom } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -24,6 +24,8 @@ import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' +import { isEventsNode, isPersonsNode } from '~/queries/utils' +import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' interface DataTableProps { query: DataTableNode @@ -73,7 +75,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem return renderColumn(key, record, query, setQuery, custom) }, })), - ...(showActions + ...(showActions && isEventsNode(query.source) ? [ { dataIndex: 'more' as any, @@ -86,7 +88,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem : []), ] const dataSource = (response as null | EventsNode['response'])?.results ?? [] - const setQuerySource = (source: EventsNode): void => setQuery?.({ ...query, source }) + const setQuerySource = (source: EventsNode | PersonsNode): void => setQuery?.({ ...query, source }) const showFilters = showEventFilter || showPropertyFilter const showTools = showReload || showExport || showColumnConfigurator @@ -98,10 +100,15 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem
{showFilters && (
- {showEventFilter && } - {showPropertyFilter && ( + {showEventFilter && isEventsNode(query.source) && ( + + )} + {showPropertyFilter && isEventsNode(query.source) && ( )} + {showPropertyFilter && isPersonsNode(query.source) && ( + + )} {inlineRow === 1 ? ( <>
@@ -117,14 +124,16 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem {showTools && (
{showReload && (canLoadNewData ? : )}
- {showColumnConfigurator && } + {showColumnConfigurator && isEventsNode(query.source) && ( + + )} {showExport && } {inlineRow === 2 ? ( void} /> ) : null}
)} - {showEventsBufferWarning && ( + {showEventsBufferWarning && isEventsNode(query.source) && ( )} {inlineRow === 0 ? ( diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 2f4eae29b9e8c..192c3df84ffc3 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -3,20 +3,19 @@ import { IconExport } from 'lib/components/icons' import { Popconfirm } from 'antd' import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' -import api from 'lib/api' import { DataTableNode } from '~/queries/schema' import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { isEventsNode, isPersonsNode } from '~/queries/utils' +import { getEventsEndpoint, getPersonsEndpoint } from '~/queries/query' function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { - const exportContext = { - path: api.events.determineListEndpoint( - { - ...(query.source.event ? { event: query.source.event } : {}), - ...(query.source.properties ? { properties: query.source.properties } : {}), - }, - 3500 - ), - max_limit: 3500, + const exportContext = isEventsNode(query.source) + ? { path: getEventsEndpoint(query.source), max_limit: query.source.limit ?? 3500 } + : isPersonsNode(query.source) + ? { path: getPersonsEndpoint(query.source), max_limit: 3500 } + : undefined + if (!exportContext) { + throw new Error('Unsupported node type') } const columnMapping = { @@ -24,7 +23,9 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void time: 'timestamp', event: 'event', source: 'properties.$lib', - person: ['person.distinct_ids.0', 'person.properties.email'], + person: isPersonsNode(query.source) + ? ['distinct_ids.0', 'properties.email'] + : ['person.distinct_ids.0', 'person.properties.email'], } if (onlySelectedColumns) { diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 6fde375a7122a..6965a824d96bf 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -25,10 +25,12 @@ export const dataTableLogic = kea([ (s) => [s.storedColumns], (storedColumns) => { // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' - const topLevelFields = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] + const topLevelFieldsEvents = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] + const topLevelFieldsPersons = ['id', 'distinct_ids', 'created_at', 'is_identified', 'name', 'person'] return storedColumns.map((column) => { if ( - topLevelFields.includes(column) || + topLevelFieldsEvents.includes(column) || + topLevelFieldsPersons.includes(column) || column.startsWith('person.properties.') || column.startsWith('properties.') || column.startsWith('custom.') diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index c81cbfd727a26..5283b8c832baf 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -1,4 +1,4 @@ -import { AnyPropertyFilter, EventType, PropertyFilterType, PropertyOperator } from '~/types' +import { AnyPropertyFilter, EventType, PersonType, PropertyFilterType, PropertyOperator } from '~/types' import { autoCaptureEventToDescription } from 'lib/utils' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { Link } from 'lib/components/Link' @@ -7,22 +7,23 @@ import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' import { DataTableNode, QueryCustom } from '~/queries/schema' -import { isEventsNode } from '~/queries/utils' +import { isEventsNode, isPersonsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' export function renderColumn( key: string, - record: EventType, + record: EventType | PersonType, query: DataTableNode, setQuery?: (node: DataTableNode) => void, custom?: QueryCustom ): JSX.Element | string { - if (key === 'event') { - if (record.event === '$autocapture') { - return autoCaptureEventToDescription(record) + if (key === 'event' && isEventsNode(query.source)) { + const eventRecord = record as EventType + if (eventRecord.event === '$autocapture') { + return autoCaptureEventToDescription(eventRecord) } else { - const content = - const { $sentry_url } = record.properties + const content = + const { $sentry_url } = eventRecord.properties return $sentry_url ? ( {content} @@ -31,17 +32,17 @@ export function renderColumn( content ) } - } else if (key === 'timestamp') { - return + } else if (key === 'timestamp' || key === 'created_at') { + return } else if (key.startsWith('properties.') || key === 'url') { const propertyKey = key === 'url' ? (record.properties['$screen_name'] ? '$screen_name' : '$current_url') : key.substring(11) - if (setQuery && isEventsNode(query.source)) { + if (setQuery && (isEventsNode(query.source) || isPersonsNode(query.source))) { const newProperty: AnyPropertyFilter = { key: propertyKey, value: record.properties[propertyKey], operator: PropertyOperator.Exact, - type: PropertyFilterType.Event, + type: isPersonsNode(query.source) ? PropertyFilterType.Person : PropertyFilterType.Event, } const matchingProperty = (query.source.properties || []).find( (p) => p.key === newProperty.key && p.type === newProperty.type @@ -80,11 +81,12 @@ export function renderColumn( } return } else if (key.startsWith('person.properties.')) { + const eventRecord = record as EventType const propertyKey = key.substring(18) if (setQuery && isEventsNode(query.source)) { const newProperty: AnyPropertyFilter = { key: propertyKey, - value: record.person?.properties[propertyKey], + value: eventRecord.person?.properties[propertyKey], operator: PropertyOperator.Exact, type: PropertyFilterType.Person, } @@ -119,15 +121,23 @@ export function renderColumn( }) }} > - + ) } - return - } else if (key === 'person') { + return + } else if (key === 'person' && isEventsNode(query.source)) { + const eventRecord = record as EventType + return ( + + + + ) + } else if (key === 'person' && isPersonsNode(query.source)) { + const personRecord = record as PersonType return ( - - + + ) } else if (key.startsWith('custom.')) { diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index 9904a3174ea83..6e8bdec7e53cc 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -5,6 +5,8 @@ import { QueryCustom } from '~/queries/schema' export function renderTitle(key: string, custom?: QueryCustom): JSX.Element | string { if (key === 'timestamp') { return 'Time' + } else if (key === 'created_at') { + return 'First seen' } else if (key === 'event') { return 'Event' } else if (key === 'person') { diff --git a/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx new file mode 100644 index 0000000000000..1749ed78ed6d8 --- /dev/null +++ b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx @@ -0,0 +1,26 @@ +import { PersonsNode } from '~/queries/schema' +import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' +import { AnyPropertyFilter } from '~/types' +import { useState } from 'react' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' + +interface PersonPropertyFiltersProps { + query: PersonsNode + setQuery?: (node: PersonsNode) => void +} + +let uniqueNode = 0 +export function PersonPropertyFilters({ query, setQuery }: PersonPropertyFiltersProps): JSX.Element { + const [id] = useState(uniqueNode++) + return !query.properties || Array.isArray(query.properties) ? ( + setQuery?.({ ...query, properties: value })} + pageKey={`PersonPropertyFilters.${id}`} + taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties]} + style={{ marginBottom: 0, marginTop: 0 }} + /> + ) : ( +
Error: property groups are not supported.
+ ) +} diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 66100c9dec657..d4f6b70aa5264 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -1,5 +1,5 @@ -import { DataNode } from './schema' -import { isEventsNode, isLegacyQuery } from './utils' +import { DataNode, EventsNode, PersonsNode } from './schema' +import { isEventsNode, isLegacyQuery, isPersonsNode } from './utils' import api, { ApiMethodOptions } from 'lib/api' import { getCurrentTeamId } from 'lib/utils/logics' import { AnyPartialFilterType } from '~/types' @@ -20,17 +20,9 @@ export async function query( methodOptions?: ApiMethodOptions ): Promise { if (isEventsNode(query)) { - return await api.events.list( - { - properties: [...(query.fixedProperties || []), ...(query.properties || [])], - ...(query.event ? { event: query.event } : {}), - ...(query.actionId ? { action_id: query.actionId } : {}), - ...(query.personId ? { person_id: query.personId } : {}), - before: query.before, - after: query.after, - }, - query.limit - ) + return await api.get(getEventsEndpoint(query)) + } else if (isPersonsNode(query)) { + return await api.get(getPersonsEndpoint(query)) } else if (isLegacyQuery(query)) { const [response] = await legacyInsightQuery({ filters: query.filters, @@ -42,6 +34,29 @@ export async function query( throw new Error(`Unsupported query: ${query.kind}`) } +export function getEventsEndpoint(query: EventsNode): string { + return api.events.determineListEndpoint( + { + properties: [...(query.fixedProperties || []), ...(query.properties || [])], + ...(query.event ? { event: query.event } : {}), + ...(query.actionId ? { action_id: query.actionId } : {}), + ...(query.personId ? { person_id: query.personId } : {}), + ...(query.before ? { before: query.before } : {}), + ...(query.after ? { after: query.after } : {}), + }, + query.limit ?? 3500 + ) +} + +export function getPersonsEndpoint(query: PersonsNode): string { + return api.persons.determineListUrl({ + properties: [...(query.fixedProperties || []), ...(query.properties || [])], + ...(query.search ? { search: query.search } : {}), + ...(query.cohort ? { cohort: query.cohort } : {}), + ...(query.distinctId ? { distinct_id: query.distinctId } : {}), + }) +} + interface LegacyInsightQueryParams { filters: AnyPartialFilterType currentTeamId: number diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 5c6315e1ee4d1..dc6b899db410b 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1306,7 +1306,14 @@ "type": "boolean" }, "source": { - "$ref": "#/definitions/EventsNode", + "anyOf": [ + { + "$ref": "#/definitions/EventsNode" + }, + { + "$ref": "#/definitions/PersonsNode" + } + ], "description": "Source of the events" } }, @@ -1902,6 +1909,44 @@ "required": ["kind"], "type": "object" }, + "PersonsNode": { + "additionalProperties": false, + "properties": { + "cohort": { + "type": "number" + }, + "distinctId": { + "type": "string" + }, + "fixedProperties": { + "description": "Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person)", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, + "kind": { + "const": "PersonsNode", + "type": "string" + }, + "properties": { + "description": "Properties configurable in the interface", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, + "response": { + "description": "Cached query response", + "type": "object" + }, + "search": { + "type": "string" + } + }, + "required": ["kind"], + "type": "object" + }, "PropertyFilterValue": { "anyOf": [ { @@ -1997,6 +2042,9 @@ { "$ref": "#/definitions/ActionsNode" }, + { + "$ref": "#/definitions/PersonsNode" + }, { "$ref": "#/definitions/DataTableNode" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 52426868133d3..9871720bf2a5d 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -23,6 +23,7 @@ export enum NodeKind { // Data nodes EventsNode = 'EventsNode', ActionsNode = 'ActionsNode', + PersonsNode = 'PersonsNode', // Interface nodes DataTableNode = 'DataTableNode', @@ -41,6 +42,7 @@ export type QuerySchema = // Data nodes (see utils.ts) | EventsNode | ActionsNode + | PersonsNode // Interface nodes | DataTableNode @@ -101,12 +103,23 @@ export interface ActionsNode extends EntityNode { id: number } +export interface PersonsNode extends DataNode { + kind: NodeKind.PersonsNode + search?: string + cohort?: number + distinctId?: string + /** Properties configurable in the interface */ + properties?: AnyPropertyFilter[] + /** Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person) */ + fixedProperties?: AnyPropertyFilter[] +} + // Data table node export interface DataTableNode extends Node { kind: NodeKind.DataTableNode /** Source of the events */ - source: EventsNode + source: EventsNode | PersonsNode /** Columns shown in the table */ columns?: DataTableStringColumn[] /** Include an event filter above the table (default: true) */ diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index 0d975d942467a..35909be0f2542 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -12,10 +12,11 @@ import { Node, NodeKind, InsightQueryNode, + PersonsNode, } from '~/queries/schema' -export function isDataNode(node?: Node): node is EventsNode | ActionsNode { - return isEventsNode(node) +export function isDataNode(node?: Node): node is EventsNode | ActionsNode | PersonsNode { + return isEventsNode(node) || isActionsNode(node) || isPersonsNode(node) } export function isEventsNode(node?: Node): node is EventsNode { @@ -26,6 +27,10 @@ export function isActionsNode(node?: Node): node is ActionsNode { return node?.kind === NodeKind.ActionsNode } +export function isPersonsNode(node?: Node): node is PersonsNode { + return node?.kind === NodeKind.PersonsNode +} + export function isDataTableNode(node?: Node): node is DataTableNode { return node?.kind === NodeKind.DataTableNode } diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index c9e34dee2a80d..2ddbc36c350cb 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -2,7 +2,7 @@ import { useActions, useValues } from 'kea' import { eventsSceneLogic } from 'scenes/events/eventsSceneLogic' import { Query } from '~/queries/Query/Query' import { DataTableNode, NodeKind } from '~/queries/schema' -import { isDataTableNode } from '~/queries/utils' +import { isDataTableNode, isEventsNode } from '~/queries/utils' import { objectsEqual } from 'lib/utils' export function EventsScene(): JSX.Element { @@ -30,7 +30,7 @@ export function EventsScene(): JSX.Element { { - if (isDataTableNode(newQuery)) { + if (isDataTableNode(newQuery) && isEventsNode(newQuery.source) && isEventsNode(query.source)) { if (!objectsEqual(newQuery.source.properties ?? [], query.source.properties ?? [])) { setProperties(newQuery.source.properties ?? []) } From 3a690830c809ecde00f90eaa115a202065989128 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 15:39:48 +0100 Subject: [PATCH 62/90] persons search --- frontend/src/queries/examples.ts | 1 + .../nodes/DataTable/DataTable.examples.ts | 4 +-- .../src/queries/nodes/DataTable/DataTable.tsx | 7 +++- .../queries/nodes/DataTable/dataTableLogic.ts | 1 + .../nodes/PersonsNode/PersonsSearch.tsx | 34 +++++++++++++++++++ frontend/src/queries/schema.json | 8 +++-- frontend/src/queries/schema.ts | 6 ++-- 7 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 91e8a201b6a0b..1bc987d1481a0 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -65,6 +65,7 @@ const PersonsTable: DataTableNode = { const PersonsTableFull: DataTableNode = { ...PersonsTable, + showSearch: true, showPropertyFilter: true, showExport: true, showReload: true, diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index 5ee238ddf6cbc..7ec2f2daba8d0 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -46,7 +46,7 @@ const ShowFilters: DataTableNode = { limit: 100, }, columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], - showEventFilter: true, + showSearch: true, showPropertyFilter: true, } @@ -78,7 +78,7 @@ const ShowAllTheThings: DataTableNode = { showExport: true, showReload: true, showColumnConfigurator: true, - showEventFilter: true, + showSearch: true, showPropertyFilter: true, showEventsBufferWarning: true, } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index fa232d2f23d22..dec5e99b68c39 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -26,6 +26,7 @@ import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/Sessi import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' +import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' interface DataTableProps { query: DataTableNode @@ -58,6 +59,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem const { showActions, + showSearch, showEventFilter, showPropertyFilter, showReload, @@ -90,7 +92,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem const dataSource = (response as null | EventsNode['response'])?.results ?? [] const setQuerySource = (source: EventsNode | PersonsNode): void => setQuery?.({ ...query, source }) - const showFilters = showEventFilter || showPropertyFilter + const showFilters = showSearch || showEventFilter || showPropertyFilter const showTools = showReload || showExport || showColumnConfigurator const inlineRow = showFilters ? 1 : showTools ? 2 : 0 @@ -103,6 +105,9 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem {showEventFilter && isEventsNode(query.source) && ( )} + {showSearch && isPersonsNode(query.source) && ( + + )} {showPropertyFilter && isEventsNode(query.source) && ( )} diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 6965a824d96bf..c294e2d76fcc0 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -49,6 +49,7 @@ export const dataTableLogic = kea([ columns: columns, showPropertyFilter: query.showPropertyFilter ?? false, showEventFilter: query.showEventFilter ?? false, + showSearch: query.showSearch ?? false, showActions: query.showActions ?? true, showExport: query.showExport ?? false, showReload: query.showReload ?? false, diff --git a/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx new file mode 100644 index 0000000000000..be4bc642461ae --- /dev/null +++ b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx @@ -0,0 +1,34 @@ +import { PersonsNode } from '~/queries/schema' +import { LemonInput } from 'lib/components/LemonInput/LemonInput' +import { IconInfo } from 'lib/components/icons' +import { Tooltip } from 'lib/components/Tooltip' + +interface PersonSearchProps { + query: PersonsNode + setQuery?: (node: PersonsNode) => void +} + +export function PersonsSearch({ query, setQuery }: PersonSearchProps): JSX.Element { + return ( +
+ setQuery?.({ ...query, search: value })} + /> + + Search by email or Distinct ID. Email will match partially, for example: "@gmail.com". Distinct + ID needs to match exactly. + + } + > + + +
+ ) +} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index dc6b899db410b..afa9b70d6affb 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1286,7 +1286,7 @@ "type": "boolean" }, "showEventFilter": { - "description": "Include an event filter above the table (default: true)", + "description": "Include an event filter above the table", "type": "boolean" }, "showEventsBufferWarning": { @@ -1298,13 +1298,17 @@ "type": "boolean" }, "showPropertyFilter": { - "description": "Include a property filter above the table (default: true)", + "description": "Include a property filter above the table", "type": "boolean" }, "showReload": { "description": "Show a reload button", "type": "boolean" }, + "showSearch": { + "description": "Include a free text search field", + "type": "boolean" + }, "source": { "anyOf": [ { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 9871720bf2a5d..75e603f835eb9 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -122,9 +122,11 @@ export interface DataTableNode extends Node { source: EventsNode | PersonsNode /** Columns shown in the table */ columns?: DataTableStringColumn[] - /** Include an event filter above the table (default: true) */ + /** Include an event filter above the table */ showEventFilter?: boolean - /** Include a property filter above the table (default: true) */ + /** Include a free text search field */ + showSearch?: boolean + /** Include a property filter above the table */ showPropertyFilter?: boolean /** Show the kebab menu at the end of the row */ showActions?: boolean From ec213ff61b5c444651bd9a6be25b9b4c1afccd27 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:11:13 +0100 Subject: [PATCH 63/90] data table on persons scene --- .../src/queries/nodes/DataTable/DataTable.tsx | 1 + frontend/src/scenes/appScenes.ts | 2 +- frontend/src/scenes/persons/PersonsScene.tsx | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 frontend/src/scenes/persons/PersonsScene.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index dec5e99b68c39..e2f9857366123 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -150,6 +150,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem className="DataTable" loading={responseLoading && !nextDataLoading && !newDataLoading} columns={lemonColumns} + key={lemonColumns.join('::')} dataSource={dataSource} expandable={ expandable diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 48db5e12dda78..87021021e36b6 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -22,7 +22,7 @@ export const appScenes: Record any> = { [Scene.SessionRecording]: () => import('./session-recordings/detail/SessionRecordingDetail'), [Scene.SessionRecordingPlaylist]: () => import('./session-recordings/playlist/SessionRecordingsPlaylist'), [Scene.Person]: () => import('./persons/Person'), - [Scene.Persons]: () => import('./persons/Persons'), + [Scene.Persons]: () => import('./persons/PersonsScene'), [Scene.Groups]: () => import('./groups/Groups'), [Scene.Group]: () => import('./groups/Group'), [Scene.Action]: () => import('./actions/Action'), // TODO diff --git a/frontend/src/scenes/persons/PersonsScene.tsx b/frontend/src/scenes/persons/PersonsScene.tsx new file mode 100644 index 0000000000000..55045adc4de6b --- /dev/null +++ b/frontend/src/scenes/persons/PersonsScene.tsx @@ -0,0 +1,45 @@ +import { SceneExport } from 'scenes/sceneTypes' +import { useValues } from 'kea' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { Persons } from 'scenes/persons/Persons' +import { PersonPageHeader } from 'scenes/persons/PersonPageHeader' + +export const scene: SceneExport = { + component: PersonsScene, +} + +export function PersonsScene(): JSX.Element { + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + return ( + <> + + {featureDataExploration ? ( + + ) : ( + + )} + + ) +} From 8edb48b8537e9aaf20eb83de437291ce03100646 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:26:04 +0100 Subject: [PATCH 64/90] default columns depend on source node kind --- frontend/src/queries/examples.ts | 25 +++++++-------- .../ColumnConfigurator/ColumnConfigurator.tsx | 9 ++++-- .../src/queries/nodes/DataTable/DataTable.tsx | 4 +-- .../nodes/DataTable/DataTableExport.tsx | 4 +-- .../queries/nodes/DataTable/dataTableLogic.ts | 11 ++++--- .../src/queries/nodes/DataTable/defaults.ts | 16 ++++++++-- .../queries/nodes/DataTable/renderColumn.tsx | 2 +- frontend/src/scenes/cohorts/CohortEdit.tsx | 31 +++++++++++++++++-- 8 files changed, 73 insertions(+), 29 deletions(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 1bc987d1481a0..f4237d0f220c6 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -1,30 +1,29 @@ // This file contains example queries, used in storybook and in the /query interface. import { - EventsNode, + ActionsNode, DataTableNode, + EventsNode, + FunnelsQuery, LegacyQuery, + LifecycleQuery, Node, NodeKind, - TrendsQuery, - FunnelsQuery, - RetentionQuery, - ActionsNode, PathsQuery, - StickinessQuery, - LifecycleQuery, PersonsNode, + RetentionQuery, + StickinessQuery, + TrendsQuery, } from '~/queries/schema' import { ChartDisplayType, + FilterLogicalOperator, InsightType, PropertyFilterType, + PropertyGroupFilter, PropertyOperator, - // PropertyMathType, - FilterLogicalOperator, StepOrderValue, - PropertyGroupFilter, } from '~/types' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { ShownAsValue } from '~/lib/constants' const Events: EventsNode = { @@ -37,7 +36,7 @@ const Events: EventsNode = { const EventsTable: DataTableNode = { kind: NodeKind.DataTableNode, - columns: defaultDataTableStringColumns, + columns: defaultDataTableColumns({ kind: NodeKind.EventsNode }), source: Events, } const EventsTableFull: DataTableNode = { @@ -59,7 +58,7 @@ const Persons: PersonsNode = { const PersonsTable: DataTableNode = { kind: NodeKind.DataTableNode, - columns: ['id', 'person', 'properties.$geoip_country_name', 'properties.$browser', 'created_at'], + columns: defaultDataTableColumns({ kind: NodeKind.PersonsNode }), source: Persons, } diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index a5f5562bbfbf1..4e4f06ab6b14b 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -21,8 +21,8 @@ import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { TeamMembershipLevel } from 'lib/constants' import { useState } from 'react' import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic' -import { defaultDataTableStringColumns } from '../defaults' -import { DataTableNode } from '~/queries/schema' +import { defaultDataTableColumns } from '../defaults' +import { DataTableNode, NodeKind } from '~/queries/schema' import { LemonModal } from 'lib/components/LemonModal' import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterButton' import { PropertyFilterType } from '~/types' @@ -165,7 +165,10 @@ function ColumnConfiguratorModal(): JSX.Element { footer={ <>
- setColumns(defaultDataTableStringColumns)}> + setColumns(defaultDataTableColumns({ kind: NodeKind.EventsNode }))} + > Reset to defaults
diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index e2f9857366123..1dee99fb4c44b 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -18,7 +18,7 @@ import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/dataTableLogic' import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' import { teamLogic } from 'scenes/teamLogic' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' @@ -52,7 +52,7 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem } = useValues(dataNodeLogic(dataNodeLogicProps)) const { currentTeam } = useValues(teamLogic) - const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableStringColumns + const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableColumns(query.source) const dataTableLogicProps: DataTableLogicProps = { query, key, defaultColumns } const { columns, queryWithDefaults } = useValues(dataTableLogic(dataTableLogicProps)) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 192c3df84ffc3..ca28618e38379 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -4,7 +4,7 @@ import { Popconfirm } from 'antd' import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' import { DataTableNode } from '~/queries/schema' -import { defaultDataTableStringColumns } from '~/queries/nodes/DataTable/defaults' +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { getEventsEndpoint, getPersonsEndpoint } from '~/queries/query' @@ -29,7 +29,7 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void } if (onlySelectedColumns) { - exportContext['columns'] = (query.columns ?? defaultDataTableStringColumns)?.flatMap( + exportContext['columns'] = (query.columns ?? defaultDataTableColumns(query.source))?.flatMap( (c) => columnMapping[c] || c ) } diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index c294e2d76fcc0..1bd2ef6a55c1e 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -1,7 +1,7 @@ import { actions, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' import type { dataTableLogicType } from './dataTableLogicType' import { DataTableNode, DataTableStringColumn } from '~/queries/schema' -import { defaultDataTableStringColumns } from './defaults' +import { defaultDataTableColumns } from './defaults' export interface DataTableLogicProps { key: string @@ -16,7 +16,9 @@ export const dataTableLogic = kea([ actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), reducers(({ props }) => ({ storedColumns: [ - (props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns) as DataTableStringColumn[], + (props.query.columns ?? + props.defaultColumns ?? + defaultDataTableColumns(props.query.source)) as DataTableStringColumn[], { setColumns: (_, { columns }) => columns }, ], })), @@ -61,8 +63,9 @@ export const dataTableLogic = kea([ ], }), propsChanged(({ actions, props }, oldProps) => { - const newColumns = props.query.columns ?? props.defaultColumns ?? defaultDataTableStringColumns - const oldColumns = oldProps.query.columns ?? oldProps.defaultColumns ?? defaultDataTableStringColumns + const newColumns = props.query.columns ?? props.defaultColumns ?? defaultDataTableColumns(props.query.source) + const oldColumns = + oldProps.query.columns ?? oldProps.defaultColumns ?? defaultDataTableColumns(oldProps.query.source) if (JSON.stringify(newColumns) !== JSON.stringify(oldColumns)) { actions.setColumns(newColumns) } diff --git a/frontend/src/queries/nodes/DataTable/defaults.ts b/frontend/src/queries/nodes/DataTable/defaults.ts index c90315ff7c2a3..f9871c919d736 100644 --- a/frontend/src/queries/nodes/DataTable/defaults.ts +++ b/frontend/src/queries/nodes/DataTable/defaults.ts @@ -1,9 +1,21 @@ -import { DataTableStringColumn } from '~/queries/schema' +import { DataNode, DataTableStringColumn, NodeKind } from '~/queries/schema' -export const defaultDataTableStringColumns: DataTableStringColumn[] = [ +export const defaultDataTableEventColumns: DataTableStringColumn[] = [ 'event', 'person', 'url', 'properties.$lib', 'timestamp', ] + +export const defaultDataTablePersonColumns: DataTableStringColumn[] = [ + 'person', + 'id', + 'created_at', + 'properties.$geoip_country_name', + 'properties.$browser', +] + +export function defaultDataTableColumns(query: DataNode): DataTableStringColumn[] { + return query.kind === NodeKind.PersonsNode ? defaultDataTablePersonColumns : defaultDataTableEventColumns +} diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 5283b8c832baf..8dee5ccc43cd0 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -37,7 +37,7 @@ export function renderColumn( } else if (key.startsWith('properties.') || key === 'url') { const propertyKey = key === 'url' ? (record.properties['$screen_name'] ? '$screen_name' : '$current_url') : key.substring(11) - if (setQuery && (isEventsNode(query.source) || isPersonsNode(query.source))) { + if (setQuery && (isEventsNode(query.source) || isPersonsNode(query.source)) && query.showPropertyFilter) { const newProperty: AnyPropertyFilter = { key: propertyKey, value: record.properties[propertyKey], diff --git a/frontend/src/scenes/cohorts/CohortEdit.tsx b/frontend/src/scenes/cohorts/CohortEdit.tsx index 96a75aff458fd..9e87a85c8ad45 100644 --- a/frontend/src/scenes/cohorts/CohortEdit.tsx +++ b/frontend/src/scenes/cohorts/CohortEdit.tsx @@ -12,7 +12,7 @@ import { LemonInput } from 'lib/components/LemonInput/LemonInput' import { Tooltip } from 'lib/components/Tooltip' import { LemonSelect } from 'lib/components/LemonSelect' import { COHORT_TYPE_OPTIONS } from 'scenes/cohorts/CohortFilters/constants' -import { CohortTypeEnum } from 'lib/constants' +import { CohortTypeEnum, FEATURE_FLAGS } from 'lib/constants' import { AvailableFeature } from '~/types' import { LemonTextArea } from 'lib/components/LemonTextArea/LemonTextArea' import Dragger from 'antd/lib/upload/Dragger' @@ -25,6 +25,9 @@ import { Persons } from 'scenes/persons/Persons' import { LemonLabel } from 'lib/components/LemonLabel/LemonLabel' import { Form } from 'kea-forms' import { NotFound } from 'lib/components/NotFound' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' export function CohortEdit({ id }: CohortLogicProps): JSX.Element { const logicProps = { id } @@ -33,6 +36,8 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { const { cohort, cohortLoading, cohortMissing } = useValues(logic) const { hasAvailableFeature } = useValues(userLogic) const isNewCohort = cohort.id === 'new' || cohort.id === undefined + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] if (cohortMissing) { return @@ -193,7 +198,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { )} - {!isNewCohort && ( + {typeof cohort.id === 'number' && ( <>
@@ -204,6 +209,28 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { We're recalculating who belongs to this cohort. This could take up to a couple of minutes.
+ ) : featureDataExploration ? ( + ) : ( )} From ad7f21d9bbaa75cc3e55b8fbfdd9c6085591a225 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:33:38 +0100 Subject: [PATCH 65/90] add to storybook --- .../nodes/DataTable/DataTable.examples.ts | 13 ++++- .../nodes/DataTable/DataTable.stories.tsx | 5 ++ .../DataTable/__mocks__/PersonsNode.json | 58 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index 7ec2f2daba8d0..218ae85de27b3 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -1,5 +1,6 @@ import { DataTableNode, NodeKind } from '~/queries/schema' import { PropertyFilterType, PropertyOperator } from '~/types' +import { defaultDataTablePersonColumns } from '~/queries/nodes/DataTable/defaults' const AllDefaults: DataTableNode = { kind: NodeKind.DataTableNode, @@ -83,4 +84,14 @@ const ShowAllTheThings: DataTableNode = { showEventsBufferWarning: true, } -export const examples = { AllDefaults, Minimalist, ManyColumns, ShowFilters, ShowTools, ShowAllTheThings } +const Persons: DataTableNode = { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.PersonsNode }, + columns: defaultDataTablePersonColumns, + showSearch: true, + showPropertyFilter: true, + showExport: true, + showReload: true, +} + +export const examples = { AllDefaults, Minimalist, ManyColumns, ShowFilters, ShowTools, ShowAllTheThings, Persons } diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx index c4b9fe39bc2e5..7da3390cf7f90 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -5,6 +5,7 @@ import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' import { examples } from './DataTable.examples' import { mswDecorator } from '~/mocks/browser' import events from './__mocks__/EventsNode.json' +import persons from './__mocks__/PersonsNode.json' export default { title: 'Queries/DataTable', @@ -22,6 +23,7 @@ export default { mswDecorator({ get: { '/api/projects/:projectId/events': events, + '/api/projects/:projectId/persons': persons, }, }), ], @@ -57,3 +59,6 @@ ShowTools.args = { query: examples['ShowTools'] } export const ShowAllTheThings = BasicTemplate.bind({}) ShowAllTheThings.args = { query: examples['ShowAllTheThings'] } + +export const Persons = BasicTemplate.bind({}) +Persons.args = { query: examples['Persons'] } diff --git a/frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json b/frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json new file mode 100644 index 0000000000000..6220c72e6487a --- /dev/null +++ b/frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json @@ -0,0 +1,58 @@ +{ + "results": [ + { + "type": "person", + "id": "0184ae06-9d4a-0000-2b26-a771725cc751", + "uuid": "0184ae06-9d4a-0000-2b26-a771725cc751", + "created_at": "2022-11-25T09:02:59.900000Z", + "properties": { + "$os": "Mac OS X", + "email": "marius@posthog.com", + "$browser": "Chrome", + "$referrer": "http://localhost:8000/events", + "$initial_os": "Mac OS X", + "$geoip_latitude": -33.8715, + "$browser_version": 107, + "$geoip_city_name": "Sydney", + "$geoip_longitude": 151.2006, + "$geoip_time_zone": "Australia/Sydney", + "$initial_browser": "Chrome", + "$initial_pathname": "/ingestion", + "$initial_referrer": "http://localhost:8000/dashboard", + "$referring_domain": "localhost:8000", + "$geoip_postal_code": "2000", + "$geoip_country_code": "AU", + "$geoip_country_name": "Australia", + "$initial_current_url": "http://localhost:8000/ingestion", + "$initial_device_type": "Desktop", + "$geoip_continent_code": "OC", + "$geoip_continent_name": "Oceania", + "$initial_geoip_latitude": -33.8715, + "$initial_browser_version": 107, + "$initial_geoip_city_name": "Sydney", + "$initial_geoip_longitude": 151.2006, + "$initial_geoip_time_zone": "Australia/Sydney", + "$geoip_subdivision_1_code": "NSW", + "$geoip_subdivision_1_name": "New South Wales", + "$initial_referring_domain": "localhost:8000", + "$initial_geoip_postal_code": "2000", + "$initial_geoip_country_code": "AU", + "$initial_geoip_country_name": "Australia", + "$initial_geoip_continent_code": "OC", + "$initial_geoip_continent_name": "Oceania", + "$initial_geoip_subdivision_1_code": "NSW", + "$initial_geoip_subdivision_1_name": "New South Wales" + }, + "is_identified": true, + "name": "marius@posthog.com", + "distinct_ids": [ + "184ae069d181757-0c7310c8ee02fe-18525635-201b88-184ae069d192e7c", + "FXFWiYV3IHFXVo2DuWESQmHvywBwAEIJpvr0KrHiM2J" + ], + "matched_recordings": [], + "value_at_data_point": null + } + ], + "next": null, + "previous": null +} From ca801a1e13097b2ac46a8f8f50c22b66028bca1f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:37:51 +0100 Subject: [PATCH 66/90] enable "load more" on persons --- frontend/src/queries/nodes/DataNode/dataNodeLogic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index a41921f0a4446..3e73efa12b039 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders' import type { dataNodeLogicType } from './dataNodeLogicType' import { DataNode, EventsNode } from '~/queries/schema' import { query } from '~/queries/query' -import { isEventsNode } from '~/queries/utils' +import { isEventsNode, isPersonsNode } from '~/queries/utils' import { subscriptions } from 'kea-subscriptions' import clsx from 'clsx' @@ -123,7 +123,7 @@ export const dataNodeLogic = kea([ (s) => [s.query, s.response], (query, response) => { return ( - isEventsNode(query) && + (isEventsNode(query) || isPersonsNode(query)) && (response as EventsNode['response'])?.next && ((response as EventsNode['response'])?.results?.length ?? 0) > 0 ) From d0832925707340fde78ff5425292d0fff12df89e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:41:35 +0100 Subject: [PATCH 67/90] copy distinct id --- frontend/src/queries/nodes/DataTable/renderColumn.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 8dee5ccc43cd0..38024d3b8b331 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -9,6 +9,7 @@ import { PersonHeader } from 'scenes/persons/PersonHeader' import { DataTableNode, QueryCustom } from '~/queries/schema' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' export function renderColumn( key: string, @@ -143,6 +144,16 @@ export function renderColumn( } else if (key.startsWith('custom.')) { const Component = custom?.[key.substring(7)]?.render return Component ? : '' + } else if (key === 'id' && isPersonsNode(query.source)) { + return ( + + {record[key]} + + ) } else { return String(record[key]) } From 7ea159a1841f64a69da66eb4f7b2e3c48461db3c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 16:51:49 +0100 Subject: [PATCH 68/90] inline editor for data nodes --- .../src/queries/nodes/DataNode/DataNode.tsx | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index ac5cb3cc1d8ea..a6b0289829e6e 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -1,13 +1,15 @@ import MonacoEditor from '@monaco-editor/react' import { useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { DataNode as DataNodeType } from '~/queries/schema' +import { DataNode as DataNodeType, DataTableNode, Node } from '~/queries/schema' import { useValues } from 'kea' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { Spinner } from 'lib/components/Spinner/Spinner' +import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' interface DataNodeProps { query: DataNodeType + setQuery?: (node: DataTableNode) => void } let uniqueNode = 0 @@ -18,19 +20,28 @@ export function DataNode(props: DataNodeProps): JSX.Element { const logic = dataNodeLogic({ ...props, key }) const { response, responseLoading } = useValues(logic) - return responseLoading ? ( - - ) : ( - - {({ height }) => ( - + return ( +
+
+ void} /> +
+ {responseLoading ? ( +
+ +
+ ) : ( + + {({ height }) => ( + + )} + )} - +
) } From 4b4831553323636f596aa311ea6c68cb27db561c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 09:19:24 +0100 Subject: [PATCH 69/90] replace a few more events tables --- frontend/src/queries/Query/Query.tsx | 8 ++-- .../queries/nodes/DataNode/dataNodeLogic.ts | 9 ++++- frontend/src/queries/query.ts | 3 +- frontend/src/queries/schema.ts | 5 +++ frontend/src/scenes/actions/Action.tsx | 33 ++++++++++++---- .../definition/DefinitionView.tsx | 37 +++++++++++++----- .../src/scenes/feature-flags/FeatureFlag.tsx | 21 +++++++++- frontend/src/scenes/groups/Group.tsx | 39 +++++++++++++++---- 8 files changed, 124 insertions(+), 31 deletions(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 02b8380a1b549..4c0feeaf5d899 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,14 +1,14 @@ import { isDataNode, isDataTableNode, isLegacyQuery, isInsightQueryNode } from '../utils' import { DataTable } from '~/queries/nodes/DataTable/DataTable' import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { Node } from '~/queries/schema' +import { Node, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' -export interface QueryProps { - query: Node | string - setQuery?: (node: Node) => void +export interface QueryProps { + query: T | string + setQuery?: (node: T) => void } export function Query(props: QueryProps): JSX.Element { diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 7c9b9e1122f21..22c005a023b80 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -87,7 +87,14 @@ export const dataNodeLogic = kea([ autoLoadToggled: [ false, // store the autoload toggle's state in localstorage, separately for each data node kind - { persist: true, storageKey: `queries.nodes.dataNodeLogic.autoLoadToggled.${props.query.kind}` }, + { + persist: true, + storageKey: [ + 'queries.nodes.dataNodeLogic.autoLoadToggled', + props.query.kind, + isEventsNode(props.query) && props.query.actionId ? 'action' : '', + ].join('.'), + }, { toggleAutoLoad: (state) => !state }, ], autoLoadStarted: [false, { startAutoLoad: () => true, stopAutoLoad: () => false }], diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 3f6c94377d1b3..16df5d2058460 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -22,8 +22,9 @@ export async function query( if (isEventsNode(query)) { return await api.events.list( { - properties: query.properties, + properties: [...(query.fixedProperties || []), ...(query.properties || [])], ...(query.event ? { event: query.event } : {}), + ...(query.actionId ? { action_id: query.actionId } : {}), before: query.before, after: query.after, }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 43736d575c958..c7ba6dc669b81 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -72,13 +72,18 @@ export interface EntityNode extends DataNode { math?: BaseMathType | PropertyMathType | CountPerActorMathType math_property?: string math_group_type_index?: 0 | 1 | 2 | 3 | 4 + /** Properties configurable in the interface */ properties?: AnyPropertyFilter[] + /** Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person) */ + fixedProperties?: AnyPropertyFilter[] } export interface EventsNode extends EntityNode { kind: NodeKind.EventsNode event?: string limit?: number + /** Show events matching a given action */ + actionId?: number /** Only fetch events that happened before this timestamp */ before?: string /** Only fetch events that happened after this timestamp */ diff --git a/frontend/src/scenes/actions/Action.tsx b/frontend/src/scenes/actions/Action.tsx index 25c330ba15871..a705a58e009b1 100644 --- a/frontend/src/scenes/actions/Action.tsx +++ b/frontend/src/scenes/actions/Action.tsx @@ -8,6 +8,10 @@ import { dayjs } from 'lib/dayjs' import { Spinner } from 'lib/components/Spinner/Spinner' import { SceneExport } from 'scenes/sceneTypes' import { actionLogic, ActionLogicProps } from 'scenes/actions/actionLogic' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export const scene: SceneExport = { logic: actionLogic, @@ -23,6 +27,9 @@ export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element { const { action, isComplete } = useValues(actionLogic) const { loadAction } = useActions(actionLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + return ( <> {(!id || action) && ( @@ -53,13 +60,25 @@ export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element { )}

- + {featureDataExploration ? ( + + ) : ( + + )}
) : (
diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx index f7cb92aa6b79e..b0e0fc0957d2d 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx @@ -27,6 +27,10 @@ import { NotFound } from 'lib/components/NotFound' import { IconPlayCircle } from 'lib/components/icons' import { combineUrl } from 'kea-router/lib/utils' import { urls } from 'scenes/urls' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' export const scene: SceneExport = { component: DefinitionView, @@ -50,6 +54,8 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { } = useValues(logic) const { setPageMode } = useActions(logic) const { hasAvailableFeature } = useValues(userLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] if (definitionLoading) { return @@ -58,6 +64,7 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { if (definitionMissing) { return } + return (
{mode === DefinitionPageMode.Edit ? ( @@ -201,15 +208,27 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { This is the list of recent events that match this definition.

- + {featureDataExploration ? ( + + ) : ( + + )}
)} diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 772d705d1cf78..e9f20e918b064 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -58,6 +58,8 @@ import { isPropertyFilterWithOperator } from 'lib/components/PropertyFilters/uti import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' import { ResourcePermission } from 'scenes/ResourcePermissionModal' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { NodeKind } from '~/queries/schema' +import { Query } from '~/queries/Query/Query' export const scene: SceneExport = { component: FeatureFlag, @@ -439,7 +441,24 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { } function ExposureTab({ id, featureFlagKey }: { id: string; featureFlagKey: string }): JSX.Element { - return ( + const { featureFlags } = useValues(enabledFeaturesLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + + return featureDataExploration ? ( + + ) : ( : @@ -70,14 +76,31 @@ export function Group(): JSX.Element { Events} key={PersonsTabType.EVENTS}> - + {featureDataExploration ? ( + + ) : ( + + )} Date: Mon, 5 Dec 2022 09:48:17 +0100 Subject: [PATCH 70/90] add custom columns, add web performance table --- frontend/src/queries/Query/Query.tsx | 10 +- .../src/queries/nodes/DataTable/DataTable.tsx | 10 +- .../queries/nodes/DataTable/dataTableLogic.ts | 7 +- .../queries/nodes/DataTable/renderColumn.tsx | 8 +- .../queries/nodes/DataTable/renderTitle.tsx | 5 +- frontend/src/queries/schema.json | 20 +++ frontend/src/queries/schema.ts | 3 + frontend/src/scenes/events/EventsTable.tsx | 8 +- .../src/scenes/performance/WebPerformance.tsx | 134 ++++++++++++------ 9 files changed, 148 insertions(+), 57 deletions(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 4c0feeaf5d899..de9b1d956cea4 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,18 +1,22 @@ import { isDataNode, isDataTableNode, isLegacyQuery, isInsightQueryNode } from '../utils' import { DataTable } from '~/queries/nodes/DataTable/DataTable' import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { Node, QuerySchema } from '~/queries/schema' +import { Node, QueryCustom, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' export interface QueryProps { + /** The query to render */ query: T | string + /** Set this if the user can update the query */ setQuery?: (node: T) => void + /** Custom components passed down to query nodes (e.g. custom table columns) */ + custom?: QueryCustom } export function Query(props: QueryProps): JSX.Element { - const { query, setQuery } = props + const { query, setQuery, custom } = props if (typeof query === 'string') { try { return @@ -24,7 +28,7 @@ export function Query(props: QueryProps): JSX.Element { if (isLegacyQuery(query)) { component = } else if (isDataTableNode(query)) { - component = + component = } else if (isDataNode(query)) { component = } else if (isInsightQueryNode(query)) { diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 2c5fdae8cfd23..17f1fe5195188 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode, Node } from '~/queries/schema' +import { DataTableNode, EventsNode, Node, QueryCustom } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -28,11 +28,13 @@ import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' interface DataTableProps { query: DataTableNode setQuery?: (node: DataTableNode) => void + /** Custom table columns */ + custom?: QueryCustom } let uniqueNode = 0 -export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { +export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Element { const [key] = useState(() => `DataTable.${uniqueNode++}`) const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key } @@ -66,9 +68,9 @@ export function DataTable({ query, setQuery }: DataTableProps): JSX.Element { const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ dataIndex: key as any, - title: renderTitle(key), + title: renderTitle(key, custom), render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(key, record, query, setQuery) + return renderColumn(key, record, query, setQuery, custom) }, })), ...(showActions diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index f8c7da71e2459..6fde375a7122a 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -27,7 +27,12 @@ export const dataTableLogic = kea([ // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' const topLevelFields = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] return storedColumns.map((column) => { - if (topLevelFields.includes(column) || column.includes('properties.')) { + if ( + topLevelFields.includes(column) || + column.startsWith('person.properties.') || + column.startsWith('properties.') || + column.startsWith('custom.') + ) { return column } else { return `properties.${column}` diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index dc6fec1fd6709..c81cbfd727a26 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -6,7 +6,7 @@ import { TZLabel } from 'lib/components/TZLabel' import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' -import { DataTableNode } from '~/queries/schema' +import { DataTableNode, QueryCustom } from '~/queries/schema' import { isEventsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' @@ -14,7 +14,8 @@ export function renderColumn( key: string, record: EventType, query: DataTableNode, - setQuery?: (node: DataTableNode) => void + setQuery?: (node: DataTableNode) => void, + custom?: QueryCustom ): JSX.Element | string { if (key === 'event') { if (record.event === '$autocapture') { @@ -129,6 +130,9 @@ export function renderColumn( ) + } else if (key.startsWith('custom.')) { + const Component = custom?.[key.substring(7)]?.render + return Component ? : '' } else { return String(record[key]) } diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index a494d54c89812..16e95f804e0a5 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -1,7 +1,8 @@ import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { QueryCustom } from '~/queries/schema' -export function renderTitle(key: string): JSX.Element | string { +export function renderTitle(key: string, custom?: QueryCustom): JSX.Element | string { if (key === 'timestamp') { return 'Time' } else if (key === 'event') { @@ -12,6 +13,8 @@ export function renderTitle(key: string): JSX.Element | string { return 'URL / Screen' } else if (key.startsWith('properties.')) { return + } else if (key.startsWith('custom.')) { + return custom?.[key.substring(7)]?.title ?? key.substring(7).replace('_', ' ') } else if (key.startsWith('person.properties.')) { // NOTE: PropertyFilterType.Event is not a mistake. PropertyKeyInfo only knows events vs elements ¯\_(ツ)_/¯ return diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index b394acb0e9b9f..bc0107bb04b18 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -8,6 +8,13 @@ "custom_name": { "type": "string" }, + "fixedProperties": { + "description": "Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person)", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, "id": { "type": "number" }, @@ -39,6 +46,7 @@ "type": "string" }, "properties": { + "description": "Properties configurable in the interface", "items": { "$ref": "#/definitions/AnyPropertyFilter" }, @@ -1421,6 +1429,10 @@ "EventsNode": { "additionalProperties": false, "properties": { + "actionId": { + "description": "Show events matching a given action", + "type": "number" + }, "after": { "description": "Only fetch events that happened after this timestamp", "type": "string" @@ -1435,6 +1447,13 @@ "event": { "type": "string" }, + "fixedProperties": { + "description": "Fixed properties in the query, can't be edited in the interface (e.g. scoping down by person)", + "items": { + "$ref": "#/definitions/AnyPropertyFilter" + }, + "type": "array" + }, "kind": { "const": "EventsNode", "type": "string" @@ -1466,6 +1485,7 @@ "type": "string" }, "properties": { + "description": "Properties configurable in the interface", "items": { "$ref": "#/definitions/AnyPropertyFilter" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index c7ba6dc669b81..f430aaf6e02cf 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -225,3 +225,6 @@ export interface BreakdownFilter { breakdown_group_type_index?: number | null aggregation_group_type_index?: number | undefined // Groups aggregation } + +/** Pass custom templates and other metadata to queries. Used for e.g. custom columns in the DataTable. */ +export type QueryCustom = Record JSX.Element }> diff --git a/frontend/src/scenes/events/EventsTable.tsx b/frontend/src/scenes/events/EventsTable.tsx index f7f0cc866a5f2..32153bf68a77c 100644 --- a/frontend/src/scenes/events/EventsTable.tsx +++ b/frontend/src/scenes/events/EventsTable.tsx @@ -472,11 +472,11 @@ export function EventsTable({ person: ['person.distinct_ids.0', 'person.properties.email'], } - return (selectedColumns === 'DEFAULT' ? defaultColumns.map((e) => e.key || '') : selectedColumns).flatMap( - (x) => { + return (selectedColumns === 'DEFAULT' ? defaultColumns.map((e) => e.key || '') : selectedColumns) + .flatMap((x) => { return columnMapping[x] || `properties.${x}` - } - ) + }) + .filter((c) => !c.startsWith('custom.')) }, [defaultColumns, selectedColumns]) return ( diff --git a/frontend/src/scenes/performance/WebPerformance.tsx b/frontend/src/scenes/performance/WebPerformance.tsx index d346309dee09c..84d869af3c83c 100644 --- a/frontend/src/scenes/performance/WebPerformance.tsx +++ b/frontend/src/scenes/performance/WebPerformance.tsx @@ -10,6 +10,10 @@ import { useActions, useValues } from 'kea' import { WebPerformanceWaterfallChart } from 'scenes/performance/WebPerformanceWaterfallChart' import { IconPlay } from 'lib/components/icons' import { LemonButton } from '@posthog/lemon-ui' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { Query } from '~/queries/Query/Query' +import { EventsNode, NodeKind } from '~/queries/schema' /* * link to SessionRecording from table and chart @@ -27,53 +31,99 @@ export const webPerformancePropertyFilters: AnyPropertyFilter[] = [ const EventsWithPerformanceTable = (): JSX.Element => { const { setEventToDisplay } = useActions(webPerformanceLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] return ( <>
- - } - type="secondary" - size="small" - onClick={() => { - setEventToDisplay(event) - }} - > - View waterfall chart - -
- ) + {featureDataExploration ? ( + + columns: ['properties.$current_url', 'properties.$lib', 'timestamp', 'custom.waterfallButton'], + showReload: true, + showColumnConfigurator: false, + showExport: true, + showEventFilter: false, + showPropertyFilter: true, + showActions: false, + expandable: false, + }} + custom={{ + waterfallButton: { + title: '', + render: function RenderWaterfallButton({ + record: event, + }: { + record: Required['response']['results'][0] + }) { + return ( +
+ } + type="secondary" + size="small" + onClick={() => setEventToDisplay(event)} + > + View waterfall chart + +
+ ) + }, + }, + }} + /> + ) : ( + + } + type="secondary" + size="small" + onClick={() => { + setEventToDisplay(event) + }} + > + View waterfall chart + +
+ ) + }, + }, + ]} + /> + )} ) } From 98cde373605ebbf24dc6cfecf9b059aa799a83c8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 09:56:20 +0100 Subject: [PATCH 71/90] set query locally --- frontend/src/queries/Query/Query.tsx | 9 ++++++++- frontend/src/scenes/groups/Group.tsx | 1 + frontend/src/scenes/performance/WebPerformance.tsx | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index de9b1d956cea4..2b1506d01c7e2 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -5,18 +5,25 @@ import { Node, QueryCustom, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' +import { useState } from 'react' export interface QueryProps { /** The query to render */ query: T | string /** Set this if the user can update the query */ setQuery?: (node: T) => void + /** Can the query can still be edited locally if there is `setQuery` */ + setQueryLocally?: boolean /** Custom components passed down to query nodes (e.g. custom table columns) */ custom?: QueryCustom } export function Query(props: QueryProps): JSX.Element { - const { query, setQuery, custom } = props + const { query: globalQuery, setQuery: globalSetQuery, setQueryLocally, custom } = props + const [localQuery, localSetQuery] = useState(globalQuery) + const query = setQueryLocally ? localQuery : globalQuery + const setQuery = setQueryLocally ? localSetQuery : globalSetQuery + if (typeof query === 'string') { try { return diff --git a/frontend/src/scenes/groups/Group.tsx b/frontend/src/scenes/groups/Group.tsx index 721bb34497f0b..f47fbfa9871ea 100644 --- a/frontend/src/scenes/groups/Group.tsx +++ b/frontend/src/scenes/groups/Group.tsx @@ -90,6 +90,7 @@ export function Group(): JSX.Element { showEventFilter: true, showPropertyFilter: true, }} + setQueryLocally /> ) : ( { showActions: false, expandable: false, }} + setQueryLocally custom={{ waterfallButton: { title: '', From 2bf89f962e349433c4c881026e84fb785272176a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 12:24:45 +0100 Subject: [PATCH 72/90] persons page --- .../queries/nodes/DataNode/dataNodeLogic.ts | 10 +++--- frontend/src/queries/query.ts | 1 + frontend/src/queries/schema.json | 4 +++ frontend/src/queries/schema.ts | 2 ++ frontend/src/scenes/persons/Person.tsx | 36 +++++++++++++++---- frontend/src/types.ts | 2 +- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 22c005a023b80..dac99ede76b4c 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -6,6 +6,7 @@ import { query } from '~/queries/query' import { isEventsNode } from '~/queries/utils' import { subscriptions } from 'kea-subscriptions' import { objectsEqual } from 'lib/utils' +import clsx from 'clsx' export interface DataNodeLogicProps { key: string @@ -89,11 +90,10 @@ export const dataNodeLogic = kea([ // store the autoload toggle's state in localstorage, separately for each data node kind { persist: true, - storageKey: [ - 'queries.nodes.dataNodeLogic.autoLoadToggled', - props.query.kind, - isEventsNode(props.query) && props.query.actionId ? 'action' : '', - ].join('.'), + storageKey: clsx('queries.nodes.dataNodeLogic.autoLoadToggled', props.query.kind, { + action: isEventsNode(props.query) && props.query.actionId, + person: isEventsNode(props.query) && props.query.personId, + }), }, { toggleAutoLoad: (state) => !state }, ], diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 16df5d2058460..66100c9dec657 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -25,6 +25,7 @@ export async function query( properties: [...(query.fixedProperties || []), ...(query.properties || [])], ...(query.event ? { event: query.event } : {}), ...(query.actionId ? { action_id: query.actionId } : {}), + ...(query.personId ? { person_id: query.personId } : {}), before: query.before, after: query.after, }, diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index bc0107bb04b18..5c6315e1ee4d1 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1484,6 +1484,10 @@ "name": { "type": "string" }, + "personId": { + "description": "Show events for a given person", + "type": "string" + }, "properties": { "description": "Properties configurable in the interface", "items": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index f430aaf6e02cf..52426868133d3 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -84,6 +84,8 @@ export interface EventsNode extends EntityNode { limit?: number /** Show events matching a given action */ actionId?: number + /** Show events for a given person */ + personId?: string /** Only fetch events that happened before this timestamp */ before?: string /** Only fetch events that happened after this timestamp */ diff --git a/frontend/src/scenes/persons/Person.tsx b/frontend/src/scenes/persons/Person.tsx index 6b9127b80413e..ca9769e3e6ba5 100644 --- a/frontend/src/scenes/persons/Person.tsx +++ b/frontend/src/scenes/persons/Person.tsx @@ -27,6 +27,10 @@ import { SpinnerOverlay } from 'lib/components/Spinner/Spinner' import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' import { NotFound } from 'lib/components/NotFound' import { RelatedFeatureFlags } from './RelatedFeatureFlags' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' const { TabPane } = Tabs @@ -97,6 +101,8 @@ export function Person(): JSX.Element | null { } = useActions(personsLogic) const { groupsEnabled } = useValues(groupsAccessLogic) const { currentTeam } = useValues(teamLogic) + const { featureFlags } = useValues(featureFlagLogic) + const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] if (!person) { return personLoading ? : @@ -157,12 +163,30 @@ export function Person(): JSX.Element | null { /> Events} key={PersonsTabType.EVENTS}> - + {featureDataExploration ? ( + + ) : ( + + )} Recordings} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index cf41d14edf594..4ad07854ff9c7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -642,7 +642,7 @@ export interface FunnelStepRangeEntityFilter { export type EntityFilterTypes = EntityFilter | ActionFilter | null export interface PersonType { - id?: number + id?: string uuid?: string name?: string distinct_ids: string[] From 2a2c46427fc6111d67c75d4c5d6c00e32d1063ba Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 5 Dec 2022 12:39:48 +0100 Subject: [PATCH 73/90] fix person id types --- frontend/src/lib/api.ts | 4 ++-- .../ActivityLog/__mocks__/activityLogMocks.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index afd3bd4cb28b5..daa591a3380f3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -816,7 +816,7 @@ const api = { async update(id: number, person: Partial): Promise { return new ApiRequest().person(id).update({ data: person }) }, - async updateProperty(id: number, property: string, value: any): Promise { + async updateProperty(id: string, property: string, value: any): Promise { return new ApiRequest() .person(id) .withAction('update_property') @@ -827,7 +827,7 @@ const api = { }, }) }, - async deleteProperty(id: number, property: string): Promise { + async deleteProperty(id: string, property: string): Promise { return new ApiRequest() .person(id) .withAction('delete_property') diff --git a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts index dab8e2ab4b1c9..daf09e69cc983 100644 --- a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts +++ b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts @@ -427,7 +427,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ type: 'Person', source: [ { - id: 502746582, + id: '502746582', name: '1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4', uuid: '01819231-75a0-0000-467e-a4be57b44a37', created_at: '2022-06-23T20:12:03.828000Z', @@ -462,7 +462,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4'], }, { - id: 502725471, + id: '502725471', name: '1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7', uuid: '01819220-aa0b-0000-6992-fad9de0ea4dc', created_at: '2022-06-23T19:53:43.137000Z', @@ -497,7 +497,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7'], }, { - id: 502715718, + id: '502715718', name: '18192189287517-0e66a695611002-3297640-75300-18192189288790', uuid: '01819218-92f2-0000-7132-90a079001dc9', created_at: '2022-06-23T19:44:52.944000Z', @@ -532,7 +532,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['18192189287517-0e66a695611002-3297640-75300-18192189288790'], }, { - id: 502696118, + id: '502696118', name: '1819208c6ed32c-0e427ef09bbae-3297640-75300-1819208c6ee74b', uuid: '01819208-c74f-0000-75e7-7d3dc16da26b', created_at: '2022-06-23T19:27:37.771000Z', @@ -568,7 +568,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ }, ], target: { - id: 502792727, + id: '502792727', name: '1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7', uuid: '01819256-1d25-0000-4ed7-ea437589ada7', created_at: '2022-06-23T20:52:06.053733Z', From 38727fe5065feac9cc36bab002f0dbedc7ddd796 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 6 Dec 2022 23:56:53 +0100 Subject: [PATCH 74/90] make all queries locally editable if no setQuery and no readOnly --- frontend/src/lib/utils.tsx | 6 ++-- frontend/src/queries/Query/Query.tsx | 19 +++++++---- .../queries/nodes/DataTable/dataTableLogic.ts | 34 ++++++++++++------- frontend/src/scenes/groups/Group.tsx | 1 - .../src/scenes/performance/WebPerformance.tsx | 1 - frontend/src/scenes/persons/Person.tsx | 1 - 6 files changed, 36 insertions(+), 26 deletions(-) diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index f91c6c2006469..8794d6eb729c5 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1191,10 +1191,10 @@ export function roundToDecimal(value: number | null, places: number = 2): string return (Math.round(value * 100) / 100).toFixed(places) } -export function sortedKeys(object: Record): Record { - const newObject: Record = {} +export function sortedKeys = Record>(object: T): T { + const newObject: T = {} as T for (const key of Object.keys(object).sort()) { - newObject[key] = object[key] + newObject[key as keyof T] = object[key] } return newObject } diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 2b1506d01c7e2..e7b186fc8f71e 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -5,24 +5,29 @@ import { Node, QueryCustom, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' -import { useState } from 'react' +import { useEffect, useState } from 'react' export interface QueryProps { /** The query to render */ query: T | string - /** Set this if the user can update the query */ + /** Set this if you're controlling the query parameter */ setQuery?: (node: T) => void - /** Can the query can still be edited locally if there is `setQuery` */ - setQueryLocally?: boolean + /** Does not call setQuery, not even locally */ + readOnly?: boolean /** Custom components passed down to query nodes (e.g. custom table columns) */ custom?: QueryCustom } export function Query(props: QueryProps): JSX.Element { - const { query: globalQuery, setQuery: globalSetQuery, setQueryLocally, custom } = props + const { query: globalQuery, setQuery: globalSetQuery, readOnly, custom } = props const [localQuery, localSetQuery] = useState(globalQuery) - const query = setQueryLocally ? localQuery : globalQuery - const setQuery = setQueryLocally ? localSetQuery : globalSetQuery + useEffect(() => { + if (globalQuery !== localQuery) { + localSetQuery(globalQuery) + } + }, [globalQuery]) + const query = readOnly ? globalQuery : localQuery + const setQuery = readOnly ? undefined : globalSetQuery ?? localSetQuery if (typeof query === 'string') { try { diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 6fde375a7122a..de62efc714251 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -2,6 +2,7 @@ import { actions, kea, key, path, props, propsChanged, reducers, selectors } fro import type { dataTableLogicType } from './dataTableLogicType' import { DataTableNode, DataTableStringColumn } from '~/queries/schema' import { defaultDataTableStringColumns } from './defaults' +import { sortedKeys } from 'lib/utils' export interface DataTableLogicProps { key: string @@ -42,19 +43,26 @@ export const dataTableLogic = kea([ ], queryWithDefaults: [ (s) => [(_, props) => props.query, s.columns], - (query: DataTableNode, columns): Required => ({ - ...query, - columns: columns, - showPropertyFilter: query.showPropertyFilter ?? false, - showEventFilter: query.showEventFilter ?? false, - showActions: query.showActions ?? true, - showExport: query.showExport ?? false, - showReload: query.showReload ?? false, - showColumnConfigurator: query.showColumnConfigurator ?? false, - showEventsBufferWarning: query.showEventsBufferWarning ?? false, - expandable: query.expandable ?? true, - propertiesViaUrl: query.propertiesViaUrl ?? false, - }), + (query: DataTableNode, columns): Required => { + const { kind, columns: _columns, source, ...rest } = query + return { + kind, + columns: columns, + source, + ...sortedKeys({ + ...rest, + expandable: query.expandable ?? true, + propertiesViaUrl: query.propertiesViaUrl ?? false, + showPropertyFilter: query.showPropertyFilter ?? false, + showEventFilter: query.showEventFilter ?? false, + showActions: query.showActions ?? true, + showExport: query.showExport ?? false, + showReload: query.showReload ?? false, + showColumnConfigurator: query.showColumnConfigurator ?? false, + showEventsBufferWarning: query.showEventsBufferWarning ?? false, + }), + } + }, ], }), propsChanged(({ actions, props }, oldProps) => { diff --git a/frontend/src/scenes/groups/Group.tsx b/frontend/src/scenes/groups/Group.tsx index f47fbfa9871ea..721bb34497f0b 100644 --- a/frontend/src/scenes/groups/Group.tsx +++ b/frontend/src/scenes/groups/Group.tsx @@ -90,7 +90,6 @@ export function Group(): JSX.Element { showEventFilter: true, showPropertyFilter: true, }} - setQueryLocally /> ) : ( { showActions: false, expandable: false, }} - setQueryLocally custom={{ waterfallButton: { title: '', diff --git a/frontend/src/scenes/persons/Person.tsx b/frontend/src/scenes/persons/Person.tsx index ca9769e3e6ba5..59cd8cbad48a4 100644 --- a/frontend/src/scenes/persons/Person.tsx +++ b/frontend/src/scenes/persons/Person.tsx @@ -177,7 +177,6 @@ export function Person(): JSX.Element | null { showEventFilter: true, showPropertyFilter: true, }} - setQueryLocally /> ) : ( Date: Wed, 7 Dec 2022 00:18:08 +0100 Subject: [PATCH 75/90] these are uuids --- .../components/ActivityLog/__mocks__/activityLogMocks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts index daf09e69cc983..2a6c3c161b5e0 100644 --- a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts +++ b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts @@ -427,7 +427,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ type: 'Person', source: [ { - id: '502746582', + id: '01819231-75a0-0000-467e-a4be57b44a37', name: '1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4', uuid: '01819231-75a0-0000-467e-a4be57b44a37', created_at: '2022-06-23T20:12:03.828000Z', @@ -462,7 +462,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819231753016b-0b04ab5a3ab143-3297640-75300-181923175313d4'], }, { - id: '502725471', + id: '01819220-aa0b-0000-6992-fad9de0ea4dc', name: '1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7', uuid: '01819220-aa0b-0000-6992-fad9de0ea4dc', created_at: '2022-06-23T19:53:43.137000Z', @@ -497,7 +497,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['1819220a99e5ec-0005fee037f8d2-3297640-75300-1819220a99f6b7'], }, { - id: '502715718', + id: '01819218-92f2-0000-7132-90a079001dc9', name: '18192189287517-0e66a695611002-3297640-75300-18192189288790', uuid: '01819218-92f2-0000-7132-90a079001dc9', created_at: '2022-06-23T19:44:52.944000Z', @@ -532,7 +532,7 @@ export const personActivityResponseJson: ActivityLogItem[] = [ distinct_ids: ['18192189287517-0e66a695611002-3297640-75300-18192189288790'], }, { - id: '502696118', + id: '01819208-c74f-0000-75e7-7d3dc16da26b', name: '1819208c6ed32c-0e427ef09bbae-3297640-75300-1819208c6ee74b', uuid: '01819208-c74f-0000-75e7-7d3dc16da26b', created_at: '2022-06-23T19:27:37.771000Z', From cd120ce3852f894057ab14c59b385b7b51ffd9d8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 00:32:32 +0100 Subject: [PATCH 76/90] rename "custom" to "context" --- frontend/src/queries/Query/Query.tsx | 10 ++-- .../src/queries/nodes/DataTable/DataTable.tsx | 10 ++-- .../queries/nodes/DataTable/dataTableLogic.ts | 2 +- .../queries/nodes/DataTable/renderColumn.tsx | 8 +-- .../queries/nodes/DataTable/renderTitle.tsx | 9 ++-- frontend/src/queries/schema.ts | 12 ++++- .../src/scenes/performance/WebPerformance.tsx | 51 +++++++++++-------- 7 files changed, 59 insertions(+), 43 deletions(-) diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index e7b186fc8f71e..fec9fa89a38c6 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,7 +1,7 @@ import { isDataNode, isDataTableNode, isLegacyQuery, isInsightQueryNode } from '../utils' import { DataTable } from '~/queries/nodes/DataTable/DataTable' import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { Node, QueryCustom, QuerySchema } from '~/queries/schema' +import { Node, QueryContext, QuerySchema } from '~/queries/schema' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { LegacyInsightQuery } from '~/queries/nodes/LegacyInsightQuery/LegacyInsightQuery' import { InsightQuery } from '~/queries/nodes/InsightQuery/InsightQuery' @@ -14,12 +14,12 @@ export interface QueryProps { setQuery?: (node: T) => void /** Does not call setQuery, not even locally */ readOnly?: boolean - /** Custom components passed down to query nodes (e.g. custom table columns) */ - custom?: QueryCustom + /** Custom components passed down to a few query nodes (e.g. custom table columns) */ + context?: QueryContext } export function Query(props: QueryProps): JSX.Element { - const { query: globalQuery, setQuery: globalSetQuery, readOnly, custom } = props + const { query: globalQuery, setQuery: globalSetQuery, readOnly, context } = props const [localQuery, localSetQuery] = useState(globalQuery) useEffect(() => { if (globalQuery !== localQuery) { @@ -40,7 +40,7 @@ export function Query(props: QueryProps): JSX.Element { if (isLegacyQuery(query)) { component = } else if (isDataTableNode(query)) { - component = + component = } else if (isDataNode(query)) { component = } else if (isInsightQueryNode(query)) { diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 17f1fe5195188..e05a238e2284c 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode, Node, QueryCustom } from '~/queries/schema' +import { DataTableNode, EventsNode, Node, QueryContext } from '~/queries/schema' import { useState } from 'react' import { useValues, BindLogic } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -29,12 +29,12 @@ interface DataTableProps { query: DataTableNode setQuery?: (node: DataTableNode) => void /** Custom table columns */ - custom?: QueryCustom + context?: QueryContext } let uniqueNode = 0 -export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Element { +export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Element { const [key] = useState(() => `DataTable.${uniqueNode++}`) const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key } @@ -68,9 +68,9 @@ export function DataTable({ query, setQuery, custom }: DataTableProps): JSX.Elem const lemonColumns: LemonTableColumn[] = [ ...columns.map((key) => ({ dataIndex: key as any, - title: renderTitle(key, custom), + title: renderTitle(key, context), render: function RenderDataTableColumn(_: any, record: EventType) { - return renderColumn(key, record, query, setQuery, custom) + return renderColumn(key, record, query, setQuery, context) }, })), ...(showActions diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index de62efc714251..ca699a83a6660 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -32,7 +32,7 @@ export const dataTableLogic = kea([ topLevelFields.includes(column) || column.startsWith('person.properties.') || column.startsWith('properties.') || - column.startsWith('custom.') + column.startsWith('context.') ) { return column } else { diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index c81cbfd727a26..18df6ca900cb6 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -6,7 +6,7 @@ import { TZLabel } from 'lib/components/TZLabel' import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' -import { DataTableNode, QueryCustom } from '~/queries/schema' +import { DataTableNode, QueryContext } from '~/queries/schema' import { isEventsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' @@ -15,7 +15,7 @@ export function renderColumn( record: EventType, query: DataTableNode, setQuery?: (node: DataTableNode) => void, - custom?: QueryCustom + context?: QueryContext ): JSX.Element | string { if (key === 'event') { if (record.event === '$autocapture') { @@ -130,8 +130,8 @@ export function renderColumn( ) - } else if (key.startsWith('custom.')) { - const Component = custom?.[key.substring(7)]?.render + } else if (key.startsWith('context.columns.')) { + const Component = context?.columns?.[key.substring(16)]?.render return Component ? : '' } else { return String(record[key]) diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index 16e95f804e0a5..61ade6ee914eb 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -1,8 +1,9 @@ import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { QueryCustom } from '~/queries/schema' +import { QueryContext } from '~/queries/schema' -export function renderTitle(key: string, custom?: QueryCustom): JSX.Element | string { +export function renderTitle(key: string, context?: QueryContext): JSX.Element | string { + console.log(key) if (key === 'timestamp') { return 'Time' } else if (key === 'event') { @@ -13,8 +14,8 @@ export function renderTitle(key: string, custom?: QueryCustom): JSX.Element | st return 'URL / Screen' } else if (key.startsWith('properties.')) { return - } else if (key.startsWith('custom.')) { - return custom?.[key.substring(7)]?.title ?? key.substring(7).replace('_', ' ') + } else if (key.startsWith('context.columns.')) { + return context?.columns?.[key.substring(16)]?.title ?? key.substring(16).replace('_', ' ') } else if (key.startsWith('person.properties.')) { // NOTE: PropertyFilterType.Event is not a mistake. PropertyKeyInfo only knows events vs elements ¯\_(ツ)_/¯ return diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 52426868133d3..0cb0a72fb357a 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -228,5 +228,13 @@ export interface BreakdownFilter { aggregation_group_type_index?: number | undefined // Groups aggregation } -/** Pass custom templates and other metadata to queries. Used for e.g. custom columns in the DataTable. */ -export type QueryCustom = Record JSX.Element }> +/** Pass custom metadata to queries. Used for e.g. custom columns in the DataTable. */ +export interface QueryContext { + /** Column templates for the DataTable */ + columns: Record +} + +interface QueryContextColumn { + title?: string + render?: (props: { record: any }) => JSX.Element +} diff --git a/frontend/src/scenes/performance/WebPerformance.tsx b/frontend/src/scenes/performance/WebPerformance.tsx index 84d869af3c83c..0ca555984a9c3 100644 --- a/frontend/src/scenes/performance/WebPerformance.tsx +++ b/frontend/src/scenes/performance/WebPerformance.tsx @@ -45,7 +45,12 @@ const EventsWithPerformanceTable = (): JSX.Element => { kind: NodeKind.EventsNode, fixedProperties: webPerformancePropertyFilters, }, - columns: ['properties.$current_url', 'properties.$lib', 'timestamp', 'custom.waterfallButton'], + columns: [ + 'properties.$current_url', + 'properties.$lib', + 'timestamp', + 'context.columns.waterfallButton', + ], showReload: true, showColumnConfigurator: false, showExport: true, @@ -54,27 +59,29 @@ const EventsWithPerformanceTable = (): JSX.Element => { showActions: false, expandable: false, }} - custom={{ - waterfallButton: { - title: '', - render: function RenderWaterfallButton({ - record: event, - }: { - record: Required['response']['results'][0] - }) { - return ( -
- } - type="secondary" - size="small" - onClick={() => setEventToDisplay(event)} - > - View waterfall chart - -
- ) + context={{ + columns: { + waterfallButton: { + title: '', + render: function RenderWaterfallButton({ + record: event, + }: { + record: Required['response']['results'][0] + }) { + return ( +
+ } + type="secondary" + size="small" + onClick={() => setEventToDisplay(event)} + > + View waterfall chart + +
+ ) + }, }, }, }} From 0a0f03f7d9d79e3fe461bb5f9551228218512425 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:03:08 +0100 Subject: [PATCH 77/90] refactor stories --- frontend/src/queries/Query/Query.stories.tsx | 44 ------------------- .../src/queries/QueryRunner/QueryRunner.tsx | 30 +++++++++++++ .../nodes/DataNode/DataNode.stories.tsx | 36 +++++++++++++++ .../__mocks__/EventsNode.json | 0 .../__mocks__/PersonsNode.json | 0 .../nodes/DataTable/DataTable.stories.tsx | 38 ++++++---------- 6 files changed, 79 insertions(+), 69 deletions(-) delete mode 100644 frontend/src/queries/Query/Query.stories.tsx create mode 100644 frontend/src/queries/QueryRunner/QueryRunner.tsx create mode 100644 frontend/src/queries/nodes/DataNode/DataNode.stories.tsx rename frontend/src/queries/nodes/{DataTable => DataNode}/__mocks__/EventsNode.json (100%) rename frontend/src/queries/nodes/{DataTable => DataNode}/__mocks__/PersonsNode.json (100%) diff --git a/frontend/src/queries/Query/Query.stories.tsx b/frontend/src/queries/Query/Query.stories.tsx deleted file mode 100644 index e5bf828fb01b4..0000000000000 --- a/frontend/src/queries/Query/Query.stories.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react' -import { Query, QueryProps } from './Query' -import { useState } from 'react' -import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' -import { examples } from '../examples' - -export default { - title: 'Queries/Query', - component: Query, - parameters: { - chromatic: { disableSnapshot: false }, - layout: 'fullscreen', - options: { showPanel: false }, - viewMode: 'story', - }, - argTypes: { - query: { defaultValue: {} }, - }, -} as ComponentMeta - -const BasicTemplate: ComponentStory = (props: QueryProps) => { - const [queryString, setQueryString] = useState(JSON.stringify(props.query)) - - return ( - <> - -
- -
- - ) -} - -export const Events = BasicTemplate.bind({}) -Events.args = { query: examples['Events'] } - -export const EventsTable = BasicTemplate.bind({}) -EventsTable.args = { query: examples['EventsTable'] } - -export const LegacyTrendsQuery = BasicTemplate.bind({}) -LegacyTrendsQuery.args = { query: examples['LegacyTrendsQuery'] } - -export const InsightTrendsQuery = BasicTemplate.bind({}) -InsightTrendsQuery.args = { query: examples['InsightTrendsQuery'] } diff --git a/frontend/src/queries/QueryRunner/QueryRunner.tsx b/frontend/src/queries/QueryRunner/QueryRunner.tsx new file mode 100644 index 0000000000000..3f601949f91be --- /dev/null +++ b/frontend/src/queries/QueryRunner/QueryRunner.tsx @@ -0,0 +1,30 @@ +import { Query } from '~/queries/Query/Query' +import { useEffect, useState } from 'react' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { Node } from '~/queries/schema' + +export interface QueryRunnerProps { + /** The query to render */ + query: Node | string +} + +export function QueryRunner(props: QueryRunnerProps): JSX.Element { + const [queryString, setQueryString] = useState( + typeof props.query === 'string' ? props.query : JSON.stringify(props.query) + ) + useEffect(() => { + const newQueryString = typeof props.query === 'string' ? props.query : JSON.stringify(props.query) + if (newQueryString !== queryString) { + setQueryString(newQueryString) + } + }, [queryString]) + + return ( + <> + +
+ +
+ + ) +} diff --git a/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx new file mode 100644 index 0000000000000..8e0169a374aac --- /dev/null +++ b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx @@ -0,0 +1,36 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react' +import { QueryRunner } from '~/queries/QueryRunner/QueryRunner' +import { examples } from '~/queries/examples' +import { mswDecorator } from '~/mocks/browser' +import events from './__mocks__/EventsNode.json' +import persons from './__mocks__/PersonsNode.json' + +export default { + title: 'Queries/DataNode', + component: QueryRunner, + parameters: { + chromatic: { disableSnapshot: false }, + layout: 'fullscreen', + options: { showPanel: false }, + viewMode: 'story', + }, + argTypes: { + query: { defaultValue: {} }, + }, + decorators: [ + mswDecorator({ + get: { + '/api/projects/:projectId/events': events, + '/api/projects/:projectId/persons': persons, + }, + }), + ], +} as ComponentMeta + +const QueryTemplate: ComponentStory = QueryRunner + +export const Events = QueryTemplate.bind({}) +Events.args = { query: examples['Events'] } + +export const Persons = QueryTemplate.bind({}) +Persons.args = { query: examples['Persons'] } diff --git a/frontend/src/queries/nodes/DataTable/__mocks__/EventsNode.json b/frontend/src/queries/nodes/DataNode/__mocks__/EventsNode.json similarity index 100% rename from frontend/src/queries/nodes/DataTable/__mocks__/EventsNode.json rename to frontend/src/queries/nodes/DataNode/__mocks__/EventsNode.json diff --git a/frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json b/frontend/src/queries/nodes/DataNode/__mocks__/PersonsNode.json similarity index 100% rename from frontend/src/queries/nodes/DataTable/__mocks__/PersonsNode.json rename to frontend/src/queries/nodes/DataNode/__mocks__/PersonsNode.json diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx index 3d1f66a8a5bd9..8e5b3555fa9cd 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -1,11 +1,10 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' -import { Query, QueryProps } from '~/queries/Query/Query' -import { useState } from 'react' -import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { Query } from '~/queries/Query/Query' import { examples } from './DataTable.examples' import { mswDecorator } from '~/mocks/browser' -import events from './__mocks__/EventsNode.json' -import persons from './__mocks__/PersonsNode.json' +import events from '../DataNode/__mocks__/EventsNode.json' +import persons from '../DataNode/__mocks__/PersonsNode.json' +import { QueryRunner } from '~/queries/QueryRunner/QueryRunner' export default { title: 'Queries/DataTable', @@ -29,39 +28,28 @@ export default { ], } as ComponentMeta -const BasicTemplate: ComponentStory = (props: QueryProps) => { - const [queryString, setQueryString] = useState(JSON.stringify(props.query)) +const QueryTemplate: ComponentStory = QueryRunner - return ( - <> - -
- -
- - ) -} - -export const AllDefaults = BasicTemplate.bind({}) +export const AllDefaults = QueryTemplate.bind({}) AllDefaults.args = { query: examples['AllDefaults'] } -export const Minimalist = BasicTemplate.bind({}) +export const Minimalist = QueryTemplate.bind({}) Minimalist.args = { query: examples['Minimalist'] } -export const ManyColumns = BasicTemplate.bind({}) +export const ManyColumns = QueryTemplate.bind({}) ManyColumns.args = { query: examples['ManyColumns'] } -export const ShowFilters = BasicTemplate.bind({}) +export const ShowFilters = QueryTemplate.bind({}) ShowFilters.args = { query: examples['ShowFilters'] } -export const ShowTools = BasicTemplate.bind({}) +export const ShowTools = QueryTemplate.bind({}) ShowTools.args = { query: examples['ShowTools'] } -export const ShowAllTheThings = BasicTemplate.bind({}) +export const ShowAllTheThings = QueryTemplate.bind({}) ShowAllTheThings.args = { query: examples['ShowAllTheThings'] } -export const Persons = BasicTemplate.bind({}) +export const Persons = QueryTemplate.bind({}) Persons.args = { query: examples['Persons'] } -export const PersonsTable = BasicTemplate.bind({}) +export const PersonsTable = QueryTemplate.bind({}) PersonsTable.args = { query: examples['PersonsTable'] } From 1c2492668dcf3c2afb4a57964bc60e477fdc4172 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:10:19 +0100 Subject: [PATCH 78/90] rename InlineEditor to InlineEditorButton to avoid confusion --- frontend/src/queries/nodes/DataNode/DataNode.tsx | 4 ++-- frontend/src/queries/nodes/DataTable/DataTable.tsx | 11 +++++++---- .../Node/{InlineEditor.tsx => InlineEditorButton.tsx} | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) rename frontend/src/queries/nodes/Node/{InlineEditor.tsx => InlineEditorButton.tsx} (89%) diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index a6b0289829e6e..c272efb00084c 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -5,7 +5,7 @@ import { DataNode as DataNodeType, DataTableNode, Node } from '~/queries/schema' import { useValues } from 'kea' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { Spinner } from 'lib/components/Spinner/Spinner' -import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' +import { InlineEditorButton } from '~/queries/nodes/Node/InlineEditorButton' interface DataNodeProps { query: DataNodeType @@ -23,7 +23,7 @@ export function DataNode(props: DataNodeProps): JSX.Element { return (
- void} /> + void} />
{responseLoading ? (
diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 9befdd341f7f3..cebe112cf26fb 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -23,7 +23,7 @@ import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' -import { InlineEditor } from '~/queries/nodes/Node/InlineEditor' +import { InlineEditorButton } from '~/queries/nodes/Node/InlineEditorButton' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' @@ -117,7 +117,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele {inlineRow === 1 ? ( <>
- void} /> @@ -134,7 +134,10 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele )} {showExport && } {inlineRow === 2 ? ( - void} /> + void} + /> ) : null}
)} @@ -143,7 +146,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele )} {inlineRow === 0 ? (
- void} /> + void} />
) : null} void } -export function InlineEditor({ query, setQuery }: InlineEditorProps): JSX.Element { +export function InlineEditorButton({ query, setQuery }: InlineEditorButtonProps): JSX.Element { const [open, setOpen] = useState(false) return ( From 3b39dd578ef4cc4ecee3ddb2b5f4a09853b23672 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:11:48 +0100 Subject: [PATCH 79/90] not in use --- .../src/queries/nodes/DataNode/LoadNew.tsx | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 frontend/src/queries/nodes/DataNode/LoadNew.tsx diff --git a/frontend/src/queries/nodes/DataNode/LoadNew.tsx b/frontend/src/queries/nodes/DataNode/LoadNew.tsx deleted file mode 100644 index 406fa2421acc7..0000000000000 --- a/frontend/src/queries/nodes/DataNode/LoadNew.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { LemonButton } from 'lib/components/LemonButton' -import { IconRefresh } from 'lib/components/icons' -import { Spinner } from 'lib/components/Spinner/Spinner' - -export function LoadNew(): JSX.Element { - const { responseLoading } = useValues(dataNodeLogic) - const { loadNewData } = useActions(dataNodeLogic) - - return ( - : } - > - Load new events - - ) -} From fc8cf02d4d3b3bde2c58f77e04a6605f20e63982 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:12:57 +0100 Subject: [PATCH 80/90] fix wrong filters --- frontend/src/queries/nodes/DataTable/DataTable.examples.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index 794b027623750..7945789ac27d3 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -47,7 +47,7 @@ const ShowFilters: DataTableNode = { limit: 100, }, columns: ['event', 'person', 'properties.$lib', 'person.properties.email'], - showSearch: true, + showEventFilter: true, showPropertyFilter: true, } @@ -79,7 +79,7 @@ const ShowAllTheThings: DataTableNode = { showExport: true, showReload: true, showColumnConfigurator: true, - showSearch: true, + showEventFilter: true, showPropertyFilter: true, showEventsBufferWarning: true, } From eb095092a6cb88b95fe7b44a53ba3b8322035dd0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:15:54 +0100 Subject: [PATCH 81/90] add more hints --- frontend/src/queries/schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 911179be5e859..f2d2b1938b786 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -122,9 +122,9 @@ export interface DataTableNode extends Node { source: EventsNode | PersonsNode /** Columns shown in the table */ columns?: DataTableStringColumn[] - /** Include an event filter above the table */ + /** Include an event filter above the table (EventsNode only) */ showEventFilter?: boolean - /** Include a free text search field */ + /** Include a free text search field (PersonsNode only) */ showSearch?: boolean /** Include a property filter above the table */ showPropertyFilter?: boolean @@ -134,7 +134,7 @@ export interface DataTableNode extends Node { showExport?: boolean /** Show a reload button */ showReload?: boolean - /** Show a button to configure the table's columns */ + /** Show a button to configure the table's columns if possible */ showColumnConfigurator?: boolean /** Can expand row to show raw event data (default: true) */ expandable?: boolean From fcc3f0fbe831b6771aa7fc793a1dc36a9a361c82 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 14:29:05 +0100 Subject: [PATCH 82/90] persons scene url --- frontend/src/scenes/persons/PersonsScene.tsx | 30 ++-------- .../src/scenes/persons/personsSceneLogic.ts | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 frontend/src/scenes/persons/personsSceneLogic.ts diff --git a/frontend/src/scenes/persons/PersonsScene.tsx b/frontend/src/scenes/persons/PersonsScene.tsx index db918a7edf472..da80fdc1d18fb 100644 --- a/frontend/src/scenes/persons/PersonsScene.tsx +++ b/frontend/src/scenes/persons/PersonsScene.tsx @@ -1,11 +1,11 @@ import { SceneExport } from 'scenes/sceneTypes' -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' import { Persons } from 'scenes/persons/Persons' import { PersonPageHeader } from 'scenes/persons/PersonPageHeader' +import { personsSceneLogic } from 'scenes/persons/personsSceneLogic' export const scene: SceneExport = { component: PersonsScene, @@ -14,31 +14,13 @@ export const scene: SceneExport = { export function PersonsScene(): JSX.Element { const { featureFlags } = useValues(featureFlagLogic) const featureDataExploration = featureFlags[FEATURE_FLAGS.DATA_EXPLORATION_LIVE_EVENTS] + const { query } = useValues(personsSceneLogic) + const { setQuery } = useActions(personsSceneLogic) + return ( <> - {featureDataExploration ? ( - - ) : ( - - )} + {featureDataExploration ? : } ) } diff --git a/frontend/src/scenes/persons/personsSceneLogic.ts b/frontend/src/scenes/persons/personsSceneLogic.ts new file mode 100644 index 0000000000000..b738a7619c210 --- /dev/null +++ b/frontend/src/scenes/persons/personsSceneLogic.ts @@ -0,0 +1,58 @@ +import { actions, kea, path, reducers } from 'kea' + +import { actionToUrl, urlToAction } from 'kea-router' +import equal from 'fast-deep-equal' +import { DataTableNode, Node, NodeKind } from '~/queries/schema' +import { urls } from 'scenes/urls' +import { objectsEqual } from 'lib/utils' +import { lemonToast } from 'lib/components/lemonToast' + +import type { personsSceneLogicType } from './personsSceneLogicType' + +const getDefaultQuery = (): DataTableNode => ({ + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.PersonsNode }, + columns: ['person', 'id', 'created_at', 'properties.$geoip_country_name', 'properties.$browser'], + propertiesViaUrl: true, + showSearch: true, + showPropertyFilter: true, + showExport: true, + showReload: true, +}) + +export const personsSceneLogic = kea([ + path(['scenes', 'persons', 'personsSceneLogic']), + + actions({ setQuery: (query: Node) => ({ query }) }), + reducers({ query: [getDefaultQuery() as Node, { setQuery: (_, { query }) => query }] }), + + actionToUrl(({ values }) => ({ + setQuery: () => [ + urls.persons(), + {}, + objectsEqual(values.query, getDefaultQuery()) ? {} : { q: values.query }, + { replace: true }, + ], + })), + + urlToAction(({ actions, values }) => ({ + [urls.persons()]: (_, __, { q: queryParam }): void => { + if (!equal(queryParam, values.query)) { + // nothing in the URL + if (!queryParam) { + // set the default unless it's already there + if (!objectsEqual(values.query, getDefaultQuery())) { + actions.setQuery(getDefaultQuery()) + } + } else { + if (typeof queryParam === 'object') { + actions.setQuery(queryParam) + } else { + lemonToast.error('Invalid query in URL') + console.error({ queryParam }) + } + } + } + }, + })), +]) From 44a935fb91d2e69822d28a8d3b1bd7ee666b6f8d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 7 Dec 2022 15:36:29 +0100 Subject: [PATCH 83/90] extract person deletion modal --- .../src/queries/nodes/DataTable/DataTable.tsx | 3 + .../queries/nodes/DataTable/dataTableLogic.ts | 23 +------ .../queries/nodes/DataTable/renderColumn.tsx | 4 ++ .../queries/nodes/DataTable/renderTitle.tsx | 2 + .../nodes/PersonsNode/DeletePersonButton.tsx | 22 +++++++ frontend/src/scenes/persons/Person.tsx | 18 +++--- .../src/scenes/persons/PersonDeleteModal.tsx | 12 ++-- frontend/src/scenes/persons/PersonsTable.tsx | 8 ++- .../scenes/persons/personDeleteModalLogic.tsx | 62 +++++++++++++++++++ frontend/src/scenes/persons/personsLogic.tsx | 32 ---------- .../src/scenes/persons/personsSceneLogic.ts | 2 +- 11 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx create mode 100644 frontend/src/scenes/persons/personDeleteModalLogic.tsx diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index cebe112cf26fb..29779c62552ea 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -27,6 +27,7 @@ import { InlineEditorButton } from '~/queries/nodes/Node/InlineEditorButton' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' +import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' interface DataTableProps { query: DataTableNode @@ -172,7 +173,9 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele } /> {canLoadNextData && ((response as any).results.length > 0 || !responseLoading) && } + {/* TODO: this doesn't seem like the right solution... */} +
diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 1b54400ea4eac..299caf8d3092f 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -16,7 +16,7 @@ export const dataTableLogic = kea([ path(['queries', 'nodes', 'DataTable', 'dataTableLogic']), actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), reducers(({ props }) => ({ - storedColumns: [ + columns: [ (props.query.columns ?? props.defaultColumns ?? defaultDataTableColumns(props.query.source)) as DataTableStringColumn[], @@ -24,27 +24,6 @@ export const dataTableLogic = kea([ ], })), selectors({ - columns: [ - (s) => [s.storedColumns], - (storedColumns) => { - // This makes old stored columns (e.g. on the Team model) compatible with the new view that prepends 'properties.' - const topLevelFieldsEvents = ['event', 'timestamp', 'id', 'distinct_id', 'person', 'url'] - const topLevelFieldsPersons = ['id', 'distinct_ids', 'created_at', 'is_identified', 'name', 'person'] - return storedColumns.map((column) => { - if ( - topLevelFieldsEvents.includes(column) || - topLevelFieldsPersons.includes(column) || - column.startsWith('person.properties.') || - column.startsWith('properties.') || - column.startsWith('context.') - ) { - return column - } else { - return `properties.${column}` - } - }) - }, - ], queryWithDefaults: [ (s) => [(_, props) => props.query, s.columns], (query: DataTableNode, columns): Required => { diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index e2c1f0cd27a5b..c750942a06a2f 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -10,6 +10,7 @@ import { DataTableNode, QueryContext } from '~/queries/schema' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { DeletePersonButton } from '~/queries/nodes/PersonsNode/DeletePersonButton' export function renderColumn( key: string, @@ -141,6 +142,9 @@ export function renderColumn( ) + } else if (key === 'person.$delete' && isPersonsNode(query.source)) { + const personRecord = record as PersonType + return } else if (key.startsWith('context.columns.')) { const Component = context?.columns?.[key.substring(16)]?.render return Component ? : '' diff --git a/frontend/src/queries/nodes/DataTable/renderTitle.tsx b/frontend/src/queries/nodes/DataTable/renderTitle.tsx index 719b3f3428090..39b9e4c4b263e 100644 --- a/frontend/src/queries/nodes/DataTable/renderTitle.tsx +++ b/frontend/src/queries/nodes/DataTable/renderTitle.tsx @@ -17,6 +17,8 @@ export function renderTitle(key: string, context?: QueryContext): JSX.Element | return } else if (key.startsWith('context.columns.')) { return context?.columns?.[key.substring(16)]?.title ?? key.substring(16).replace('_', ' ') + } else if (key === 'person.$delete') { + return '' } else if (key.startsWith('person.properties.')) { // NOTE: PropertyFilterType.Event is not a mistake. PropertyKeyInfo only knows events vs elements ¯\_(ツ)_/¯ return diff --git a/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx b/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx new file mode 100644 index 0000000000000..40b3b53217ba6 --- /dev/null +++ b/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx @@ -0,0 +1,22 @@ +import { LemonButton } from 'lib/components/LemonButton' +import { IconDelete } from 'lib/components/icons' +import { useActions } from 'kea' +import { PersonType } from '~/types' +import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' + +interface DeletePersonButtonProps { + person: PersonType +} +export function DeletePersonButton({ person }: DeletePersonButtonProps): JSX.Element { + const { showPersonDeleteModal } = useActions(personDeleteModalLogic) + const { loadData } = useActions(dataNodeLogic) + return ( + showPersonDeleteModal(person, () => loadData())} + icon={} + status="danger" + size="small" + /> + ) +} diff --git a/frontend/src/scenes/persons/Person.tsx b/frontend/src/scenes/persons/Person.tsx index 59cd8cbad48a4..bc6a82f5e0478 100644 --- a/frontend/src/scenes/persons/Person.tsx +++ b/frontend/src/scenes/persons/Person.tsx @@ -31,6 +31,7 @@ import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' +import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' const { TabPane } = Tabs @@ -89,16 +90,11 @@ function PersonCaption({ person }: { person: PersonType }): JSX.Element { } export function Person(): JSX.Element | null { - const { person, personLoading, deletedPersonLoading, currentTab, splitMergeModalShown, urlId, distinctId } = - useValues(personsLogic) - const { - editProperty, - deleteProperty, - navigateToTab, - setSplitMergeModalShown, - showPersonDeleteModal, - setDistinctId, - } = useActions(personsLogic) + const { person, personLoading, currentTab, splitMergeModalShown, urlId, distinctId } = useValues(personsLogic) + const { loadPersons, editProperty, deleteProperty, navigateToTab, setSplitMergeModalShown, setDistinctId } = + useActions(personsLogic) + const { showPersonDeleteModal } = useActions(personDeleteModalLogic) + const { deletedPersonLoading } = useValues(personDeleteModalLogic) const { groupsEnabled } = useValues(groupsAccessLogic) const { currentTeam } = useValues(teamLogic) const { featureFlags } = useValues(featureFlagLogic) @@ -116,7 +112,7 @@ export function Person(): JSX.Element | null { buttons={
showPersonDeleteModal(person)} + onClick={() => showPersonDeleteModal(person, () => loadPersons())} disabled={deletedPersonLoading} loading={deletedPersonLoading} type="secondary" diff --git a/frontend/src/scenes/persons/PersonDeleteModal.tsx b/frontend/src/scenes/persons/PersonDeleteModal.tsx index 438dfea8ce781..7e3826e2c627d 100644 --- a/frontend/src/scenes/persons/PersonDeleteModal.tsx +++ b/frontend/src/scenes/persons/PersonDeleteModal.tsx @@ -1,12 +1,12 @@ import { useActions, useValues } from 'kea' -import { personsLogic } from './personsLogic' import { asDisplay } from './PersonHeader' import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { PersonType } from '~/types' +import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' export function PersonDeleteModal(): JSX.Element | null { - const { personDeleteModal } = useValues(personsLogic) - const { deletePerson, showPersonDeleteModal } = useActions(personsLogic) + const { personDeleteModal } = useValues(personDeleteModalLogic) + const { deletePerson, showPersonDeleteModal } = useActions(personDeleteModalLogic) return ( { - deletePerson({ person: personDeleteModal as PersonType, deleteEvents: true }) - showPersonDeleteModal(null) + deletePerson(personDeleteModal as PersonType, true) }} data-attr="delete-person-with-events" > @@ -53,8 +52,7 @@ export function PersonDeleteModal(): JSX.Element | null { type="primary" status="danger" onClick={() => { - deletePerson({ person: personDeleteModal as PersonType, deleteEvents: false }) - showPersonDeleteModal(null) + deletePerson(personDeleteModal as PersonType, false) }} data-attr="delete-person-no-events" > diff --git a/frontend/src/scenes/persons/PersonsTable.tsx b/frontend/src/scenes/persons/PersonsTable.tsx index 4e7ce0f1fbc2a..0b00bff9bf76e 100644 --- a/frontend/src/scenes/persons/PersonsTable.tsx +++ b/frontend/src/scenes/persons/PersonsTable.tsx @@ -8,8 +8,9 @@ import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/components/ import { LemonButton } from '@posthog/lemon-ui' import { IconDelete } from 'lib/components/icons' import { useActions } from 'kea' -import { personsLogic } from 'scenes/persons/personsLogic' import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' +import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' +import { personsLogic } from 'scenes/persons/personsLogic' interface PersonsTableType { people: PersonType[] @@ -30,7 +31,8 @@ export function PersonsTable({ loadNext, compact, }: PersonsTableType): JSX.Element { - const { showPersonDeleteModal } = useActions(personsLogic) + const { showPersonDeleteModal } = useActions(personDeleteModalLogic) + const { loadPersons } = useActions(personsLogic) const columns: LemonTableColumns = [ { @@ -72,7 +74,7 @@ export function PersonsTable({ render: function Render(_, person: PersonType) { return ( showPersonDeleteModal(person)} + onClick={() => showPersonDeleteModal(person, () => loadPersons())} icon={} status="danger" size="small" diff --git a/frontend/src/scenes/persons/personDeleteModalLogic.tsx b/frontend/src/scenes/persons/personDeleteModalLogic.tsx new file mode 100644 index 0000000000000..9435c665f6a2a --- /dev/null +++ b/frontend/src/scenes/persons/personDeleteModalLogic.tsx @@ -0,0 +1,62 @@ +import { actions, kea, props, reducers, path } from 'kea' +import api from 'lib/api' +import { PersonType } from '~/types' +import { toParams } from 'lib/utils' +import { asDisplay } from 'scenes/persons/PersonHeader' +import { lemonToast } from 'lib/components/lemonToast' +import type { personDeleteModalLogicType } from './personDeleteModalLogicType' +import { loaders } from 'kea-loaders' + +export interface PersonDeleteModalLogicProps { + person: PersonType +} +export type PersonDeleteCallback = (person: PersonType, deleteEvents: boolean) => void + +export const personDeleteModalLogic = kea([ + path(['scenes', 'persons', 'personDeleteModalLogic']), + props({} as PersonDeleteModalLogicProps), + actions({ + showPersonDeleteModal: (person: PersonType | null, callback?: PersonDeleteCallback) => ({ + person, + callback, + }), + deletePerson: (person: PersonType, deleteEvents: boolean) => ({ person, deleteEvents }), + }), + reducers({ + personDeleteModal: [ + null as PersonType | null, + { + showPersonDeleteModal: (_, { person }) => person, + }, + ], + personDeleteCallback: [ + null as PersonDeleteCallback | null, + { + showPersonDeleteModal: (_, { callback }) => callback ?? null, + }, + ], + }), + loaders(({ actions, values }) => ({ + deletedPerson: [ + null as PersonType | null, + { + deletePerson: async ({ person, deleteEvents }) => { + const params = deleteEvents ? { delete_events: true } : {} + await api.delete(`api/person/${person.id}?${toParams(params)}`) + lemonToast.success( + <> + The person {asDisplay(person)} was removed from the project. + {deleteEvents + ? ' Corresponding events will be deleted on a set schedule during non-peak usage times.' + : ' Their ID(s) will be usable again in an hour or so.'} + + ) + console.log('personDeleteCallback', values.personDeleteCallback) + values.personDeleteCallback?.(person, deleteEvents) + actions.showPersonDeleteModal(null) + return person + }, + }, + ], + })), +]) diff --git a/frontend/src/scenes/persons/personsLogic.tsx b/frontend/src/scenes/persons/personsLogic.tsx index 6ea853a29f3f5..44b45540c0aa9 100644 --- a/frontend/src/scenes/persons/personsLogic.tsx +++ b/frontend/src/scenes/persons/personsLogic.tsx @@ -47,8 +47,6 @@ export const personsLogic = kea({ navigateToCohort: (cohort: CohortType) => ({ cohort }), navigateToTab: (tab: PersonsTabType) => ({ tab }), setSplitMergeModalShown: (shown: boolean) => ({ shown }), - showPersonDeleteModal: (person: PersonType | null) => ({ person }), - deletePerson: (payload: { person: PersonType; deleteEvents: boolean }) => payload, setDistinctId: (distinctId: string) => ({ distinctId }), }, reducers: { @@ -91,12 +89,6 @@ export const personsLogic = kea({ loadPerson: () => null, setPerson: (_, { person }): PersonType | null => person, }, - personDeleteModal: [ - null as PersonType | null, - { - showPersonDeleteModal: (_, { person }) => person, - }, - ], distinctId: [ null as string | null, { @@ -155,20 +147,6 @@ export const personsLogic = kea({ urlId: [() => [(_, props) => props.urlId], (urlId) => urlId], }), listeners: ({ actions, values }) => ({ - deletePersonSuccess: ({ deletedPerson }) => { - // The deleted person's distinct IDs won't be usable until the person disappears from PersonManager's LRU. - // This can take up to an hour. Until then, the plugin server won't know to regenerate the person. - lemonToast.success( - <> - The person {asDisplay(deletedPerson.person)} was removed from the project. - {deletedPerson.deleteEvents - ? ' Corresponding events will be deleted on a set schedule during non-peak usage times.' - : ' Their ID(s) will be usable again in an hour or so.'} - - ) - actions.loadPersons() - router.actions.push(urls.persons()) - }, editProperty: async ({ key, newValue }) => { const person = values.person @@ -275,16 +253,6 @@ export const personsLogic = kea({ }, }, ], - deletedPerson: [ - {} as { person?: PersonType; deleteEvents?: boolean }, - { - deletePerson: async ({ person, deleteEvents }) => { - const params = deleteEvents ? { delete_events: true } : {} - await api.delete(`api/person/${person.id}?${toParams(params)}`) - return { person, deleteEvents } - }, - }, - ], }), actionToUrl: ({ values, props }) => ({ setListFilters: () => { diff --git a/frontend/src/scenes/persons/personsSceneLogic.ts b/frontend/src/scenes/persons/personsSceneLogic.ts index b738a7619c210..7d812ada44569 100644 --- a/frontend/src/scenes/persons/personsSceneLogic.ts +++ b/frontend/src/scenes/persons/personsSceneLogic.ts @@ -12,7 +12,7 @@ import type { personsSceneLogicType } from './personsSceneLogicType' const getDefaultQuery = (): DataTableNode => ({ kind: NodeKind.DataTableNode, source: { kind: NodeKind.PersonsNode }, - columns: ['person', 'id', 'created_at', 'properties.$geoip_country_name', 'properties.$browser'], + columns: ['person', 'id', 'created_at', 'properties.$geoip_country_name', 'properties.$browser', 'person.$delete'], propertiesViaUrl: true, showSearch: true, showPropertyFilter: true, From 61a16be333dc72b3bfa9163078df85722b537128 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:17:38 +0100 Subject: [PATCH 84/90] backport few fixes --- .../nodes/DataTable/DataTableExport.tsx | 9 +++++++-- .../queries/nodes/DataTable/dataTableLogic.ts | 20 +++++++------------ .../src/queries/nodes/DataTable/defaults.ts | 12 +++++++---- frontend/src/queries/schema.json | 14 ++++++------- frontend/src/queries/schema.ts | 4 ++-- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index ca28618e38379..a2ab0ca4590b4 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -8,11 +8,16 @@ import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { getEventsEndpoint, getPersonsEndpoint } from '~/queries/query' +const DEFAULT_EXPORT_LIMIT = 3500 + function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { const exportContext = isEventsNode(query.source) - ? { path: getEventsEndpoint(query.source), max_limit: query.source.limit ?? 3500 } + ? { + path: getEventsEndpoint({ ...query.source, limit: DEFAULT_EXPORT_LIMIT }), + max_limit: query.source.limit ?? DEFAULT_EXPORT_LIMIT, + } : isPersonsNode(query.source) - ? { path: getPersonsEndpoint(query.source), max_limit: 3500 } + ? { path: getPersonsEndpoint(query.source), max_limit: DEFAULT_EXPORT_LIMIT } : undefined if (!exportContext) { throw new Error('Unsupported node type') diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 299caf8d3092f..9642cd4d119a5 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -1,27 +1,22 @@ import { actions, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' import type { dataTableLogicType } from './dataTableLogicType' -import { DataTableNode, DataTableStringColumn } from '~/queries/schema' -import { defaultDataTableColumns } from './defaults' +import { DataTableNode, DataTableColumn } from '~/queries/schema' +import { defaultsForDataTable } from './defaults' import { sortedKeys } from 'lib/utils' export interface DataTableLogicProps { key: string query: DataTableNode - defaultColumns?: DataTableStringColumn[] + defaultColumns?: DataTableColumn[] } export const dataTableLogic = kea([ props({} as DataTableLogicProps), key((props) => props.key), path(['queries', 'nodes', 'DataTable', 'dataTableLogic']), - actions({ setColumns: (columns: DataTableStringColumn[]) => ({ columns }) }), + actions({ setColumns: (columns: DataTableColumn[]) => ({ columns }) }), reducers(({ props }) => ({ - columns: [ - (props.query.columns ?? - props.defaultColumns ?? - defaultDataTableColumns(props.query.source)) as DataTableStringColumn[], - { setColumns: (_, { columns }) => columns }, - ], + columns: [defaultsForDataTable(props.query, props.defaultColumns), { setColumns: (_, { columns }) => columns }], })), selectors({ queryWithDefaults: [ @@ -50,9 +45,8 @@ export const dataTableLogic = kea([ ], }), propsChanged(({ actions, props }, oldProps) => { - const newColumns = props.query.columns ?? props.defaultColumns ?? defaultDataTableColumns(props.query.source) - const oldColumns = - oldProps.query.columns ?? oldProps.defaultColumns ?? defaultDataTableColumns(oldProps.query.source) + const newColumns = defaultsForDataTable(props.query, props.defaultColumns) + const oldColumns = defaultsForDataTable(oldProps.query, oldProps.defaultColumns) if (JSON.stringify(newColumns) !== JSON.stringify(oldColumns)) { actions.setColumns(newColumns) } diff --git a/frontend/src/queries/nodes/DataTable/defaults.ts b/frontend/src/queries/nodes/DataTable/defaults.ts index f9871c919d736..8fd771d80353d 100644 --- a/frontend/src/queries/nodes/DataTable/defaults.ts +++ b/frontend/src/queries/nodes/DataTable/defaults.ts @@ -1,6 +1,6 @@ -import { DataNode, DataTableStringColumn, NodeKind } from '~/queries/schema' +import { DataNode, DataTableColumn, DataTableNode, NodeKind } from '~/queries/schema' -export const defaultDataTableEventColumns: DataTableStringColumn[] = [ +export const defaultDataTableEventColumns: DataTableColumn[] = [ 'event', 'person', 'url', @@ -8,7 +8,7 @@ export const defaultDataTableEventColumns: DataTableStringColumn[] = [ 'timestamp', ] -export const defaultDataTablePersonColumns: DataTableStringColumn[] = [ +export const defaultDataTablePersonColumns: DataTableColumn[] = [ 'person', 'id', 'created_at', @@ -16,6 +16,10 @@ export const defaultDataTablePersonColumns: DataTableStringColumn[] = [ 'properties.$browser', ] -export function defaultDataTableColumns(query: DataNode): DataTableStringColumn[] { +export function defaultDataTableColumns(query: DataNode): DataTableColumn[] { return query.kind === NodeKind.PersonsNode ? defaultDataTablePersonColumns : defaultDataTableEventColumns } + +export function defaultsForDataTable(query: DataTableNode, defaultColumns?: DataTableColumn[]): DataTableColumn[] { + return query.columns ?? defaultColumns ?? defaultDataTableColumns(query.source) +} diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index afa9b70d6affb..df64950e34555 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1255,13 +1255,16 @@ ], "type": "string" }, + "DataTableColumn": { + "type": "string" + }, "DataTableNode": { "additionalProperties": false, "properties": { "columns": { "description": "Columns shown in the table", "items": { - "$ref": "#/definitions/DataTableStringColumn" + "$ref": "#/definitions/DataTableColumn" }, "type": "array" }, @@ -1282,11 +1285,11 @@ "type": "boolean" }, "showColumnConfigurator": { - "description": "Show a button to configure the table's columns", + "description": "Show a button to configure the table's columns if possible", "type": "boolean" }, "showEventFilter": { - "description": "Include an event filter above the table", + "description": "Include an event filter above the table (EventsNode only)", "type": "boolean" }, "showEventsBufferWarning": { @@ -1306,7 +1309,7 @@ "type": "boolean" }, "showSearch": { - "description": "Include a free text search field", + "description": "Include a free text search field (PersonsNode only)", "type": "boolean" }, "source": { @@ -1324,9 +1327,6 @@ "required": ["kind", "source"], "type": "object" }, - "DataTableStringColumn": { - "type": "string" - }, "DateRange": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index f2d2b1938b786..1218631d15f4d 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -121,7 +121,7 @@ export interface DataTableNode extends Node { /** Source of the events */ source: EventsNode | PersonsNode /** Columns shown in the table */ - columns?: DataTableStringColumn[] + columns?: DataTableColumn[] /** Include an event filter above the table (EventsNode only) */ showEventFilter?: boolean /** Include a free text search field (PersonsNode only) */ @@ -216,7 +216,7 @@ export type InsightQueryNode = | LifecycleQuery export type InsightNodeKind = InsightQueryNode['kind'] -export type DataTableStringColumn = string +export type DataTableColumn = string // Legacy queries From 72beda4c3b1c050cd8fbac7fd0fd6a3a878cf708 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:21:55 +0100 Subject: [PATCH 85/90] fix a few issues --- frontend/src/queries/nodes/DataTable/defaults.ts | 13 +++++-------- frontend/src/scenes/cohorts/CohortEdit.tsx | 8 +------- frontend/src/scenes/persons/personsSceneLogic.ts | 2 +- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/defaults.ts b/frontend/src/queries/nodes/DataTable/defaults.ts index 8fd771d80353d..5ac5dd87ac236 100644 --- a/frontend/src/queries/nodes/DataTable/defaults.ts +++ b/frontend/src/queries/nodes/DataTable/defaults.ts @@ -1,4 +1,5 @@ import { DataNode, DataTableColumn, DataTableNode, NodeKind } from '~/queries/schema' +import { isEventsNode } from '~/queries/utils' export const defaultDataTableEventColumns: DataTableColumn[] = [ 'event', @@ -8,18 +9,14 @@ export const defaultDataTableEventColumns: DataTableColumn[] = [ 'timestamp', ] -export const defaultDataTablePersonColumns: DataTableColumn[] = [ - 'person', - 'id', - 'created_at', - 'properties.$geoip_country_name', - 'properties.$browser', -] +export const defaultDataTablePersonColumns: DataTableColumn[] = ['person', 'id', 'created_at', 'person.$delete'] export function defaultDataTableColumns(query: DataNode): DataTableColumn[] { return query.kind === NodeKind.PersonsNode ? defaultDataTablePersonColumns : defaultDataTableEventColumns } export function defaultsForDataTable(query: DataTableNode, defaultColumns?: DataTableColumn[]): DataTableColumn[] { - return query.columns ?? defaultColumns ?? defaultDataTableColumns(query.source) + return ( + query.columns ?? (isEventsNode(query.source) ? defaultColumns : null) ?? defaultDataTableColumns(query.source) + ) } diff --git a/frontend/src/scenes/cohorts/CohortEdit.tsx b/frontend/src/scenes/cohorts/CohortEdit.tsx index 0243e29190391..70ad45b833e00 100644 --- a/frontend/src/scenes/cohorts/CohortEdit.tsx +++ b/frontend/src/scenes/cohorts/CohortEdit.tsx @@ -229,13 +229,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { kind: NodeKind.PersonsNode, cohort: cohort.id, }, - columns: [ - 'person', - 'id', - 'created_at', - 'properties.$geoip_country_name', - 'properties.$browser', - ], + columns: undefined, showSearch: true, showPropertyFilter: true, showExport: true, diff --git a/frontend/src/scenes/persons/personsSceneLogic.ts b/frontend/src/scenes/persons/personsSceneLogic.ts index 7d812ada44569..f7aaa0893aa7e 100644 --- a/frontend/src/scenes/persons/personsSceneLogic.ts +++ b/frontend/src/scenes/persons/personsSceneLogic.ts @@ -12,7 +12,7 @@ import type { personsSceneLogicType } from './personsSceneLogicType' const getDefaultQuery = (): DataTableNode => ({ kind: NodeKind.DataTableNode, source: { kind: NodeKind.PersonsNode }, - columns: ['person', 'id', 'created_at', 'properties.$geoip_country_name', 'properties.$browser', 'person.$delete'], + columns: undefined, propertiesViaUrl: true, showSearch: true, showPropertyFilter: true, From 3f10a7116d36f1ace612500509304f268105e8dc Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:34:34 +0100 Subject: [PATCH 86/90] remove delete from export --- frontend/src/queries/nodes/DataTable/DataTableExport.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index a2ab0ca4590b4..1010981e88821 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -34,9 +34,9 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void } if (onlySelectedColumns) { - exportContext['columns'] = (query.columns ?? defaultDataTableColumns(query.source))?.flatMap( - (c) => columnMapping[c] || c - ) + exportContext['columns'] = (query.columns ?? defaultDataTableColumns(query.source)) + ?.flatMap((c) => columnMapping[c] || c) + .filter((c) => c !== 'person.$delete') } triggerExport({ export_format: ExporterFormat.CSV, From 65dc0f7810bc5feb28e4a266b0d430a8b27d64c2 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:41:28 +0100 Subject: [PATCH 87/90] export limit --- .../nodes/DataTable/DataTableExport.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 1010981e88821..b49450eb398ef 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -8,16 +8,17 @@ import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { getEventsEndpoint, getPersonsEndpoint } from '~/queries/query' -const DEFAULT_EXPORT_LIMIT = 3500 +const EXPORT_LIMIT_EVENTS = 3500 +const EXPORT_LIMIT_PERSONS = 10000 function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { const exportContext = isEventsNode(query.source) ? { - path: getEventsEndpoint({ ...query.source, limit: DEFAULT_EXPORT_LIMIT }), - max_limit: query.source.limit ?? DEFAULT_EXPORT_LIMIT, + path: getEventsEndpoint({ ...query.source, limit: EXPORT_LIMIT_EVENTS }), + max_limit: query.source.limit ?? EXPORT_LIMIT_EVENTS, } : isPersonsNode(query.source) - ? { path: getPersonsEndpoint(query.source), max_limit: DEFAULT_EXPORT_LIMIT } + ? { path: getPersonsEndpoint(query.source), max_limit: EXPORT_LIMIT_PERSONS } : undefined if (!exportContext) { throw new Error('Unsupported node type') @@ -59,6 +60,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | { startDownload(query, true) }} @@ -70,6 +72,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | startDownload(query, false)} > @@ -89,19 +92,22 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | interface ExportWithConfirmationProps { placement: 'topRight' | 'bottomRight' onConfirm: (e?: React.MouseEvent) => void + query: DataTableNode children: React.ReactNode } -function ExportWithConfirmation({ placement, onConfirm, children }: ExportWithConfirmationProps): JSX.Element { +function ExportWithConfirmation({ query, placement, onConfirm, children }: ExportWithConfirmationProps): JSX.Element { + const actor = isPersonsNode(query.source) ? 'events' : 'persons' + const limit = isPersonsNode(query.source) ? EXPORT_LIMIT_EVENTS : EXPORT_LIMIT_PERSONS return ( - Exporting by csv is limited to 3,500 events. + Exporting by csv is limited to {limit} {actor}.
- To return more, please use the API. Do you want to - export by CSV? + To return more, please use the API. Do you want + to export by CSV? } onConfirm={onConfirm} From 8389f76648a21175053761e7df5a1542d9a44307 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:49:34 +0100 Subject: [PATCH 88/90] show filter count --- .../src/queries/nodes/DataTable/DataTableExport.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index b49450eb398ef..310949938f8b8 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -3,7 +3,7 @@ import { IconExport } from 'lib/components/icons' import { Popconfirm } from 'antd' import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' -import { DataTableNode } from '~/queries/schema' +import { DataNode, DataTableNode } from '~/queries/schema' import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { isEventsNode, isPersonsNode } from '~/queries/utils' import { getEventsEndpoint, getPersonsEndpoint } from '~/queries/query' @@ -51,6 +51,12 @@ interface DataTableExportProps { } export function DataTableExport({ query }: DataTableExportProps): JSX.Element | null { + const source: DataNode = query.source + const filterCount = + (isEventsNode(source) || isPersonsNode(source) ? source.properties?.length || 0 : 0) + + (isEventsNode(source) && source.event ? 1 : 0) + + (isPersonsNode(source) && source.search ? 1 : 0) + return ( } > - Export + Export{filterCount > 0 ? ` (${filterCount} filter${filterCount === 1 ? '' : 's'})` : ''} ) } From 3f016672dfa2e2a14bea42461b8b2eb9815173b6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 9 Dec 2022 23:56:05 +0100 Subject: [PATCH 89/90] few fixes --- frontend/src/queries/nodes/DataTable/DataTable.tsx | 2 +- frontend/src/scenes/persons/Persons.tsx | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 29779c62552ea..0cf348139ec24 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -154,7 +154,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele className="DataTable" loading={responseLoading && !nextDataLoading && !newDataLoading} columns={lemonColumns} - key={lemonColumns.join('::')} + key={lemonColumns.join('::') /* Bust the LemonTable cache when columns change */} dataSource={dataSource} expandable={ expandable diff --git a/frontend/src/scenes/persons/Persons.tsx b/frontend/src/scenes/persons/Persons.tsx index fe2b6cf0a08fe..d87a2b22e8905 100644 --- a/frontend/src/scenes/persons/Persons.tsx +++ b/frontend/src/scenes/persons/Persons.tsx @@ -4,20 +4,12 @@ import { Popconfirm } from 'antd' import { personsLogic } from './personsLogic' import { CohortType } from '~/types' import { PersonsSearch } from './PersonsSearch' -import { SceneExport } from 'scenes/sceneTypes' -import { PersonPageHeader } from './PersonPageHeader' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { LemonButton } from 'lib/components/LemonButton' import { IconExport } from 'lib/components/icons' import { triggerExport } from 'lib/components/ExportButton/exporter' -export const scene: SceneExport = { - component: PersonsScene, - logic: personsLogic, - paramsToProps: () => ({ syncWithUrl: true }), -} - interface PersonsProps { cohort?: CohortType['id'] } @@ -32,11 +24,10 @@ export function Persons({ cohort }: PersonsProps = {}): JSX.Element { export function PersonsScene(): JSX.Element { const { loadPersons, setListFilters } = useActions(personsLogic) - const { cohortId, persons, listFilters, personsLoading, exporterProps, apiDocsURL } = useValues(personsLogic) + const { persons, listFilters, personsLoading, exporterProps, apiDocsURL } = useValues(personsLogic) return (
- {!cohortId && }
From eaa0504114989111a9c04e6c2538eff7d5506dab Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 10 Dec 2022 00:27:36 +0100 Subject: [PATCH 90/90] call live_events_columns what it is --- frontend/src/queries/nodes/DataTable/DataTable.tsx | 5 ++--- .../src/queries/nodes/DataTable/dataTableLogic.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 0cf348139ec24..ac9b58f7cdf63 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -18,7 +18,6 @@ import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' import { dataTableLogic, DataTableLogicProps } from '~/queries/nodes/DataTable/dataTableLogic' import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' import { teamLogic } from 'scenes/teamLogic' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/defaults' import { LemonDivider } from 'lib/components/LemonDivider' import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' @@ -53,9 +52,9 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele } = useValues(dataNodeLogic(dataNodeLogicProps)) const { currentTeam } = useValues(teamLogic) - const defaultColumns = currentTeam?.live_events_columns ?? defaultDataTableColumns(query.source) + const defaultEventsColumns = currentTeam?.live_events_columns ?? undefined - const dataTableLogicProps: DataTableLogicProps = { query, key, defaultColumns } + const dataTableLogicProps: DataTableLogicProps = { query, key, defaultEventsColumns } const { columns, queryWithDefaults } = useValues(dataTableLogic(dataTableLogicProps)) const { diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 9642cd4d119a5..c58afa3d34cbb 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -7,7 +7,7 @@ import { sortedKeys } from 'lib/utils' export interface DataTableLogicProps { key: string query: DataTableNode - defaultColumns?: DataTableColumn[] + defaultEventsColumns?: DataTableColumn[] } export const dataTableLogic = kea([ @@ -16,7 +16,10 @@ export const dataTableLogic = kea([ path(['queries', 'nodes', 'DataTable', 'dataTableLogic']), actions({ setColumns: (columns: DataTableColumn[]) => ({ columns }) }), reducers(({ props }) => ({ - columns: [defaultsForDataTable(props.query, props.defaultColumns), { setColumns: (_, { columns }) => columns }], + columns: [ + defaultsForDataTable(props.query, props.defaultEventsColumns), + { setColumns: (_, { columns }) => columns }, + ], })), selectors({ queryWithDefaults: [ @@ -45,8 +48,8 @@ export const dataTableLogic = kea([ ], }), propsChanged(({ actions, props }, oldProps) => { - const newColumns = defaultsForDataTable(props.query, props.defaultColumns) - const oldColumns = defaultsForDataTable(oldProps.query, oldProps.defaultColumns) + const newColumns = defaultsForDataTable(props.query, props.defaultEventsColumns) + const oldColumns = defaultsForDataTable(oldProps.query, oldProps.defaultEventsColumns) if (JSON.stringify(newColumns) !== JSON.stringify(oldColumns)) { actions.setColumns(newColumns) }