From 897a458446639fe7af81dea8f5275297c5e62706 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 20 Nov 2023 17:26:08 +0000 Subject: [PATCH] fix: Circular imports issue (#18518) * Removed all models in favour of "permanent" mounting * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Fix * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Fixed up mounting * Update UI snapshots for `chromium` (2) * Fixes for permanent mount * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Fix api to not have a dependency on logics * Tried fixing import situation * Update UI snapshots for `chromium` (2) * Move deleteWithUndo * Update UI snapshots for `chromium` (1) * Fix utils imports * Moved some things around * Fixes * fix up cyclic deps issue * Moved more types * Moved things around... * Fixes * Remove issue * Fix * Update UI snapshots for `chromium` (2) * Fixed import issue * Update UI snapshots for `chromium` (2) * Fixed up api import * Update UI snapshots for `chromium` (1) * Fixed the imports!!!!!! * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `webkit` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `webkit` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `webkit` (2) * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (1) * Fix imports * Fix * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Fixes * Update UI snapshots for `webkit` (2) * Update UI snapshots for `chromium` (2) * Fix * Fix * Fix --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- ...nes-app-notebooks--recordings-playlist.png | Bin 83427 -> 83334 bytes .../navigation-3000/sidebars/featureFlags.tsx | 3 +- .../navigation-3000/sidebars/insights.ts | 2 +- .../sidepanel/panels/SidePanelSettings.tsx | 3 +- .../panels/sidePanelSettingsLogic.tsx | 2 +- frontend/src/lib/api.ts | 87 +++-- .../ActivityLog/activityLogLogic.tsx | 3 +- .../Cards/InsightCard/InsightDetails.tsx | 3 +- .../components/CodeSnippet/CodeSnippet.tsx | 2 +- .../CommandPalette/commandPaletteLogic.tsx | 3 +- .../src/lib/components/CopyToClipboard.tsx | 2 +- .../lib/components/DateFilter/DateFilter.tsx | 10 +- .../components/DateFilter/dateFilterLogic.ts | 6 +- .../src/lib/components/DateFilter/types.ts | 4 + .../src/lib/components/NotFound/index.tsx | 2 +- .../components/PropertyFilterButton.tsx | 3 +- .../components/PropertyFilters/utils.test.ts | 68 ++++ .../lib/components/PropertyFilters/utils.ts | 85 ++++- .../propertyGroupFilterLogic.ts | 3 +- .../src/lib/components/RestrictedArea.tsx | 4 +- .../lib/components/Sharing/SharingModal.tsx | 2 +- .../Subscriptions/subscriptionsLogic.ts | 2 +- frontend/src/lib/components/UUIDShortener.tsx | 2 +- frontend/src/lib/constants.tsx | 13 +- frontend/src/lib/taxonomy.tsx | 7 +- frontend/src/lib/utils.test.ts | 120 +------ frontend/src/lib/utils.tsx | 327 +----------------- frontend/src/lib/utils/copyToClipboard.tsx | 34 ++ frontend/src/lib/utils/deleteWithUndo.tsx | 34 ++ frontend/src/lib/utils/eventUsageLogic.ts | 3 +- frontend/src/lib/utils/permissioning.ts | 7 +- frontend/src/models/annotationsModel.ts | 2 +- frontend/src/models/cohortsModel.ts | 47 ++- frontend/src/models/notebooksModel.ts | 2 +- .../nodes/DataTable/DataTableExport.tsx | 2 +- .../nodes/DataTable/EventRowActions.tsx | 3 +- .../propertyGroupFilterLogic.ts | 3 +- .../src/scenes/actions/actionEditLogic.tsx | 3 +- frontend/src/scenes/apps/AppMetricsGraph.tsx | 3 +- frontend/src/scenes/apps/AppMetricsScene.tsx | 3 +- frontend/src/scenes/apps/HistoricalExport.tsx | 2 +- frontend/src/scenes/apps/MetricsTab.tsx | 3 +- .../src/scenes/apps/appMetricsSceneLogic.ts | 18 +- frontend/src/scenes/apps/constants.tsx | 2 +- .../src/scenes/cohorts/cohortEditLogic.ts | 3 +- .../data-management/actions/ActionsTable.tsx | 3 +- .../events/EventDefinitionProperties.tsx | 6 +- .../events/EventDefinitionsTable.tsx | 6 +- .../events/eventDefinitionsTableLogic.test.ts | 7 +- .../events/eventDefinitionsTableLogic.ts | 7 +- .../properties/PropertyDefinitionsTable.tsx | 6 +- .../propertyDefinitionsTableLogic.test.ts | 6 +- .../propertyDefinitionsTableLogic.ts | 3 +- .../external/DataWarehouseTables.tsx | 2 +- .../DataWarehouseSavedQueriesContainer.tsx | 2 +- .../src/scenes/feature-flags/FeatureFlags.tsx | 3 +- .../scenes/feature-flags/featureFlagLogic.ts | 4 +- .../src/scenes/insights/InsightPageHeader.tsx | 2 +- .../filters/ActionFilter/entityFilterLogic.ts | 3 +- frontend/src/scenes/insights/utils.tsx | 48 +++ .../src/scenes/insights/utils/cleanFilters.ts | 43 ++- .../AddToNotebook/DraggableToNotebook.tsx | 2 +- .../scenes/notebooks/Nodes/NodeWrapper.tsx | 3 +- .../notebooks/Nodes/NotebookNodeContext.ts | 10 + .../notebooks/Nodes/notebookNodeLogic.ts | 8 - .../notebooks/Notebook/NotebookShare.tsx | 2 +- .../NotebookSelectButton.tsx | 2 +- frontend/src/scenes/organizationLogic.tsx | 7 +- .../src/scenes/paths/PathNodeCardButton.tsx | 2 +- frontend/src/scenes/persons/PersonDisplay.tsx | 2 +- frontend/src/scenes/persons/personsLogic.tsx | 4 +- .../src/scenes/pipeline/Transformations.tsx | 3 +- .../src/scenes/plugins/plugin/PluginLogs.tsx | 3 +- .../scenes/plugins/plugin/pluginLogsLogic.ts | 3 +- .../scenes/plugins/tabs/apps/components.tsx | 2 +- .../scenes/saved-insights/SavedInsights.tsx | 2 +- .../player/PlayerMetaLinks.tsx | 2 +- .../player/inspector/components/ItemEvent.tsx | 3 +- .../player/share/PlayerShare.tsx | 2 +- .../playlist/SessionRecordingsPlaylist.tsx | 2 +- .../playlist/playlistUtils.ts | 4 +- frontend/src/scenes/settings/Settings.tsx | 4 +- .../VerifiedDomains/VerifiedDomains.tsx | 14 +- frontend/src/scenes/settings/settingsLogic.ts | 14 +- frontend/src/scenes/settings/types.ts | 13 +- .../settings/user/personalAPIKeysLogic.ts | 2 +- frontend/src/scenes/teamLogic.tsx | 7 +- frontend/src/scenes/urls.ts | 2 +- frontend/src/scenes/userLogic.ts | 6 +- frontend/src/test/init.ts | 2 + frontend/src/types.ts | 24 ++ 91 files changed, 611 insertions(+), 638 deletions(-) create mode 100644 frontend/src/lib/utils/copyToClipboard.tsx create mode 100644 frontend/src/lib/utils/deleteWithUndo.tsx create mode 100644 frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png index ae5e9601bdbb4e2adcb56491b88f17b2602aecbf..ae267af0034d9b3bf209eb7ad0d6b70df84af8e2 100644 GIT binary patch delta 45522 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 S=y;SI3hltr{b~D-|MTBSNN%J6 diff --git a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx index 056a0ccb06cdc..cd05460eb2669 100644 --- a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx @@ -10,7 +10,8 @@ import type { featureFlagsSidebarLogicType } from './featureFlagsType' import Fuse from 'fuse.js' import { FeatureFlagType } from '~/types' import { subscriptions } from 'kea-subscriptions' -import { copyToClipboard, deleteWithUndo } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { teamLogic } from 'scenes/teamLogic' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { navigation3000Logic } from '../navigationLogic' diff --git a/frontend/src/layout/navigation-3000/sidebars/insights.ts b/frontend/src/layout/navigation-3000/sidebars/insights.ts index efc9766d0a7c6..71021b95246b6 100644 --- a/frontend/src/layout/navigation-3000/sidebars/insights.ts +++ b/frontend/src/layout/navigation-3000/sidebars/insights.ts @@ -9,7 +9,7 @@ import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' import { INSIGHTS_PER_PAGE, savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' import type { insightsSidebarLogicType } from './insightsType' import { findSearchTermInItemName } from './utils' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { teamLogic } from 'scenes/teamLogic' import { api } from '@posthog/apps-common' import { insightsModel } from '~/models/insightsModel' diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx index 4ed5684861719..42de4417d1aba 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx @@ -3,10 +3,11 @@ import { sidePanelSettingsLogic } from './sidePanelSettingsLogic' import { Settings } from 'scenes/settings/Settings' import { LemonButton } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' -import { SettingsLogicProps, settingsLogic } from 'scenes/settings/settingsLogic' +import { settingsLogic } from 'scenes/settings/settingsLogic' import { useEffect } from 'react' import { SidePanelPaneHeader } from '../components/SidePanelPane' import { IconExternal } from '@posthog/icons' +import { SettingsLogicProps } from 'scenes/settings/types' export const SidePanelSettings = (): JSX.Element => { const { settings } = useValues(sidePanelSettingsLogic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx index da07a199f139d..b39077ca39523 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx @@ -7,7 +7,7 @@ import { LemonDialog } from '@posthog/lemon-ui' import type { sidePanelSettingsLogicType } from './sidePanelSettingsLogicType' import { sidePanelStateLogic } from '../sidePanelStateLogic' import { SidePanelTab } from '~/types' -import { SettingsLogicProps } from 'scenes/settings/settingsLogic' +import { SettingsLogicProps } from 'scenes/settings/types' export const sidePanelSettingsLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelSettingsLogic']), diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1a279b58c350b..d51c4226ab69b 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,7 @@ import posthog from 'posthog-js' +import { decompressSync, strFromU8 } from 'fflate' +import { encodeParams } from 'kea-router' + import { ActionType, BatchExportLogEntry, @@ -55,22 +58,27 @@ import { ExternalDataStripeSourceCreatePayload, ExternalDataStripeSource, } from '~/types' -import { getCurrentOrganizationId, getCurrentTeamId } from './utils/logics' -import { CheckboxValueType } from 'antd/lib/checkbox/Group' -import { LOGS_PORTION_LIMIT } from 'scenes/plugins/plugin/pluginLogsLogic' +import { + ACTIVITY_PAGE_SIZE, + DashboardPrivilegeLevel, + EVENT_DEFINITIONS_PER_PAGE, + EVENT_PROPERTY_DEFINITIONS_PER_PAGE, + LOGS_PORTION_LIMIT, +} from './constants' import { toParams } from 'lib/utils' -import { DashboardPrivilegeLevel } from './constants' -import { EVENT_DEFINITIONS_PER_PAGE } from 'scenes/data-management/events/eventDefinitionsTableLogic' -import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' import { ActivityLogProps } from 'lib/components/ActivityLog/ActivityLog' import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' import { QuerySchema, QueryStatus } from '~/queries/schema' -import { decompressSync, strFromU8 } from 'fflate' import { getCurrentExporterData } from '~/exporter/exporterViewLogic' -import { encodeParams } from 'kea-router' -export const ACTIVITY_PAGE_SIZE = 20 +/** + * WARNING: Be very careful importing things here. This file is heavily used and can trigger a lot of cyclic imports + * Preferably create a dedicated file in utils/.. + */ + +type CheckboxValueType = string | number | boolean + const PAGINATION_DEFAULT_MAX_PAGES = 10 export interface PaginatedResponse { @@ -116,6 +124,33 @@ export async function getJSONOrThrow(response: Response): Promise { } } +export class ApiConfig { + private static _currentOrganizationId: OrganizationType['id'] | null = null + private static _currentTeamId: TeamType['id'] | null = null + + static getCurrentOrganizationId(): OrganizationType['id'] { + if (!this._currentOrganizationId) { + throw new Error('Organization ID is not known.') + } + return this._currentOrganizationId + } + + static setCurrentOrganizationId(id: OrganizationType['id']): void { + this._currentOrganizationId = id + } + + static getCurrentTeamId(): TeamType['id'] { + if (!this._currentTeamId) { + throw new Error('Team ID is not known.') + } + return this._currentTeamId + } + + static setCurrentTeamId(id: TeamType['id']): void { + this._currentTeamId = id + } +} + class ApiRequest { private pathComponents: string[] private queryString: string | undefined @@ -169,7 +204,7 @@ class ApiRequest { return this.addPathComponent('organizations') } - public organizationsDetail(id: OrganizationType['id'] = getCurrentOrganizationId()): ApiRequest { + public organizationsDetail(id: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId()): ApiRequest { return this.organizations().addPathComponent(id) } @@ -200,7 +235,7 @@ class ApiRequest { return this.addPathComponent('projects') } - public projectsDetail(id: TeamType['id'] = getCurrentTeamId()): ApiRequest { + public projectsDetail(id: TeamType['id'] = ApiConfig.getCurrentTeamId()): ApiRequest { return this.projects().addPathComponent(id) } @@ -528,7 +563,7 @@ class ApiRequest { // Resource Access Permissions public featureFlagAccessPermissions(flagId: FeatureFlagType['id']): ApiRequest { - return this.featureFlag(flagId, getCurrentTeamId()).addPathComponent('role_access') + return this.featureFlag(flagId, ApiConfig.getCurrentTeamId()).addPathComponent('role_access') } public featureFlagAccessPermissionsDetail( @@ -697,13 +732,13 @@ const api = { organizationFeatureFlags: { async get( - orgId: OrganizationType['id'] = getCurrentOrganizationId(), + orgId: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId(), featureFlagKey: FeatureFlagType['key'] ): Promise { return await new ApiRequest().organizationFeatureFlags(orgId, featureFlagKey).get() }, async copy( - orgId: OrganizationType['id'] = getCurrentOrganizationId(), + orgId: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId(), data: OrganizationFeatureFlagsCopyBody ): Promise<{ success: FeatureFlagType[]; failed: any }> { return await new ApiRequest().copyOrganizationFeatureFlags(orgId).create({ data }) @@ -745,7 +780,7 @@ const api = { list( activityLogProps: ActivityLogProps, page: number = 1, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise> { const requestForScope: Record ApiRequest | null> = { [ActivityScope.FEATURE_FLAG]: (props) => { @@ -790,7 +825,7 @@ const api = { }, exports: { - determineExportUrl(exportId: number, teamId: TeamType['id'] = getCurrentTeamId()): string { + determineExportUrl(exportId: number, teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): string { return new ApiRequest() .export(exportId, teamId) .withAction('content') @@ -801,12 +836,12 @@ const api = { async create( data: Partial, params: Record = {}, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise { return new ApiRequest().exports(teamId).withQueryString(toParams(params)).create({ data }) }, - async get(id: number, teamId: TeamType['id'] = getCurrentTeamId()): Promise { + async get(id: number, teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): Promise { return new ApiRequest().export(id, teamId).get() }, }, @@ -815,7 +850,7 @@ const api = { async get( id: EventType['id'], includePerson: boolean = false, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise { let apiRequest = new ApiRequest().event(id, teamId) if (includePerson) { @@ -826,7 +861,7 @@ const api = { async list( filters: EventsListQueryParams, limit: number = 100, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise> { const params: EventsListQueryParams = { ...filters, limit, orderBy: filters.orderBy ?? ['-timestamp'] } return new ApiRequest().events(teamId).withQueryString(toParams(params)).get() @@ -834,7 +869,7 @@ const api = { determineListEndpoint( filters: EventsListQueryParams, limit: number = 100, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): string { const params: EventsListQueryParams = { ...filters, limit } return new ApiRequest().events(teamId).withQueryString(toParams(params)).assembleFullUrl() @@ -842,7 +877,7 @@ const api = { }, tags: { - async list(teamId: TeamType['id'] = getCurrentTeamId()): Promise { + async list(teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): Promise { return new ApiRequest().tags(teamId).get() }, }, @@ -865,7 +900,7 @@ const api = { }, async list({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { limit?: number @@ -881,7 +916,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { limit?: number @@ -930,7 +965,7 @@ const api = { }, async list({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { event_names?: string[] @@ -956,7 +991,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { event_names?: string[] diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx index fcc633bb7dfe0..4e19411f067bb 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx @@ -1,6 +1,6 @@ import { loaders } from 'kea-loaders' import { kea, props, key, path, actions, reducers, selectors, listeners, events } from 'kea' -import api, { ACTIVITY_PAGE_SIZE, ActivityLogPaginatedResponse } from 'lib/api' +import api, { ActivityLogPaginatedResponse } from 'lib/api' import { ActivityLogItem, ActivityScope, @@ -19,6 +19,7 @@ import { insightActivityDescriber } from 'scenes/saved-insights/activityDescript import { personActivityDescriber } from 'scenes/persons/activityDescriptions' import { dataManagementActivityDescriber } from 'scenes/data-management/dataManagementDescribers' import { notebookActivityDescriber } from 'scenes/notebooks/Notebook/notebookActivityDescriber' +import { ACTIVITY_PAGE_SIZE } from 'lib/constants' /** * Having this function inside the `humanizeActivity module was causing very weird test errors in other modules diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx index f594637987c79..35198f96ed277 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx @@ -1,5 +1,5 @@ import { useValues } from 'kea' -import { allOperatorsMapping, capitalizeFirstLetter, formatPropertyLabel } from 'lib/utils' +import { allOperatorsMapping, capitalizeFirstLetter } from 'lib/utils' import { LocalFilter, toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { humanizePathsEventTypes } from 'scenes/insights/utils' import { apiValueToMathType, MathCategory, MathDefinition, mathsLogic } from 'scenes/trends/mathsLogic' @@ -26,6 +26,7 @@ import { cohortsModel } from '~/models/cohortsModel' import React from 'react' import { isPathsFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' import { + formatPropertyLabel, isAnyPropertyfilter, isCohortPropertyFilter, isPropertyFilterWithOperator, diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index 9c9ca574f8194..4b101df7bfa1a 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -18,7 +18,7 @@ import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml' import markup from 'react-syntax-highlighter/dist/esm/languages/prism/markup' import http from 'react-syntax-highlighter/dist/esm/languages/prism/http' import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { Popconfirm } from 'antd' import { PopconfirmProps } from 'antd/lib/popconfirm' import './CodeSnippet.scss' diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 1555276502b3a..27057c4de09eb 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -6,7 +6,8 @@ import { dashboardsModel } from '~/models/dashboardsModel' import { Parser } from 'expr-eval' import { DashboardType, InsightType } from '~/types' import api from 'lib/api' -import { copyToClipboard, isMobile, isURL, sample, uniqueBy } from 'lib/utils' +import { isMobile, isURL, sample, uniqueBy } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { userLogic } from 'scenes/userLogic' import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' import { teamLogic } from 'scenes/teamLogic' diff --git a/frontend/src/lib/components/CopyToClipboard.tsx b/frontend/src/lib/components/CopyToClipboard.tsx index 4ffd1456cef3f..0e85ee60317d8 100644 --- a/frontend/src/lib/components/CopyToClipboard.tsx +++ b/frontend/src/lib/components/CopyToClipboard.tsx @@ -1,5 +1,5 @@ import { HTMLProps } from 'react' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' diff --git a/frontend/src/lib/components/DateFilter/DateFilter.tsx b/frontend/src/lib/components/DateFilter/DateFilter.tsx index 01bb247117747..254493add06ed 100644 --- a/frontend/src/lib/components/DateFilter/DateFilter.tsx +++ b/frontend/src/lib/components/DateFilter/DateFilter.tsx @@ -3,14 +3,20 @@ import { dateMapping, dateFilterToText, uuid } from 'lib/utils' import { DateMappingOption } from '~/types' import { dayjs } from 'lib/dayjs' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { CUSTOM_OPTION_DESCRIPTION, CUSTOM_OPTION_KEY, CUSTOM_OPTION_VALUE, dateFilterLogic } from './dateFilterLogic' +import { dateFilterLogic } from './dateFilterLogic' import { RollingDateRangeFilter } from './RollingDateRangeFilter' import { useActions, useValues } from 'kea' import { LemonButtonWithDropdown, LemonDivider, LemonButton, LemonButtonProps } from '@posthog/lemon-ui' import { IconCalendar } from 'lib/lemon-ui/icons' import { LemonCalendarSelect } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' import { LemonCalendarRange } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' -import { DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' +import { + CUSTOM_OPTION_DESCRIPTION, + CUSTOM_OPTION_KEY, + CUSTOM_OPTION_VALUE, + DateFilterLogicProps, + DateFilterView, +} from 'lib/components/DateFilter/types' import { Placement } from '@floating-ui/react' export interface DateFilterProps { diff --git a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts index afe18d4e7b7e3..13536740e4c93 100644 --- a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts +++ b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts @@ -3,11 +3,7 @@ import { dayjs, Dayjs } from 'lib/dayjs' import type { dateFilterLogicType } from './dateFilterLogicType' import { isDate, dateFilterToText, dateStringToDayJs, formatDateRange, formatDate } from 'lib/utils' import { DateMappingOption } from '~/types' -import { DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' - -export const CUSTOM_OPTION_KEY = 'Custom' -export const CUSTOM_OPTION_VALUE = 'No date range override' -export const CUSTOM_OPTION_DESCRIPTION = 'Use the original date ranges of insights' +import { CUSTOM_OPTION_VALUE, DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' export const dateFilterLogic = kea([ path(['lib', 'components', 'DateFilter', 'DateFilterLogic']), diff --git a/frontend/src/lib/components/DateFilter/types.ts b/frontend/src/lib/components/DateFilter/types.ts index 5d65aed3d4cfb..63bbd2c29303a 100644 --- a/frontend/src/lib/components/DateFilter/types.ts +++ b/frontend/src/lib/components/DateFilter/types.ts @@ -15,3 +15,7 @@ export type DateFilterLogicProps = { dateOptions?: DateMappingOption[] isDateFormatted?: boolean } + +export const CUSTOM_OPTION_KEY = 'Custom' +export const CUSTOM_OPTION_VALUE = 'No date range override' +export const CUSTOM_OPTION_DESCRIPTION = 'Use the original date ranges of insights' diff --git a/frontend/src/lib/components/NotFound/index.tsx b/frontend/src/lib/components/NotFound/index.tsx index 1c1a20c595925..9e18b27d6ecc9 100644 --- a/frontend/src/lib/components/NotFound/index.tsx +++ b/frontend/src/lib/components/NotFound/index.tsx @@ -4,7 +4,7 @@ import './NotFound.scss' import { useActions, useValues } from 'kea' import { supportLogic } from '../Support/supportLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { LemonButton } from '@posthog/lemon-ui' interface NotFoundProps { diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx index 959a8a47831ed..f9fd9a37f7432 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx @@ -5,10 +5,11 @@ import { CloseButton } from 'lib/components/CloseButton' import { cohortsModel } from '~/models/cohortsModel' import { useValues } from 'kea' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatPropertyLabel, midEllipsis } from 'lib/utils' +import { midEllipsis } from 'lib/utils' import { KEY_MAPPING } from 'lib/taxonomy' import React from 'react' import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon' +import { formatPropertyLabel } from '../utils' export interface PropertyFilterButtonProps { onClick?: () => void diff --git a/frontend/src/lib/components/PropertyFilters/utils.test.ts b/frontend/src/lib/components/PropertyFilters/utils.test.ts index f8ecda127588b..56ff189f94e0e 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.test.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.test.ts @@ -3,7 +3,9 @@ import { CohortPropertyFilter, ElementPropertyFilter, EmptyPropertyFilter, + FilterLogicalOperator, PropertyFilterType, + PropertyGroupFilter, PropertyOperator, SessionPropertyFilter, } from '../../../types' @@ -11,6 +13,8 @@ import { isValidPropertyFilter, propertyFilterTypeToTaxonomicFilterType, breakdownFilterToTaxonomicFilterType, + convertPropertiesToPropertyGroup, + convertPropertyGroupToProperties, } from 'lib/components/PropertyFilters/utils' import { TaxonomicFilterGroupType } from '../TaxonomicFilter/types' import { BreakdownFilter } from '~/queries/schema' @@ -123,3 +127,67 @@ describe('breakdownFilterToTaxonomicFilterType()', () => { ) }) }) + +describe('convertPropertyGroupToProperties()', () => { + it('converts a single layer property group into an array of properties', () => { + const propertyGroup = { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [ + { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ] as AnyPropertyFilter[], + }, + { + type: FilterLogicalOperator.And, + values: [ + { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ] as AnyPropertyFilter[], + }, + ], + } + expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([ + { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ]) + }) + + it('converts a deeply nested property group into an array of properties', () => { + const propertyGroup: PropertyGroupFilter = { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [{ type: FilterLogicalOperator.And, values: [{ key: '$lib' } as any] }], + }, + { type: FilterLogicalOperator.And, values: [{ key: '$browser' } as any] }, + ], + } + expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([{ key: '$lib' }, { key: '$browser' }]) + }) +}) + +describe('convertPropertiesToPropertyGroup', () => { + it('converts properties to one AND operator property group', () => { + const properties: any[] = [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }] + expect(convertPropertiesToPropertyGroup(properties)).toEqual({ + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }], + }, + ], + }) + }) + + it('converts properties to one AND operator property group', () => { + expect(convertPropertiesToPropertyGroup(undefined)).toEqual({ + type: FilterLogicalOperator.And, + values: [], + }) + }) +}) diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index 48a181c2acdbb..6ce5fe2dd174f 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -2,16 +2,20 @@ import { AnyFilterLike, AnyPropertyFilter, CohortPropertyFilter, + CohortType, ElementPropertyFilter, + EmptyPropertyFilter, EventDefinition, EventPropertyFilter, FeaturePropertyFilter, FilterLogicalOperator, GroupPropertyFilter, HogQLPropertyFilter, + KeyMappingInterface, PersonPropertyFilter, PropertyDefinitionType, PropertyFilterType, + PropertyFilterValue, PropertyGroupFilter, PropertyGroupFilterValue, PropertyOperator, @@ -19,8 +23,87 @@ import { SessionPropertyFilter, } from '~/types' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { flattenPropertyGroup, isPropertyGroup } from 'lib/utils' +import { allOperatorsMapping, isOperatorFlag } from 'lib/utils' import { BreakdownFilter } from '~/queries/schema' +import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' + +export function isPropertyGroup( + properties: + | PropertyGroupFilter + | PropertyGroupFilterValue + | AnyPropertyFilter[] + | AnyPropertyFilter + | Record + | null + | undefined +): properties is PropertyGroupFilter { + return ( + (properties as PropertyGroupFilter)?.type !== undefined && + (properties as PropertyGroupFilter)?.values !== undefined + ) +} + +function flattenPropertyGroup( + flattenedProperties: AnyPropertyFilter[], + propertyGroup: PropertyGroupFilter | PropertyGroupFilterValue | AnyPropertyFilter +): AnyPropertyFilter[] { + const obj: AnyPropertyFilter = {} as EmptyPropertyFilter + Object.keys(propertyGroup).forEach(function (k) { + obj[k] = propertyGroup[k] + }) + if (isValidPropertyFilter(obj)) { + flattenedProperties.push(obj) + } + if (isPropertyGroup(propertyGroup)) { + return propertyGroup.values.reduce(flattenPropertyGroup, flattenedProperties) + } + return flattenedProperties +} + +export function convertPropertiesToPropertyGroup( + properties: PropertyGroupFilter | AnyPropertyFilter[] | undefined +): PropertyGroupFilter { + if (isPropertyGroup(properties)) { + return properties + } + if (properties && properties.length > 0) { + return { type: FilterLogicalOperator.And, values: [{ type: FilterLogicalOperator.And, values: properties }] } + } + return { type: FilterLogicalOperator.And, values: [] } +} + +/** Flatten a filter group into an array of filters. NB: Logical operators (AND/OR) are lost in the process. */ +export function convertPropertyGroupToProperties( + properties?: PropertyGroupFilter | AnyPropertyFilter[] +): AnyPropertyFilter[] | undefined { + if (isPropertyGroup(properties)) { + return flattenPropertyGroup([], properties).filter(isValidPropertyFilter) + } + if (properties) { + return properties.filter(isValidPropertyFilter) + } + return properties +} + +export function formatPropertyLabel( + item: Record, + cohortsById: Partial>, + keyMapping: KeyMappingInterface, + valueFormatter: (value: PropertyFilterValue | undefined) => string | string[] | null = (s) => [String(s)] +): string { + if (isHogQLPropertyFilter(item as AnyFilterLike)) { + return extractExpressionComment(item.key) + } + const { value, key, operator, type } = item + return type === 'cohort' + ? cohortsById[value]?.name || `ID ${value}` + : (keyMapping[type === 'element' ? 'element' : 'event'][key]?.label || key) + + (isOperatorFlag(operator) + ? ` ${allOperatorsMapping[operator]}` + : ` ${(allOperatorsMapping[operator || 'exact'] || '?').split(' ')[0]} ${ + value && value.length === 1 && value[0] === '' ? '(empty string)' : valueFormatter(value) || '' + } `) +} /** Make sure unverified user property filter input has at least a "type" */ export function sanitizePropertyFilter(propertyFilter: AnyPropertyFilter): AnyPropertyFilter { diff --git a/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts b/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts index 88d228ea88be8..a592cf444e94e 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts +++ b/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts @@ -4,8 +4,9 @@ import { PropertyGroupFilter, FilterLogicalOperator, EmptyPropertyFilter } from import { PropertyGroupFilterLogicProps } from 'lib/components/PropertyFilters/types' import type { propertyGroupFilterLogicType } from './propertyGroupFilterLogicType' -import { convertPropertiesToPropertyGroup, objectsEqual } from 'lib/utils' +import { objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { convertPropertiesToPropertyGroup } from '../PropertyFilters/utils' export const propertyGroupFilterLogic = kea([ path(['lib', 'components', 'PropertyGroupFilters', 'propertyGroupFilterLogic']), diff --git a/frontend/src/lib/components/RestrictedArea.tsx b/frontend/src/lib/components/RestrictedArea.tsx index c334a3a2235e6..a1ec4a419a06a 100644 --- a/frontend/src/lib/components/RestrictedArea.tsx +++ b/frontend/src/lib/components/RestrictedArea.tsx @@ -1,9 +1,9 @@ import { useValues } from 'kea' import { useMemo } from 'react' import { organizationLogic } from '../../scenes/organizationLogic' -import { OrganizationMembershipLevel } from '../constants' +import { EitherMembershipLevel, OrganizationMembershipLevel } from '../constants' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { EitherMembershipLevel, membershipLevelToName } from '../utils/permissioning' +import { membershipLevelToName } from '../utils/permissioning' import { isAuthenticatedTeam, teamLogic } from '../../scenes/teamLogic' export interface RestrictedComponentProps { diff --git a/frontend/src/lib/components/Sharing/SharingModal.tsx b/frontend/src/lib/components/Sharing/SharingModal.tsx index 57ce2eb06d509..0c61a40165bf0 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.tsx @@ -3,7 +3,7 @@ import { InsightModel, InsightShortId, InsightType } from '~/types' import { useActions, useValues } from 'kea' import { sharingLogic } from './sharingLogic' import { LemonButton, LemonSwitch } from '@posthog/lemon-ui' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { IconGlobeLock, IconInfo, IconLink, IconLock, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { DashboardCollaboration } from 'scenes/dashboard/DashboardCollaborators' diff --git a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts index b1fc22ef02e39..fbfde31c7e69f 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts @@ -4,7 +4,7 @@ import { SubscriptionType } from '~/types' import api from 'lib/api' import { loaders } from 'kea-loaders' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import type { subscriptionsLogicType } from './subscriptionsLogicType' import { getInsightId } from 'scenes/insights/utils' diff --git a/frontend/src/lib/components/UUIDShortener.tsx b/frontend/src/lib/components/UUIDShortener.tsx index c943725ba270b..133c0eedaf6f3 100644 --- a/frontend/src/lib/components/UUIDShortener.tsx +++ b/frontend/src/lib/components/UUIDShortener.tsx @@ -1,5 +1,5 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' export function truncateUuid(uuid: string): string { // Simple function to truncate a UUID. Useful for more simple displaying but should always be made clear it is truncated. diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index f8b3e96456bbc..712c7c77fa957 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -1,4 +1,3 @@ -import { urls } from 'scenes/urls' import { AvailableFeature, ChartDisplayType, LicensePlan, Region, SSOProvider } from '../types' /** Display types which don't allow grouping by unit of time. Sync with backend NON_TIME_SERIES_DISPLAY_TYPES. */ @@ -39,6 +38,8 @@ export enum TeamMembershipLevel { Admin = 8, } +export type EitherMembershipLevel = OrganizationMembershipLevel | TeamMembershipLevel + /** See posthog/api/organization.py for details. */ export enum PluginsAccessLevel { None = 0, @@ -239,10 +240,6 @@ export const SSO_PROVIDER_NAMES: Record = { saml: 'Single sign-on (SAML)', } -// TODO: Remove UPGRADE_LINK, as the billing page is now universal -export const UPGRADE_LINK = (cloud?: boolean): { url: string; target?: '_blank' } => - cloud ? { url: urls.organizationBilling() } : { url: 'https://posthog.com/pricing', target: '_blank' } - export const DOMAIN_REGEX = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/ export const SECURE_URL_REGEX = /^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=]+$/gi @@ -257,3 +254,9 @@ export const SESSION_RECORDINGS_PLAYLIST_FREE_COUNT = 5 export const AUTO_REFRESH_DASHBOARD_THRESHOLD_HOURS = 20 export const GENERATED_DASHBOARD_PREFIX = 'Generated Dashboard' + +export const ACTIVITY_PAGE_SIZE = 20 +export const EVENT_DEFINITIONS_PER_PAGE = 50 +export const PROPERTY_DEFINITIONS_PER_EVENT = 5 +export const EVENT_PROPERTY_DEFINITIONS_PER_PAGE = 50 +export const LOGS_PORTION_LIMIT = 50 diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index b66cbb3358bcb..a8f5774ab5b24 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -1,11 +1,6 @@ -import { KeyMapping, PropertyFilterValue } from '~/types' +import { KeyMapping, KeyMappingInterface, PropertyFilterValue } from '~/types' import { Link } from './lemon-ui/Link' -export interface KeyMappingInterface { - event: Record - element: Record -} - // If adding event properties with labels, check whether they should be added to // PROPERTY_NAME_ALIASES in posthog/api/property_definition.py // see code to output JSON below this diff --git a/frontend/src/lib/utils.test.ts b/frontend/src/lib/utils.test.ts index 49764e4fb3201..b9a49d899dc69 100644 --- a/frontend/src/lib/utils.test.ts +++ b/frontend/src/lib/utils.test.ts @@ -9,8 +9,6 @@ import { chooseOperatorMap, colonDelimitedDuration, compactNumber, - convertPropertiesToPropertyGroup, - convertPropertyGroupToProperties, dateFilterToText, dateMapping, dateStringToDayJs, @@ -20,7 +18,6 @@ import { ensureStringIsNotBlank, eventToDescription, floorMsToClosestSecond, - formatLabel, genericOperatorMap, getFormattedLastWeekDate, hexToRGBA, @@ -44,18 +41,7 @@ import { shortTimeZone, humanFriendlyLargeNumber, } from './utils' -import { - ActionFilter, - AnyPropertyFilter, - ElementType, - EventType, - FilterLogicalOperator, - PropertyFilterType, - PropertyGroupFilter, - PropertyOperator, - PropertyType, - TimeUnitType, -} from '~/types' +import { ElementType, EventType, PropertyType, TimeUnitType } from '~/types' import { dayjs } from 'lib/dayjs' describe('toParams', () => { @@ -111,46 +97,6 @@ describe('identifierToHuman()', () => { }) }) -describe('formatLabel()', () => { - const action: ActionFilter = { - id: 123, - name: 'Test Action', - properties: [], - type: 'actions', - } - - it('formats the label', () => { - expect(formatLabel('some_event', action)).toEqual('some_event') - }) - - it('DAU queries', () => { - expect(formatLabel('some_event', { ...action, math: 'dau' })).toEqual('some_event (Unique users)') - }) - - it('summing by property', () => { - expect(formatLabel('some_event', { ...action, math: 'sum', math_property: 'event_property' })).toEqual( - 'some_event (sum of event_property)' - ) - }) - - it('action with properties', () => { - expect( - formatLabel('some_event', { - ...action, - properties: [ - { - value: 'hello', - key: 'greeting', - operator: PropertyOperator.Exact, - type: PropertyFilterType.Person, - }, - { operator: PropertyOperator.GreaterThan, value: 5, key: '', type: PropertyFilterType.Person }, - ], - }) - ).toEqual('some_event (greeting = hello, > 5)') - }) -}) - describe('midEllipsis()', () => { it('returns same string if short', () => { expect(midEllipsis('12', 10)).toEqual('12') @@ -754,70 +700,6 @@ describe('{floor|ceil}MsToClosestSecond()', () => { }) }) -describe('convertPropertyGroupToProperties()', () => { - it('converts a single layer property group into an array of properties', () => { - const propertyGroup = { - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [ - { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ] as AnyPropertyFilter[], - }, - { - type: FilterLogicalOperator.And, - values: [ - { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ] as AnyPropertyFilter[], - }, - ], - } - expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([ - { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ]) - }) - - it('converts a deeply nested property group into an array of properties', () => { - const propertyGroup: PropertyGroupFilter = { - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [{ type: FilterLogicalOperator.And, values: [{ key: '$lib' } as any] }], - }, - { type: FilterLogicalOperator.And, values: [{ key: '$browser' } as any] }, - ], - } - expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([{ key: '$lib' }, { key: '$browser' }]) - }) -}) - -describe('convertPropertiesToPropertyGroup', () => { - it('converts properties to one AND operator property group', () => { - const properties: any[] = [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }] - expect(convertPropertiesToPropertyGroup(properties)).toEqual({ - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }], - }, - ], - }) - }) - - it('converts properties to one AND operator property group', () => { - expect(convertPropertiesToPropertyGroup(undefined)).toEqual({ - type: FilterLogicalOperator.And, - values: [], - }) - }) -}) - describe('calculateDays', () => { it('1 day to 1 day', () => { expect(calculateDays(1, TimeUnitType.Day)).toEqual(1) diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 4daea66d2c7dc..26a5efa5e7652 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1,54 +1,27 @@ import { CSSProperties } from 'react' -import api from './api' import { - ActionFilter, ActionType, ActorType, - AnyCohortCriteriaType, - AnyFilterLike, - AnyFilterType, - AnyPropertyFilter, - BehavioralCohortType, - BehavioralEventType, - ChartDisplayType, - CohortCriteriaGroupFilter, - CohortType, DateMappingOption, - EmptyPropertyFilter, EventType, - FilterLogicalOperator, - FunnelVizType, GroupActorType, - InsightType, - IntervalType, - PropertyFilterValue, - PropertyGroupFilter, - PropertyGroupFilterValue, PropertyOperator, PropertyType, TimeUnitType, - TrendsFilterType, } from '~/types' import * as Sentry from '@sentry/react' import equal from 'fast-deep-equal' import { tagColors } from 'lib/colors' -import { NON_TIME_SERIES_DISPLAY_TYPES, WEBHOOK_SERVICES } from 'lib/constants' -import { KeyMappingInterface } from 'lib/taxonomy' +import { WEBHOOK_SERVICES } from 'lib/constants' import { AlignType } from 'rc-trigger/lib/interface' import { dayjs } from 'lib/dayjs' import { getAppContext } from './utils/getAppContext' -import { - isHogQLPropertyFilter, - isPropertyFilterWithOperator, - isValidPropertyFilter, -} from './components/PropertyFilters/utils' -import { IconCopy } from 'lib/lemon-ui/icons' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' -import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' -import { urls } from 'scenes/urls' -import { isFunnelsFilter } from 'scenes/insights/sharedUtils' -import { CUSTOM_OPTION_KEY } from './components/DateFilter/dateFilterLogic' +import { CUSTOM_OPTION_KEY } from './components/DateFilter/types' + +/** + * WARNING: Be very careful importing things here. This file is heavily used and can trigger a lot of cyclic imports + * Preferably create a dedicated file in utils/.. + */ export const ANTD_TOOLTIP_PLACEMENTS: Record = { // `@yiminghe/dom-align` objects @@ -182,38 +155,6 @@ export function percentage( }) } -export async function deleteWithUndo>({ - undo = false, - ...props -}: { - undo?: boolean - endpoint: string - object: T - idField?: keyof T - callback?: (undo: boolean, object: T) => void -}): Promise { - await api.update(`api/${props.endpoint}/${props.object[props.idField || 'id']}`, { - ...props.object, - deleted: !undo, - }) - props.callback?.(undo, props.object) - lemonToast[undo ? 'success' : 'info']( - <> - {props.object.name || {props.object.derived_name || 'Unnamed'}} has been{' '} - {undo ? 'restored' : 'deleted'} - , - { - toastId: `delete-item-${props.object.id}-${undo}`, - button: undo - ? undefined - : { - label: 'Undo', - action: () => deleteWithUndo({ undo: true, ...props }), - }, - } - ) -} - export const selectStyle: Record) => Partial> = { control: (base) => ({ ...base, @@ -365,50 +306,6 @@ export function isOperatorDate(operator: PropertyOperator): boolean { ) } -export function formatPropertyLabel( - item: Record, - cohortsById: Partial>, - keyMapping: KeyMappingInterface, - valueFormatter: (value: PropertyFilterValue | undefined) => string | string[] | null = (s) => [String(s)] -): string { - if (isHogQLPropertyFilter(item as AnyFilterLike)) { - return extractExpressionComment(item.key) - } - const { value, key, operator, type } = item - return type === 'cohort' - ? cohortsById[value]?.name || `ID ${value}` - : (keyMapping[type === 'element' ? 'element' : 'event'][key]?.label || key) + - (isOperatorFlag(operator) - ? ` ${allOperatorsMapping[operator]}` - : ` ${(allOperatorsMapping[operator || 'exact'] || '?').split(' ')[0]} ${ - value && value.length === 1 && value[0] === '' ? '(empty string)' : valueFormatter(value) || '' - } `) -} - -/** Format a label that gets returned from the /insights api */ -export function formatLabel(label: string, action: ActionFilter): string { - if (action.math === 'dau') { - label += ` (Unique users) ` - } else if (action.math === 'hogql') { - label += ` (${action.math_hogql})` - } else if (['sum', 'avg', 'min', 'max', 'median', 'p90', 'p95', 'p99'].includes(action.math || '')) { - label += ` (${action.math} of ${action.math_property}) ` - } - if (action.properties?.length) { - label += ` (${action.properties - .map( - (property) => - `${property.key ? `${property.key} ` : ''}${ - allOperatorsMapping[ - (isPropertyFilterWithOperator(property) && property.operator) || 'exact' - ].split(' ')[0] - } ${property.value}` - ) - .join(', ')})` - } - return label.trim() -} - /** Compare objects deeply. */ export function objectsEqual(obj1: any, obj2: any): boolean { return equal(obj1, obj2) @@ -1068,38 +965,6 @@ export function dateStringToDayJs(date: string | null): dayjs.Dayjs | null { return response } -export async function copyToClipboard(value: string, description: string = 'text'): Promise { - if (!navigator.clipboard) { - lemonToast.warning('Oops! Clipboard capabilities are only available over HTTPS or on localhost') - return false - } - - try { - await navigator.clipboard.writeText(value) - lemonToast.info(`Copied ${description} to clipboard`, { - icon: , - }) - return true - } catch (e) { - // If the Clipboard API fails, fallback to textarea method - try { - const textArea = document.createElement('textarea') - textArea.value = value - document.body.appendChild(textArea) - textArea.select() - document.execCommand('copy') - document.body.removeChild(textArea) - lemonToast.info(`Copied ${description} to clipboard`, { - icon: , - }) - return true - } catch (err) { - lemonToast.error(`Could not copy ${description} to clipboard: ${err}`) - return false - } - } -} - export function clamp(value: number, min: number, max: number): number { return value > max ? max : value < min ? min : value } @@ -1258,46 +1123,6 @@ export function midEllipsis(input: string, maxLength: number): string { return `${input.slice(0, middle - excessLeft)}…${input.slice(middle + excessRight)}` } -export const disableHourFor: Record = { - dStart: false, - '-1d': false, - '-7d': false, - '-14d': false, - '-30d': false, - '-90d': true, - mStart: false, - '-1mStart': false, - yStart: true, - all: true, - other: false, -} - -export function autocorrectInterval(filters: Partial): IntervalType | undefined { - if ('display' in filters && filters.display && NON_TIME_SERIES_DISPLAY_TYPES.includes(filters.display)) { - // Non-time-series insights should not have an interval - return undefined - } - if (isFunnelsFilter(filters) && filters.funnel_viz_type !== FunnelVizType.Trends) { - // Only trend funnels support intervals - return undefined - } - if (!filters.interval) { - return 'day' - } - - // @ts-expect-error - Old legacy interval support - const minute_disabled = filters.interval === 'minute' - const hour_disabled = disableHourFor[filters.date_from || 'other'] && filters.interval === 'hour' - - if (minute_disabled) { - return 'hour' - } else if (hour_disabled) { - return 'day' - } else { - return filters.interval - } -} - export function pluralize(count: number, singular: string, plural?: string, includeNumber: boolean = true): string { if (!plural) { plural = singular + 's' @@ -1544,64 +1369,6 @@ export function getEventNamesForAction(actionId: string | number, allActions: Ac .flatMap((a) => a.steps?.filter((step) => step.event).map((step) => String(step.event)) as string[]) } -export function isPropertyGroup( - properties: - | PropertyGroupFilter - | PropertyGroupFilterValue - | AnyPropertyFilter[] - | AnyPropertyFilter - | Record - | null - | undefined -): properties is PropertyGroupFilter { - return ( - (properties as PropertyGroupFilter)?.type !== undefined && - (properties as PropertyGroupFilter)?.values !== undefined - ) -} - -export function flattenPropertyGroup( - flattenedProperties: AnyPropertyFilter[], - propertyGroup: PropertyGroupFilter | PropertyGroupFilterValue | AnyPropertyFilter -): AnyPropertyFilter[] { - const obj: AnyPropertyFilter = {} as EmptyPropertyFilter - Object.keys(propertyGroup).forEach(function (k) { - obj[k] = propertyGroup[k] - }) - if (isValidPropertyFilter(obj)) { - flattenedProperties.push(obj) - } - if (isPropertyGroup(propertyGroup)) { - return propertyGroup.values.reduce(flattenPropertyGroup, flattenedProperties) - } - return flattenedProperties -} - -export function convertPropertiesToPropertyGroup( - properties: PropertyGroupFilter | AnyPropertyFilter[] | undefined -): PropertyGroupFilter { - if (isPropertyGroup(properties)) { - return properties - } - if (properties && properties.length > 0) { - return { type: FilterLogicalOperator.And, values: [{ type: FilterLogicalOperator.And, values: properties }] } - } - return { type: FilterLogicalOperator.And, values: [] } -} - -/** Flatten a filter group into an array of filters. NB: Logical operators (AND/OR) are lost in the process. */ -export function convertPropertyGroupToProperties( - properties?: PropertyGroupFilter | AnyPropertyFilter[] -): AnyPropertyFilter[] | undefined { - if (isPropertyGroup(properties)) { - return flattenPropertyGroup([], properties).filter(isValidPropertyFilter) - } - if (properties) { - return properties.filter(isValidPropertyFilter) - } - return properties -} - export const isUserLoggedIn = (): boolean => !getAppContext()?.anonymous /** Sorting function for Array.prototype.sort that works for numbers and strings automatically. */ @@ -1711,41 +1478,6 @@ export function range(startOrEnd: number, end?: number): number[] { return Array.from({ length }, (_, i) => i + start) } -export function processCohort(cohort: CohortType): CohortType { - return { - ...cohort, - ...{ - /* Populate value_property with value and overwrite value with corresponding behavioral filter type */ - filters: { - properties: { - ...cohort.filters.properties, - values: (cohort.filters.properties?.values?.map((group) => - 'values' in group - ? { - ...group, - values: (group.values as AnyCohortCriteriaType[]).map((c) => - c.type && - [BehavioralFilterKey.Cohort, BehavioralFilterKey.Person].includes(c.type) && - !('value_property' in c) - ? { - ...c, - value_property: c.value, - value: - c.type === BehavioralFilterKey.Cohort - ? BehavioralCohortType.InCohort - : BehavioralEventType.HaveProperty, - } - : c - ), - } - : group - ) ?? []) as CohortCriteriaGroupFilter[] | AnyCohortCriteriaType[], - }, - }, - }, - } -} - export function interleave(arr: any[], delimiter: any): any[] { return arr.flatMap((item, index, _arr) => _arr.length - 1 !== index // check for the last item @@ -1773,51 +1505,6 @@ export function downloadFile(file: File): void { }, 0) } -export function insightUrlForEvent(event: Pick): string | undefined { - let insightParams: Partial | undefined - if (event.event === '$pageview') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: '$pageview', - name: '$pageview', - type: 'events', - order: 0, - properties: [ - { - key: '$current_url', - value: event.properties.$current_url, - type: 'event', - }, - ], - }, - ], - } - } else if (event.event !== '$autocapture') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: event.event, - name: event.event, - type: 'events', - order: 0, - properties: [], - }, - ], - } - } - - return insightParams ? urls.insightNew(insightParams) : undefined -} - export function inStorybookTestRunner(): boolean { return navigator.userAgent.includes('StorybookTestRunner') } diff --git a/frontend/src/lib/utils/copyToClipboard.tsx b/frontend/src/lib/utils/copyToClipboard.tsx new file mode 100644 index 0000000000000..b29ec0dbbe14b --- /dev/null +++ b/frontend/src/lib/utils/copyToClipboard.tsx @@ -0,0 +1,34 @@ +import { IconCopy } from '@posthog/icons' +import { lemonToast } from '@posthog/lemon-ui' + +export async function copyToClipboard(value: string, description: string = 'text'): Promise { + if (!navigator.clipboard) { + lemonToast.warning('Oops! Clipboard capabilities are only available over HTTPS or on localhost') + return false + } + + try { + await navigator.clipboard.writeText(value) + lemonToast.info(`Copied ${description} to clipboard`, { + icon: , + }) + return true + } catch (e) { + // If the Clipboard API fails, fallback to textarea method + try { + const textArea = document.createElement('textarea') + textArea.value = value + document.body.appendChild(textArea) + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + lemonToast.info(`Copied ${description} to clipboard`, { + icon: , + }) + return true + } catch (err) { + lemonToast.error(`Could not copy ${description} to clipboard: ${err}`) + return false + } + } +} diff --git a/frontend/src/lib/utils/deleteWithUndo.tsx b/frontend/src/lib/utils/deleteWithUndo.tsx new file mode 100644 index 0000000000000..023a0c1bc4ad2 --- /dev/null +++ b/frontend/src/lib/utils/deleteWithUndo.tsx @@ -0,0 +1,34 @@ +import { lemonToast } from '@posthog/lemon-ui' +import api from 'lib/api' + +export async function deleteWithUndo>({ + undo = false, + ...props +}: { + undo?: boolean + endpoint: string + object: T + idField?: keyof T + callback?: (undo: boolean, object: T) => void +}): Promise { + await api.update(`api/${props.endpoint}/${props.object[props.idField || 'id']}`, { + ...props.object, + deleted: !undo, + }) + props.callback?.(undo, props.object) + lemonToast[undo ? 'success' : 'info']( + <> + {props.object.name || {props.object.derived_name || 'Unnamed'}} has been{' '} + {undo ? 'restored' : 'deleted'} + , + { + toastId: `delete-item-${props.object.id}-${undo}`, + button: undo + ? undefined + : { + label: 'Undo', + action: () => deleteWithUndo({ undo: true, ...props }), + }, + } + ) +} diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 5ce1faefa6d13..a94f5989178e8 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -31,7 +31,6 @@ import { } from '~/types' import type { Dayjs } from 'lib/dayjs' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { convertPropertyGroupToProperties } from 'lib/utils' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { now } from 'lib/dayjs' import { @@ -42,7 +41,7 @@ import { isStickinessFilter, isTrendsFilter, } from 'scenes/insights/sharedUtils' -import { isGroupPropertyFilter } from 'lib/components/PropertyFilters/utils' +import { convertPropertyGroupToProperties, isGroupPropertyFilter } from 'lib/components/PropertyFilters/utils' import { EventIndex } from 'scenes/session-recordings/player/eventIndex' import { SurveyTemplateType } from 'scenes/surveys/constants' diff --git a/frontend/src/lib/utils/permissioning.ts b/frontend/src/lib/utils/permissioning.ts index bbd0ca3d0f999..0ec496aaa4da4 100644 --- a/frontend/src/lib/utils/permissioning.ts +++ b/frontend/src/lib/utils/permissioning.ts @@ -1,8 +1,5 @@ -import { ExplicitTeamMemberType, OrganizationMemberType, UserType } from '../../types' -import { OrganizationMembershipLevel, TeamMembershipLevel } from '../constants' - -export type EitherMembershipLevel = OrganizationMembershipLevel | TeamMembershipLevel -export type EitherMemberType = OrganizationMemberType | ExplicitTeamMemberType +import { EitherMemberType, ExplicitTeamMemberType, OrganizationMemberType, UserType } from '../../types' +import { EitherMembershipLevel, OrganizationMembershipLevel, TeamMembershipLevel } from '../constants' /** If access level change is disallowed given the circumstances, returns a reason why so. Otherwise returns null. */ export function getReasonForAccessLevelChangeProhibition( diff --git a/frontend/src/models/annotationsModel.ts b/frontend/src/models/annotationsModel.ts index d193e56cee6b3..c22abb4754971 100644 --- a/frontend/src/models/annotationsModel.ts +++ b/frontend/src/models/annotationsModel.ts @@ -1,6 +1,6 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import api from 'lib/api' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import type { annotationsModelType } from './annotationsModelType' import { RawAnnotationType, AnnotationType } from '~/types' import { loaders } from 'kea-loaders' diff --git a/frontend/src/models/cohortsModel.ts b/frontend/src/models/cohortsModel.ts index 970458bb44be9..13f994ab31b95 100644 --- a/frontend/src/models/cohortsModel.ts +++ b/frontend/src/models/cohortsModel.ts @@ -2,16 +2,59 @@ import { loaders } from 'kea-loaders' import { kea, path, connect, actions, reducers, selectors, listeners, beforeUnmount, afterMount } from 'kea' import api from 'lib/api' import type { cohortsModelType } from './cohortsModelType' -import { CohortType, ExporterFormat } from '~/types' +import { + AnyCohortCriteriaType, + BehavioralCohortType, + BehavioralEventType, + CohortCriteriaGroupFilter, + CohortType, + ExporterFormat, +} from '~/types' import { personsLogic } from 'scenes/persons/personsLogic' -import { deleteWithUndo, processCohort } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { triggerExport } from 'lib/components/ExportButton/exporter' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' import Fuse from 'fuse.js' import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' const POLL_TIMEOUT = 5000 +export function processCohort(cohort: CohortType): CohortType { + return { + ...cohort, + ...{ + /* Populate value_property with value and overwrite value with corresponding behavioral filter type */ + filters: { + properties: { + ...cohort.filters.properties, + values: (cohort.filters.properties?.values?.map((group) => + 'values' in group + ? { + ...group, + values: (group.values as AnyCohortCriteriaType[]).map((c) => + c.type && + [BehavioralFilterKey.Cohort, BehavioralFilterKey.Person].includes(c.type) && + !('value_property' in c) + ? { + ...c, + value_property: c.value, + value: + c.type === BehavioralFilterKey.Cohort + ? BehavioralCohortType.InCohort + : BehavioralEventType.HaveProperty, + } + : c + ), + } + : group + ) ?? []) as CohortCriteriaGroupFilter[] | AnyCohortCriteriaType[], + }, + }, + }, + } +} + export const cohortsModel = kea([ path(['models', 'cohortsModel']), connect({ diff --git a/frontend/src/models/notebooksModel.ts b/frontend/src/models/notebooksModel.ts index f59f697b991d2..70936e1c6c8bd 100644 --- a/frontend/src/models/notebooksModel.ts +++ b/frontend/src/models/notebooksModel.ts @@ -6,7 +6,7 @@ import { DashboardType, NotebookListItemType, NotebookNodeType, NotebookTarget } import api from 'lib/api' import posthog from 'posthog-js' import { LOCAL_NOTEBOOK_TEMPLATES } from 'scenes/notebooks/NotebookTemplates/notebookTemplates' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { teamLogic } from 'scenes/teamLogic' import { defaultNotebookContent, EditorFocusPosition, JSONContent } from 'scenes/notebooks/Notebook/utils' diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 9c9cead143b70..6965548e4afac 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -17,7 +17,7 @@ import { useValues } from 'kea' import { LemonDivider, lemonToast } from '@posthog/lemon-ui' import { asDisplay } from 'scenes/persons/person-utils' import { urls } from 'scenes/urls' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' const EXPORT_MAX_LIMIT = 10000 diff --git a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx index 4b74e14e09d6a..bcf63f7cb7eff 100644 --- a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx +++ b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx @@ -8,8 +8,9 @@ import { teamLogic } from 'scenes/teamLogic' import { IconLink, IconPlayCircle } from 'lib/lemon-ui/icons' import { useActions } from 'kea' import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' -import { copyToClipboard, insightUrlForEvent } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { dayjs } from 'lib/dayjs' +import { insightUrlForEvent } from 'scenes/insights/utils' interface EventActionProps { event: EventType diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts index 772c97ddd7f75..aa99e6a2b2a22 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts @@ -3,9 +3,10 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers, sele import { PropertyGroupFilter, FilterLogicalOperator, EmptyPropertyFilter } from '~/types' import type { propertyGroupFilterLogicType } from './propertyGroupFilterLogicType' -import { convertPropertiesToPropertyGroup, objectsEqual } from 'lib/utils' +import { objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { StickinessQuery, TrendsQuery } from '~/queries/schema' +import { convertPropertiesToPropertyGroup } from 'lib/components/PropertyFilters/utils' export type PropertyGroupFilterLogicProps = { pageKey: string diff --git a/frontend/src/scenes/actions/actionEditLogic.tsx b/frontend/src/scenes/actions/actionEditLogic.tsx index e1763cdaa0d29..980461d56e80c 100644 --- a/frontend/src/scenes/actions/actionEditLogic.tsx +++ b/frontend/src/scenes/actions/actionEditLogic.tsx @@ -1,6 +1,7 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers } from 'kea' import api from 'lib/api' -import { deleteWithUndo, uuid } from 'lib/utils' +import { uuid } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { actionsModel } from '~/models/actionsModel' import type { actionEditLogicType } from './actionEditLogicType' import { ActionStepType, ActionType } from '~/types' diff --git a/frontend/src/scenes/apps/AppMetricsGraph.tsx b/frontend/src/scenes/apps/AppMetricsGraph.tsx index b8bb316a2bf3f..f816ce47b66ea 100644 --- a/frontend/src/scenes/apps/AppMetricsGraph.tsx +++ b/frontend/src/scenes/apps/AppMetricsGraph.tsx @@ -6,7 +6,8 @@ import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import './AppMetricsGraph.scss' import { inStorybookTestRunner, lightenDarkenColor } from 'lib/utils' -import { AppMetrics, AppMetricsTab } from './appMetricsSceneLogic' +import { AppMetrics } from './appMetricsSceneLogic' +import { AppMetricsTab } from '~/types' export interface AppMetricsGraphProps { tab: AppMetricsTab diff --git a/frontend/src/scenes/apps/AppMetricsScene.tsx b/frontend/src/scenes/apps/AppMetricsScene.tsx index 74b847872f39e..7b4c62f3d1b83 100644 --- a/frontend/src/scenes/apps/AppMetricsScene.tsx +++ b/frontend/src/scenes/apps/AppMetricsScene.tsx @@ -1,5 +1,5 @@ import { SceneExport } from 'scenes/sceneTypes' -import { appMetricsSceneLogic, AppMetricsTab } from 'scenes/apps/appMetricsSceneLogic' +import { appMetricsSceneLogic } from 'scenes/apps/appMetricsSceneLogic' import { PageHeader } from 'lib/components/PageHeader' import { useValues, useActions } from 'kea' import { MetricsTab } from './MetricsTab' @@ -15,6 +15,7 @@ import { AppLogsTab } from './AppLogsTab' import { LemonButton } from '@posthog/lemon-ui' import { IconSettings } from 'lib/lemon-ui/icons' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { AppMetricsTab } from '~/types' export const scene: SceneExport = { component: AppMetrics, diff --git a/frontend/src/scenes/apps/HistoricalExport.tsx b/frontend/src/scenes/apps/HistoricalExport.tsx index e32cadc276244..0642110b3b3c9 100644 --- a/frontend/src/scenes/apps/HistoricalExport.tsx +++ b/frontend/src/scenes/apps/HistoricalExport.tsx @@ -1,9 +1,9 @@ import { Card } from 'antd' import { useValues } from 'kea' import { AppMetricsGraph } from './AppMetricsGraph' -import { AppMetricsTab } from './appMetricsSceneLogic' import { historicalExportLogic, HistoricalExportLogicProps } from './historicalExportLogic' import { ErrorsOverview, MetricsOverview } from './MetricsTab' +import { AppMetricsTab } from '~/types' export function HistoricalExport(props: HistoricalExportLogicProps): JSX.Element { const { data, dataLoading } = useValues(historicalExportLogic(props)) diff --git a/frontend/src/scenes/apps/MetricsTab.tsx b/frontend/src/scenes/apps/MetricsTab.tsx index 0af2e7984c814..425219384c912 100644 --- a/frontend/src/scenes/apps/MetricsTab.tsx +++ b/frontend/src/scenes/apps/MetricsTab.tsx @@ -1,4 +1,4 @@ -import { AppErrorSummary, AppMetrics, appMetricsSceneLogic, AppMetricsTab } from './appMetricsSceneLogic' +import { AppErrorSummary, AppMetrics, appMetricsSceneLogic } from './appMetricsSceneLogic' import { DescriptionColumns } from './constants' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { humanFriendlyDuration, humanFriendlyNumber } from 'lib/utils' @@ -10,6 +10,7 @@ import { TZLabel } from 'lib/components/TZLabel' import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { IconInfo } from 'lib/lemon-ui/icons' +import { AppMetricsTab } from '~/types' export interface MetricsTabProps { tab: AppMetricsTab diff --git a/frontend/src/scenes/apps/appMetricsSceneLogic.ts b/frontend/src/scenes/apps/appMetricsSceneLogic.ts index 1f68e22236849..577b3624e6287 100644 --- a/frontend/src/scenes/apps/appMetricsSceneLogic.ts +++ b/frontend/src/scenes/apps/appMetricsSceneLogic.ts @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders' import type { appMetricsSceneLogicType } from './appMetricsSceneLogicType' import { urls } from 'scenes/urls' -import { Breadcrumb, PluginConfigWithPluginInfo, UserBasicType } from '~/types' +import { AppMetricsTab, AppMetricsUrlParams, Breadcrumb, PluginConfigWithPluginInfo, UserBasicType } from '~/types' import api, { PaginatedResponse } from 'lib/api' import { teamLogic } from 'scenes/teamLogic' import { actionToUrl, urlToAction } from 'kea-router' @@ -19,22 +19,6 @@ export interface AppMetricsLogicProps { pluginConfigId: number } -export interface AppMetricsUrlParams { - tab?: AppMetricsTab - from?: string - error?: [string, string] -} - -export enum AppMetricsTab { - Logs = 'logs', - ProcessEvent = 'processEvent', - OnEvent = 'onEvent', - ComposeWebhook = 'composeWebhook', - ExportEvents = 'exportEvents', - ScheduledTask = 'scheduledTask', - HistoricalExports = 'historical_exports', - History = 'history', -} export const TabsWithMetrics = [ AppMetricsTab.ProcessEvent, AppMetricsTab.OnEvent, diff --git a/frontend/src/scenes/apps/constants.tsx b/frontend/src/scenes/apps/constants.tsx index 9870936b5cfc5..fb5077b3df336 100644 --- a/frontend/src/scenes/apps/constants.tsx +++ b/frontend/src/scenes/apps/constants.tsx @@ -1,4 +1,4 @@ -import { AppMetricsTab } from './appMetricsSceneLogic' +import { AppMetricsTab } from '~/types' interface Description { successes: string diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.ts b/frontend/src/scenes/cohorts/cohortEditLogic.ts index 6c836e0a32887..58b7d6af4c111 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.ts @@ -1,6 +1,6 @@ import { actions, afterMount, beforeUnmount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import api from 'lib/api' -import { cohortsModel } from '~/models/cohortsModel' +import { cohortsModel, processCohort } from '~/models/cohortsModel' import { ENTITY_MATCH_TYPE, FEATURE_FLAGS } from 'lib/constants' import { AnyCohortCriteriaType, @@ -27,7 +27,6 @@ import { } from 'scenes/cohorts/cohortUtils' import { NEW_COHORT, NEW_CRITERIA, NEW_CRITERIA_GROUP } from 'scenes/cohorts/CohortFilters/constants' import type { cohortEditLogicType } from './cohortEditLogicType' -import { processCohort } from 'lib/utils' import { DataTableNode, Node, NodeKind } from '~/queries/schema' import { isDataTableNode } from '~/queries/utils' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' diff --git a/frontend/src/scenes/data-management/actions/ActionsTable.tsx b/frontend/src/scenes/data-management/actions/ActionsTable.tsx index d7255f347ba9f..3ab88c29bb0be 100644 --- a/frontend/src/scenes/data-management/actions/ActionsTable.tsx +++ b/frontend/src/scenes/data-management/actions/ActionsTable.tsx @@ -1,5 +1,6 @@ import { Link } from 'lib/lemon-ui/Link' -import { deleteWithUndo, stripHTTP } from 'lib/utils' +import { stripHTTP } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { useActions, useValues } from 'kea' import { actionsModel } from '~/models/actionsModel' import { NewActionButton } from '../../actions/NewActionButton' diff --git a/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx b/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx index 70a5b6a88a8e8..4d1134eb60133 100644 --- a/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx +++ b/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx @@ -1,14 +1,12 @@ import { useActions, useValues } from 'kea' import { useEffect } from 'react' -import { - eventDefinitionsTableLogic, - PROPERTY_DEFINITIONS_PER_EVENT, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { EventDefinition, PropertyDefinition } from '~/types' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { organizationLogic } from 'scenes/organizationLogic' import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' +import { PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' export function EventDefinitionProperties({ definition }: { definition: EventDefinition }): JSX.Element { const { loadPropertiesForEvent } = useActions(eventDefinitionsTableLogic) diff --git a/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx b/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx index 7571d91bf5731..54a3bd16b3086 100644 --- a/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx +++ b/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx @@ -2,10 +2,7 @@ import './EventDefinitionsTable.scss' import { useActions, useValues } from 'kea' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { EventDefinition, EventDefinitionType } from '~/types' -import { - EVENT_DEFINITIONS_PER_PAGE, - eventDefinitionsTableLogic, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { organizationLogic } from 'scenes/organizationLogic' import { EventDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' @@ -17,6 +14,7 @@ import { combineUrl } from 'kea-router' import { IconPlayCircle } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { TZLabel } from 'lib/components/TZLabel' +import { EVENT_DEFINITIONS_PER_PAGE } from 'lib/constants' const eventTypeOptions: LemonSelectOptions = [ { value: EventDefinitionType.Event, label: 'All events', 'data-attr': 'event-type-option-event' }, diff --git a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts index 991d4c0b7d5ff..d4b4e66cfe05b 100644 --- a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts +++ b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts @@ -1,9 +1,5 @@ import { initKeaTests } from '~/test/init' -import { - EVENT_DEFINITIONS_PER_PAGE, - eventDefinitionsTableLogic, - PROPERTY_DEFINITIONS_PER_EVENT, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { api, MOCK_TEAM_ID } from 'lib/api.mock' import { expectLogic, partial } from 'kea-test-utils' import { mockEvent, mockEventDefinitions, mockEventPropertyDefinitions } from '~/test/mocks' @@ -13,6 +9,7 @@ import { combineUrl, router } from 'kea-router' import { keyMappingKeys } from 'lib/taxonomy' import { urls } from 'scenes/urls' import { EventDefinitionType } from '~/types' +import { EVENT_DEFINITIONS_PER_PAGE, PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' describe('eventDefinitionsTableLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts index af7c766e1889f..ea35ce72e4dfe 100644 --- a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts +++ b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts @@ -4,9 +4,11 @@ import type { eventDefinitionsTableLogicType } from './eventDefinitionsTableLogi import api, { PaginatedResponse } from 'lib/api' import { keyMappingKeys } from 'lib/taxonomy' import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' -import { convertPropertyGroupToProperties, objectsEqual } from 'lib/utils' +import { objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { loaders } from 'kea-loaders' +import { EVENT_DEFINITIONS_PER_PAGE, PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' export interface EventDefinitionsPaginatedResponse extends PaginatedResponse { current?: string @@ -37,9 +39,6 @@ function cleanFilters(filter: Partial): Filters { } } -export const EVENT_DEFINITIONS_PER_PAGE = 50 -export const PROPERTY_DEFINITIONS_PER_EVENT = 5 - export function createDefinitionKey(event?: EventDefinition, property?: PropertyDefinition): string { return `${event?.id ?? 'event'}-${property?.id ?? 'property'}` } diff --git a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx index 19a9d8429a0db..aa9de92bec92d 100644 --- a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx +++ b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx @@ -5,13 +5,11 @@ import { PropertyDefinition } from '~/types' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { organizationLogic } from 'scenes/organizationLogic' import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' -import { - EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - propertyDefinitionsTableLogic, -} from 'scenes/data-management/properties/propertyDefinitionsTableLogic' +import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' import { LemonInput, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { urls } from 'scenes/urls' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' export function PropertyDefinitionsTable(): JSX.Element { const { propertyDefinitions, propertyDefinitionsLoading, filters, propertyTypeOptions } = diff --git a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts index 97f6abf271d0b..8d9258bb18820 100644 --- a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts +++ b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts @@ -5,11 +5,9 @@ import { mockEventPropertyDefinitions } from '~/test/mocks' import { useMocks } from '~/mocks/jest' import { organizationLogic } from 'scenes/organizationLogic' import { combineUrl, router } from 'kea-router' -import { - EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - propertyDefinitionsTableLogic, -} from 'scenes/data-management/properties/propertyDefinitionsTableLogic' +import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' import { urls } from 'scenes/urls' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' describe('propertyDefinitionsTableLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts index 3aa493789acb8..ea8b7a199975f 100644 --- a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts +++ b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts @@ -14,6 +14,7 @@ import { urls } from 'scenes/urls' import type { propertyDefinitionsTableLogicType } from './propertyDefinitionsTableLogicType' import { groupsModel } from '../../../models/groupsModel' import { LemonSelectOption } from 'lib/lemon-ui/LemonSelect' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' export interface Filters { property: string @@ -38,8 +39,6 @@ function removeDefaults(filter: Filters): Partial { } } -export const EVENT_PROPERTY_DEFINITIONS_PER_PAGE = 50 - export interface PropertyDefinitionsTableLogicProps { key: string } diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx index 168b307e29935..11d16e85e8d3f 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx @@ -4,7 +4,7 @@ import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonButton } from '@posthog/lemon-ui' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { teamLogic } from 'scenes/teamLogic' import { DataWarehouseSceneRow } from '../types' diff --git a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx index c45ec66c3431b..25f506a4df903 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx @@ -3,7 +3,7 @@ import { DatabaseTables } from 'scenes/data-management/database/DatabaseTables' import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonButton, Link } from '@posthog/lemon-ui' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { teamLogic } from 'scenes/teamLogic' import { DataWarehouseSceneRow } from '../types' import { dataWarehouseSavedQueriesLogic } from './dataWarehouseSavedQueriesLogic' diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index 55ed1d2af5636..6320ccefe3a45 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -1,7 +1,8 @@ import { useActions, useValues } from 'kea' import { featureFlagsLogic, FeatureFlagsTab } from './featureFlagsLogic' import { Link } from 'lib/lemon-ui/Link' -import { copyToClipboard, deleteWithUndo } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { PageHeader } from 'lib/components/PageHeader' import { AnyPropertyFilter, AvailableFeature, FeatureFlagFilters, FeatureFlagType, ProductKey } from '~/types' import { normalizeColumnTitle } from 'lib/components/Table/utils' diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index 3a232a5a25947..2c34da9925380 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -25,7 +25,8 @@ import { } from '~/types' import api from 'lib/api' import { router, urlToAction } from 'kea-router' -import { convertPropertyGroupToProperties, deleteWithUndo, sum, toParams } from 'lib/utils' +import { sum, toParams } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { urls } from 'scenes/urls' import { teamLogic } from '../teamLogic' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' @@ -45,6 +46,7 @@ import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' import { organizationLogic } from '../organizationLogic' import { NEW_EARLY_ACCESS_FEATURE } from 'scenes/early-access-features/earlyAccessFeatureLogic' import { NEW_SURVEY, NewSurvey } from 'scenes/surveys/constants' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' import { Scene } from 'scenes/sceneTypes' const getDefaultRollbackCondition = (): FeatureFlagRollbackConditions => ({ diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx index c123af4ac782e..be1c74bb77cc6 100644 --- a/frontend/src/scenes/insights/InsightPageHeader.tsx +++ b/frontend/src/scenes/insights/InsightPageHeader.tsx @@ -16,7 +16,7 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { urls } from 'scenes/urls' import { SubscribeButton, SubscriptionsModal } from 'lib/components/Subscriptions/SubscriptionsModal' import { ExportButton } from 'lib/components/ExportButton/ExportButton' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { AddToDashboard } from 'lib/components/AddToDashboard/AddToDashboard' import { InsightSaveButton } from 'scenes/insights/InsightSaveButton' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index 6e6cfc856b639..0529a2d311838 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -2,7 +2,8 @@ import { kea, props, key, path, connect, actions, reducers, selectors, listeners import { EntityTypes, FilterType, Entity, EntityType, ActionFilter, EntityFilter, AnyPropertyFilter } from '~/types' import type { entityFilterLogicType } from './entityFilterLogicType' import { eventUsageLogic, GraphSeriesAddedSource } from 'lib/utils/eventUsageLogic' -import { convertPropertyGroupToProperties, uuid } from 'lib/utils' +import { uuid } from 'lib/utils' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' export type LocalFilter = ActionFilter & { order: number diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index 272b2bf031ba4..26ad7a3a77a8d 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -3,14 +3,17 @@ import { AnyPartialFilterType, BreakdownKeyType, BreakdownType, + ChartDisplayType, CohortType, EntityFilter, EntityTypes, + EventType, InsightModel, InsightShortId, InsightType, PathsFilterType, PathType, + TrendsFilterType, } from '~/types' import { ensureStringIsNotBlank, humanFriendlyNumber, objectsEqual } from 'lib/utils' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' @@ -296,3 +299,48 @@ export function concatWithPunctuation(phrases: string[]): string { return `${phrases.slice(0, phrases.length - 1).join(', ')}, and ${phrases[phrases.length - 1]}` } } + +export function insightUrlForEvent(event: Pick): string | undefined { + let insightParams: Partial | undefined + if (event.event === '$pageview') { + insightParams = { + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [], + events: [ + { + id: '$pageview', + name: '$pageview', + type: 'events', + order: 0, + properties: [ + { + key: '$current_url', + value: event.properties.$current_url, + type: 'event', + }, + ], + }, + ], + } + } else if (event.event !== '$autocapture') { + insightParams = { + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [], + events: [ + { + id: event.event, + name: event.event, + type: 'events', + order: 0, + properties: [], + }, + ], + } + } + + return insightParams ? urls.insightNew(insightParams) : undefined +} diff --git a/frontend/src/scenes/insights/utils/cleanFilters.ts b/frontend/src/scenes/insights/utils/cleanFilters.ts index bd16e3ca1ba79..fced598e698a0 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.ts @@ -7,6 +7,7 @@ import { FunnelsFilterType, FunnelVizType, InsightType, + IntervalType, LifecycleFilterType, PathsFilterType, PathType, @@ -19,12 +20,12 @@ import { deepCleanFunnelExclusionEvents, getClampedStepRangeFilter, isStepsUndef import { getDefaultEventName } from 'lib/utils/getAppContext' import { BIN_COUNT_AUTO, + NON_TIME_SERIES_DISPLAY_TYPES, NON_VALUES_ON_SERIES_DISPLAY_TYPES, PERCENT_STACK_VIEW_DISPLAY_TYPE, RETENTION_FIRST_TIME, ShownAsValue, } from 'lib/constants' -import { autocorrectInterval } from 'lib/utils' import { DEFAULT_STEP_LIMIT } from 'scenes/paths/pathsDataLogic' import { smoothingOptions } from 'lib/components/SmoothingFilter/smoothings' import { LocalFilter, toLocalFilters } from '../filters/ActionFilter/entityFilterLogic' @@ -165,6 +166,46 @@ export const setTestAccountFilterForNewInsight = ( } } +const disableHourFor: Record = { + dStart: false, + '-1d': false, + '-7d': false, + '-14d': false, + '-30d': false, + '-90d': true, + mStart: false, + '-1mStart': false, + yStart: true, + all: true, + other: false, +} + +export function autocorrectInterval(filters: Partial): IntervalType | undefined { + if ('display' in filters && filters.display && NON_TIME_SERIES_DISPLAY_TYPES.includes(filters.display)) { + // Non-time-series insights should not have an interval + return undefined + } + if (isFunnelsFilter(filters) && filters.funnel_viz_type !== FunnelVizType.Trends) { + // Only trend funnels support intervals + return undefined + } + if (!filters.interval) { + return 'day' + } + + // @ts-expect-error - Old legacy interval support + const minute_disabled = filters.interval === 'minute' + const hour_disabled = disableHourFor[filters.date_from || 'other'] && filters.interval === 'hour' + + if (minute_disabled) { + return 'hour' + } else if (hour_disabled) { + return 'day' + } else { + return filters.interval + } +} + export function cleanFilters( filters: Partial, test_account_filters_default_checked?: boolean diff --git a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx index 27544da05c561..594cfa583e4bc 100644 --- a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx +++ b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx @@ -6,8 +6,8 @@ import clsx from 'clsx' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { useNotebookNode } from '../Nodes/notebookNodeLogic' import { notebookPanelLogic } from '../NotebookPanel/notebookPanelLogic' +import { useNotebookNode } from '../Nodes/NotebookNodeContext' export type DraggableToNotebookBaseProps = { href?: string diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index 34f70d6c85f25..cba90e520677c 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -25,7 +25,7 @@ import { notebookLogic } from '../Notebook/notebookLogic' import { useInView } from 'react-intersection-observer' import { NotebookNodeResource } from '~/types' import { ErrorBoundary } from '~/layout/ErrorBoundary' -import { NotebookNodeContext, NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' +import { NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' import { posthogNodePasteRule, useSyncedAttributes } from './utils' import { KNOWN_NODES, @@ -39,6 +39,7 @@ import { NotebookNodeTitle } from './components/NotebookNodeTitle' import { notebookNodeLogicType } from './notebookNodeLogicType' import { SlashCommandsPopover } from '../Notebook/SlashCommands' import posthog from 'posthog-js' +import { NotebookNodeContext } from './NotebookNodeContext' import { IconGear } from '@posthog/icons' function NodeWrapper(props: NodeWrapperProps): JSX.Element { diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts b/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts new file mode 100644 index 0000000000000..d5db3c5035793 --- /dev/null +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts @@ -0,0 +1,10 @@ +import { BuiltLogic } from 'kea' +import { createContext, useContext } from 'react' +import type { notebookNodeLogicType } from './notebookNodeLogicType' + +export const NotebookNodeContext = createContext | undefined>(undefined) + +// Currently there is no way to optionally get bound logics so this context allows us to maybe get a logic if it is "bound" via the provider +export const useNotebookNode = (): BuiltLogic | undefined => { + return useContext(NotebookNodeContext) +} diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index 9fa2e7013196e..c40cdfbbdb781 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -13,7 +13,6 @@ import { selectors, } from 'kea' import type { notebookNodeLogicType } from './notebookNodeLogicType' -import { createContext, useContext } from 'react' import { notebookLogicType } from '../Notebook/notebookLogicType' import { CustomNotebookNodeAttributes, @@ -281,10 +280,3 @@ export const notebookNodeLogic = kea([ props.notebookLogic.actions.unregisterNodeLogic(values.nodeId) }), ]) - -export const NotebookNodeContext = createContext | undefined>(undefined) - -// Currently there is no way to optionally get bound logics so this context allows us to maybe get a logic if it is "bound" via the provider -export const useNotebookNode = (): BuiltLogic | undefined => { - return useContext(NotebookNodeContext) -} diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx index 8eda1fc7d11da..5658ddbf7e5cb 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx @@ -2,7 +2,7 @@ import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui' import { combineUrl } from 'kea-router' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import posthog from 'posthog-js' import { useState } from 'react' import { urls } from 'scenes/urls' diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx index a22692be85990..7d313a160ec59 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx @@ -9,7 +9,7 @@ import { BuiltLogic, useActions, useValues } from 'kea' import { dayjs } from 'lib/dayjs' import { NotebookListItemType, NotebookTarget } from '~/types' import { notebooksModel, openNotebook } from '~/models/notebooksModel' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { Popover, PopoverProps } from 'lib/lemon-ui/Popover' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { notebookLogicType } from '../Notebook/notebookLogicType' diff --git a/frontend/src/scenes/organizationLogic.tsx b/frontend/src/scenes/organizationLogic.tsx index c582391f506ef..873903881c6b9 100644 --- a/frontend/src/scenes/organizationLogic.tsx +++ b/frontend/src/scenes/organizationLogic.tsx @@ -1,5 +1,5 @@ import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' +import api, { ApiConfig } from 'lib/api' import type { organizationLogicType } from './organizationLogicType' import { AvailableFeature, OrganizationType } from '~/types' import { userLogic } from './userLogic' @@ -92,6 +92,11 @@ export const organizationLogic = kea([ ], }), listeners(({ actions }) => ({ + loadCurrentOrganizationSuccess: ({ currentOrganization }) => { + if (currentOrganization) { + ApiConfig.setCurrentOrganizationId(currentOrganization.id) + } + }, createOrganizationSuccess: () => { window.location.href = '/organization/members' }, diff --git a/frontend/src/scenes/paths/PathNodeCardButton.tsx b/frontend/src/scenes/paths/PathNodeCardButton.tsx index 0bb86e403890e..4e491d5501e81 100644 --- a/frontend/src/scenes/paths/PathNodeCardButton.tsx +++ b/frontend/src/scenes/paths/PathNodeCardButton.tsx @@ -5,7 +5,7 @@ import { userLogic } from 'scenes/userLogic' import { AvailableFeature, PathsFilterType } from '~/types' import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' import { IconEllipsis } from 'lib/lemon-ui/icons' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { pageUrl, PathNodeData } from './pathUtils' import { pathsDataLogicType } from './pathsDataLogicType' diff --git a/frontend/src/scenes/persons/PersonDisplay.tsx b/frontend/src/scenes/persons/PersonDisplay.tsx index 06b3e4b2158ad..1feadfb8b3733 100644 --- a/frontend/src/scenes/persons/PersonDisplay.tsx +++ b/frontend/src/scenes/persons/PersonDisplay.tsx @@ -7,7 +7,7 @@ import { PersonPreview } from './PersonPreview' import { useMemo, useState } from 'react' import { router } from 'kea-router' import { asDisplay, asLink } from './person-utils' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { NotebookNodeType } from '~/types' type PersonPropType = diff --git a/frontend/src/scenes/persons/personsLogic.tsx b/frontend/src/scenes/persons/personsLogic.tsx index f5b6d0e6b333f..dc3dbfc01ea4b 100644 --- a/frontend/src/scenes/persons/personsLogic.tsx +++ b/frontend/src/scenes/persons/personsLogic.tsx @@ -16,8 +16,8 @@ import { import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { urls } from 'scenes/urls' import { teamLogic } from 'scenes/teamLogic' -import { convertPropertyGroupToProperties, toParams } from 'lib/utils' -import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' +import { toParams } from 'lib/utils' +import { convertPropertyGroupToProperties, isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { TriggerExportProps } from 'lib/components/ExportButton/exporter' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' diff --git a/frontend/src/scenes/pipeline/Transformations.tsx b/frontend/src/scenes/pipeline/Transformations.tsx index 1a627f0439bc4..c16333ff78a00 100644 --- a/frontend/src/scenes/pipeline/Transformations.tsx +++ b/frontend/src/scenes/pipeline/Transformations.tsx @@ -20,11 +20,12 @@ import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifi import { CSS } from '@dnd-kit/utilities' import { More } from 'lib/lemon-ui/LemonButton/More' import { updatedAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { deleteWithUndo, humanFriendlyDetailedTime } from 'lib/utils' +import { humanFriendlyDetailedTime } from 'lib/utils' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown/LemonMarkdown' import { dayjs } from 'lib/dayjs' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' import { NewButton } from './NewButton' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' export function Transformations(): JSX.Element { const { diff --git a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx index 20438edc4a136..87c61298cd182 100644 --- a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx @@ -1,9 +1,10 @@ import { useActions, useValues } from 'kea' import { pluralize } from 'lib/utils' import { PluginLogEntryType } from '../../../types' -import { LOGS_PORTION_LIMIT, pluginLogsLogic, PluginLogsProps } from './pluginLogsLogic' +import { pluginLogsLogic, PluginLogsProps } from './pluginLogsLogic' import { dayjs } from 'lib/dayjs' import { LemonButton, LemonCheckbox, LemonInput, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' +import { LOGS_PORTION_LIMIT } from 'lib/constants' function PluginLogEntryTypeDisplay(type: PluginLogEntryType): JSX.Element { let color: string | undefined diff --git a/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts b/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts index 437393efcb4cf..f2e46d6deb683 100644 --- a/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts +++ b/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts @@ -5,13 +5,12 @@ import { PluginLogEntry, PluginLogEntryType } from '~/types' import { teamLogic } from '../../teamLogic' import type { pluginLogsLogicType } from './pluginLogsLogicType' import { CheckboxValueType } from 'antd/lib/checkbox/Group' +import { LOGS_PORTION_LIMIT } from 'lib/constants' export interface PluginLogsProps { pluginConfigId: number } -export const LOGS_PORTION_LIMIT = 50 - export const pluginLogsLogic = kea([ props({} as PluginLogsProps), key(({ pluginConfigId }: PluginLogsProps) => pluginConfigId), diff --git a/frontend/src/scenes/plugins/tabs/apps/components.tsx b/frontend/src/scenes/plugins/tabs/apps/components.tsx index 4033a7f86a166..aa0892cf9e0db 100644 --- a/frontend/src/scenes/plugins/tabs/apps/components.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/components.tsx @@ -6,7 +6,7 @@ import { useValues } from 'kea' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { organizationLogic } from 'scenes/organizationLogic' import { PluginsAccessLevel } from 'lib/constants' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { urls } from 'scenes/urls' diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index d8c87972cdf00..4b9052ff38c36 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' import { Link } from 'lib/lemon-ui/Link' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { InsightModel, InsightType, LayoutView, SavedInsightsTabs } from '~/types' import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' import './SavedInsights.scss' diff --git a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx b/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx index d0fd56e93e16b..c0059f905f168 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx @@ -10,7 +10,7 @@ import { PlaylistPopoverButton } from './playlist-popover/PlaylistPopover' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { NotebookNodeType } from '~/types' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { sessionPlayerModalLogic } from './modal/sessionPlayerModalLogic' import { personsModalLogic } from 'scenes/trends/persons-modal/personsModalLogic' import { IconNotebook } from 'scenes/notebooks/IconNotebook' diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx index 119951b6a6160..046ba542d8dd6 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx @@ -1,11 +1,12 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { capitalizeFirstLetter, autoCaptureEventToDescription, insightUrlForEvent } from 'lib/utils' +import { capitalizeFirstLetter, autoCaptureEventToDescription } from 'lib/utils' import { InspectorListItemEvent } from '../playerInspectorLogic' import { SimpleKeyValueList } from './SimpleKeyValueList' import { Spinner } from 'lib/lemon-ui/Spinner' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' +import { insightUrlForEvent } from 'scenes/insights/utils' export interface ItemEventProps { item: InspectorListItemEvent diff --git a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx index 4e34628a2f797..4c2003842efff 100644 --- a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx +++ b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx @@ -5,7 +5,7 @@ import { Form } from 'kea-forms' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { Field } from 'lib/forms/Field' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { playerShareLogic, PlayerShareLogicProps } from './playerShareLogic' import { SharingModalContent } from 'lib/components/Sharing/SharingModal' import { captureException } from '@sentry/react' diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index ec3aa4b9a723c..90368ee82d0cc 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -23,7 +23,7 @@ import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SessionRecordingsPlaylistSettings } from './SessionRecordingsPlaylistSettings' import { SessionRecordingsPlaylistTroubleshooting } from './SessionRecordingsPlaylistTroubleshooting' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { LemonTableLoader } from 'lib/lemon-ui/LemonTable/LemonTableLoader' import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' import { range } from 'd3' diff --git a/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts b/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts index 9767d4b41809a..68ae4384e0504 100644 --- a/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts +++ b/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts @@ -2,7 +2,8 @@ import { PropertyOperator, RecordingFilters, SessionRecordingPlaylistType } from import { cohortsModelType } from '~/models/cohortsModelType' import { toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' -import { convertPropertyGroupToProperties, deleteWithUndo, genericOperatorMap } from 'lib/utils' +import { genericOperatorMap } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { getKeyMapping } from 'lib/taxonomy' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' @@ -11,6 +12,7 @@ import { router } from 'kea-router' import { urls } from 'scenes/urls' import { openBillingPopupModal } from 'scenes/billing/BillingPopup' import { PLAYLIST_LIMIT_REACHED_MESSAGE } from 'scenes/session-recordings/sessionRecordingsLogic' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' function getOperatorSymbol(operator: PropertyOperator | null): string { if (!operator) { diff --git a/frontend/src/scenes/settings/Settings.tsx b/frontend/src/scenes/settings/Settings.tsx index 80097630a0486..d4b99a7750532 100644 --- a/frontend/src/scenes/settings/Settings.tsx +++ b/frontend/src/scenes/settings/Settings.tsx @@ -1,8 +1,8 @@ import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui' import { IconChevronRight, IconLink } from 'lib/lemon-ui/icons' -import { SettingsLogicProps, settingsLogic } from './settingsLogic' +import { settingsLogic } from './settingsLogic' import { useActions, useValues } from 'kea' -import { SettingLevelIds } from './types' +import { SettingLevelIds, SettingsLogicProps } from './types' import clsx from 'clsx' import { capitalizeFirstLetter } from 'lib/utils' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx index 548a89ad5ca5a..f62668714b831 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx @@ -12,11 +12,11 @@ import { SSOSelect } from './SSOSelect' import { VerifyDomainModal } from './VerifyDomainModal' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { Link } from 'lib/lemon-ui/Link' -import { UPGRADE_LINK } from 'lib/constants' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' import { ConfigureSAMLModal } from './ConfigureSAMLModal' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { IconInfo } from '@posthog/icons' +import { urls } from 'scenes/urls' const iconStyle = { marginRight: 4, fontSize: '1.15em', paddingTop: 2 } @@ -140,11 +140,7 @@ function VerifiedDomainsTable(): JSX.Element { render: function SSOEnforcement(_, { sso_enforcement, is_verified, id, has_saml }, index) { if (!isSSOEnforcementAvailable) { return index === 0 ? ( - + Upgrade to enable SSO enforcement @@ -170,11 +166,7 @@ function VerifiedDomainsTable(): JSX.Element { render: function SAML(_, { is_verified, saml_acs_url, saml_entity_id, saml_x509_cert, has_saml }, index) { if (!isSAMLAvailable) { return index === 0 ? ( - + Upgrade to enable SAML ) : ( diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index b754950febf11..3b72938fd742c 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -3,21 +3,11 @@ import { SettingsMap } from './SettingsMap' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' -import { SettingSection, Setting, SettingSectionId, SettingLevelId, SettingId } from './types' +import { SettingSection, Setting, SettingSectionId, SettingLevelId, SettingId, SettingsLogicProps } from './types' import type { settingsLogicType } from './settingsLogicType' import { urls } from 'scenes/urls' -import { copyToClipboard } from 'lib/utils' - -export type SettingsLogicProps = { - logicKey?: string - // Optional - if given, renders only the given level - settingLevelId?: SettingLevelId - // Optional - if given, renders only the given section - sectionId?: SettingSectionId - // Optional - if given, renders only the given setting - settingId?: SettingId -} +import { copyToClipboard } from 'lib/utils/copyToClipboard' export const settingsLogic = kea([ props({} as SettingsLogicProps), diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index 30ee8324d0ebe..038da8dd71126 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -1,5 +1,14 @@ -import { FEATURE_FLAGS } from 'lib/constants' -import { EitherMembershipLevel } from 'lib/utils/permissioning' +import { EitherMembershipLevel, FEATURE_FLAGS } from 'lib/constants' + +export type SettingsLogicProps = { + logicKey?: string + // Optional - if given, renders only the given level + settingLevelId?: SettingLevelId + // Optional - if given, renders only the given section + sectionId?: SettingSectionId + // Optional - if given, renders only the given setting + settingId?: SettingId +} export type SettingLevelId = 'user' | 'project' | 'organization' export const SettingLevelIds: SettingLevelId[] = ['project', 'organization', 'user'] diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts index 54314396d9ae8..4097d6997f895 100644 --- a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts @@ -3,7 +3,7 @@ import { kea, path, listeners } from 'kea' import api from 'lib/api' import { PersonalAPIKeyType } from '~/types' import type { personalAPIKeysLogicType } from './personalAPIKeysLogicType' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { lemonToast } from 'lib/lemon-ui/lemonToast' export const personalAPIKeysLogic = kea([ diff --git a/frontend/src/scenes/teamLogic.tsx b/frontend/src/scenes/teamLogic.tsx index 26fadbed4ed66..32f877f7ee5c7 100644 --- a/frontend/src/scenes/teamLogic.tsx +++ b/frontend/src/scenes/teamLogic.tsx @@ -1,5 +1,5 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' +import api, { ApiConfig } from 'lib/api' import type { teamLogicType } from './teamLogicType' import { CorrelationConfigType, PropertyOperator, TeamPublicType, TeamType } from '~/types' import { userLogic } from './userLogic' @@ -206,6 +206,11 @@ export const teamLogic = kea([ ], })), listeners(({ actions }) => ({ + loadCurrentTeamSuccess: ({ currentTeam }) => { + if (currentTeam) { + ApiConfig.setCurrentTeamId(currentTeam.id) + } + }, createTeamSuccess: () => { organizationLogic.actions.loadCurrentOrganization() }, diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index d84ac0cfa7473..51a66af2ff593 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -7,11 +7,11 @@ import { InsightShortId, ReplayTabs, PipelineTabs, + AppMetricsUrlParams, PipelineAppTabs, } from '~/types' import { combineUrl } from 'kea-router' import { ExportOptions } from '~/exporter/types' -import { AppMetricsUrlParams } from './apps/appMetricsSceneLogic' import { PluginTab } from './plugins/types' import { toParams } from 'lib/utils' import { SettingId, SettingLevelId, SettingSectionId } from './settings/types' diff --git a/frontend/src/scenes/userLogic.ts b/frontend/src/scenes/userLogic.ts index 8964fc32ab291..251eace6e5cd5 100644 --- a/frontend/src/scenes/userLogic.ts +++ b/frontend/src/scenes/userLogic.ts @@ -1,10 +1,9 @@ -import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' import api from 'lib/api' import type { userLogicType } from './userLogicType' import { AvailableFeature, OrganizationBasicType, ProductKey, UserType } from '~/types' import posthog from 'posthog-js' import { getAppContext } from 'lib/utils/getAppContext' -import { preflightLogic } from './PreflightCheck/preflightLogic' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { loaders } from 'kea-loaders' import { forms } from 'kea-forms' @@ -17,9 +16,6 @@ export interface UserDetailsFormType { export const userLogic = kea([ path(['scenes', 'userLogic']), - connect({ - values: [preflightLogic, ['preflight']], - }), actions(() => ({ loadUser: (resetOnFailure?: boolean) => ({ resetOnFailure }), updateCurrentTeam: (teamId: number, destination?: string) => ({ teamId, destination }), diff --git a/frontend/src/test/init.ts b/frontend/src/test/init.ts index dc896f740e8c9..32e4e2b7d8110 100644 --- a/frontend/src/test/init.ts +++ b/frontend/src/test/init.ts @@ -7,6 +7,7 @@ import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' import { dayjs } from 'lib/dayjs' import { organizationLogic } from 'scenes/organizationLogic' import { teamLogic } from 'scenes/teamLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' process.on('unhandledRejection', (err) => { console.warn(err) @@ -33,6 +34,7 @@ export function initKeaTests(mountCommonLogic = true, teamForWindowContext: Team ;(history as any).replaceState = history.replace initKea({ beforePlugins: [testUtilsPlugin], routerLocation: history.location, routerHistory: history }) if (mountCommonLogic) { + preflightLogic.mount() teamLogic.mount() organizationLogic.mount() } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index eed32835f8ac2..1ba6f2e6157e8 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -279,6 +279,8 @@ export interface ExplicitTeamMemberType extends BaseMemberType { effective_level: OrganizationMembershipLevel } +export type EitherMemberType = OrganizationMemberType | ExplicitTeamMemberType + /** * While OrganizationMemberType and ExplicitTeamMemberType refer to actual Django models, * this interface is only used in the frontend for fusing the data from these models together. @@ -2690,6 +2692,11 @@ export interface KeyMapping { system?: boolean } +export interface KeyMappingInterface { + event: Record + element: Record +} + export interface TileParams { title: string targetPath: string @@ -3410,6 +3417,23 @@ export enum SDKTag { export type SDKInstructionsMap = Partial> +export interface AppMetricsUrlParams { + tab?: AppMetricsTab + from?: string + error?: [string, string] +} + +export enum AppMetricsTab { + Logs = 'logs', + ProcessEvent = 'processEvent', + OnEvent = 'onEvent', + ComposeWebhook = 'composeWebhook', + ExportEvents = 'exportEvents', + ScheduledTask = 'scheduledTask', + HistoricalExports = 'historical_exports', + History = 'history', +} + export enum SidePanelTab { Notebooks = 'notebook', Support = 'support',