diff --git a/.github/actions/run-backend-tests/action.yml b/.github/actions/run-backend-tests/action.yml index 3f13a10f1e6dc..a45caaf8aef18 100644 --- a/.github/actions/run-backend-tests/action.yml +++ b/.github/actions/run-backend-tests/action.yml @@ -145,7 +145,6 @@ runs: run: echo "PYTEST_ARGS=--snapshot-update" >> $GITHUB_ENV # We can only update snapshots within the PostHog org # Tests - - name: Run FOSS tests if: ${{ inputs.segment == 'FOSS' }} env: diff --git a/.github/workflows/container-images-cd.yml b/.github/workflows/container-images-cd.yml index ada203c979ec4..8b1312bf948a0 100644 --- a/.github/workflows/container-images-cd.yml +++ b/.github/workflows/container-images-cd.yml @@ -107,7 +107,7 @@ jobs: message: | { "image_tag": "${{ steps.build.outputs.digest }}" - } + } - name: Check for changes in plugins directory id: check_changes_plugins diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 06f3c5bbf09c7..2483a69d6ad41 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -12,14 +12,10 @@ from ee.clickhouse.queries.related_actors_query import RelatedActorsQuery from posthog.api.documentation import extend_schema from posthog.api.routing import TeamAndOrgViewSetMixin -from posthog.auth import SharingAccessTokenAuthentication from posthog.clickhouse.kafka_engine import trim_quotes_expr from posthog.client import sync_execute from posthog.models.group import Group from posthog.models.group_type_mapping import GroupTypeMapping -from posthog.permissions import ( - SharingTokenPermission, -) class GroupTypeSerializer(serializers.ModelSerializer): @@ -35,14 +31,6 @@ class ClickhouseGroupsTypesView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, v pagination_class = None sharing_enabled_actions = ["list"] - def get_permissions(self): - if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): - return [SharingTokenPermission()] - return super().get_permissions() - - def get_authenticators(self): - return [SharingAccessTokenAuthentication(), *super().get_authenticators()] - @action(detail=False, methods=["PATCH"], name="Update group types metadata") def update_metadata(self, request: request.Request, *args, **kwargs): for row in cast(List[Dict], request.data): diff --git a/ee/clickhouse/views/test/test_clickhouse_groups.py b/ee/clickhouse/views/test/test_clickhouse_groups.py index 1b3687c0fec61..10e064095c421 100644 --- a/ee/clickhouse/views/test/test_clickhouse_groups.py +++ b/ee/clickhouse/views/test/test_clickhouse_groups.py @@ -434,6 +434,26 @@ def test_cannot_list_group_types_of_another_org(self): self.permission_denied_response("You don't have access to the project."), ) + def test_cannot_list_group_types_of_another_org_with_sharing_token(self): + sharing_configuration = SharingConfiguration.objects.create(team=self.team, enabled=True) + + other_org = Organization.objects.create(name="other org") + other_team = Team.objects.create(organization=other_org, name="other project") + + GroupTypeMapping.objects.create(team=other_team, group_type="organization", group_type_index=0) + GroupTypeMapping.objects.create(team=other_team, group_type="playlist", group_type_index=1) + GroupTypeMapping.objects.create(team=other_team, group_type="another", group_type_index=2) + + response = self.client.get( + f"/api/projects/{other_team.id}/groups_types/?sharing_access_token={sharing_configuration.access_token}" + ) + + self.assertEqual(response.status_code, 403, response.json()) + self.assertEqual( + response.json(), + self.permission_denied_response("You do not have permission to perform this action."), + ) + def test_can_list_group_types_of_another_org_with_sharing_access_token(self): other_org = Organization.objects.create(name="other org") other_team = Team.objects.create(organization=other_org, name="other project") diff --git a/frontend/__snapshots__/components-command-bar--actions--dark.png b/frontend/__snapshots__/components-command-bar--actions--dark.png index 55ff12cdf7a64..e15ffa5164b1b 100644 Binary files a/frontend/__snapshots__/components-command-bar--actions--dark.png and b/frontend/__snapshots__/components-command-bar--actions--dark.png differ diff --git a/frontend/__snapshots__/components-command-bar--actions--light.png b/frontend/__snapshots__/components-command-bar--actions--light.png index 3b8e1b2efd516..a886435fba6b1 100644 Binary files a/frontend/__snapshots__/components-command-bar--actions--light.png and b/frontend/__snapshots__/components-command-bar--actions--light.png differ diff --git a/frontend/__snapshots__/components-command-bar--search--dark.png b/frontend/__snapshots__/components-command-bar--search--dark.png index c5d76efb38fcd..4546e3221a8b2 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--dark.png and b/frontend/__snapshots__/components-command-bar--search--dark.png differ diff --git a/frontend/__snapshots__/components-command-bar--search--light.png b/frontend/__snapshots__/components-command-bar--search--light.png index b8791bfc9ce45..fbcf9433f8f5f 100644 Binary files a/frontend/__snapshots__/components-command-bar--search--light.png and b/frontend/__snapshots__/components-command-bar--search--light.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--basic--dark.png b/frontend/__snapshots__/layout-feature-previews--basic--dark.png new file mode 100644 index 0000000000000..75ea7a1bd5aec Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--basic--dark.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--basic--light.png b/frontend/__snapshots__/layout-feature-previews--basic--light.png new file mode 100644 index 0000000000000..a535a38508717 Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--basic--light.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--empty--dark.png b/frontend/__snapshots__/layout-feature-previews--empty--dark.png new file mode 100644 index 0000000000000..1b73a2eee7403 Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--empty--dark.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--empty--light.png b/frontend/__snapshots__/layout-feature-previews--empty--light.png new file mode 100644 index 0000000000000..7d21c3c7deefb Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--empty--light.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--dark.png b/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--dark.png new file mode 100644 index 0000000000000..7abb672985a20 Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--dark.png differ diff --git a/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--light.png b/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--light.png new file mode 100644 index 0000000000000..866fbf244450a Binary files /dev/null and b/frontend/__snapshots__/layout-feature-previews--with-constrained-feature--light.png differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--basic--dark.png b/frontend/__snapshots__/layout-feature-previews-modal--basic--dark.png deleted file mode 100644 index b7892f991f53c..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--basic--dark.png and /dev/null differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--basic--light.png b/frontend/__snapshots__/layout-feature-previews-modal--basic--light.png deleted file mode 100644 index 668b1893e7251..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--basic--light.png and /dev/null differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--empty--dark.png b/frontend/__snapshots__/layout-feature-previews-modal--empty--dark.png deleted file mode 100644 index 7af703246b24e..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--empty--dark.png and /dev/null differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--empty--light.png b/frontend/__snapshots__/layout-feature-previews-modal--empty--light.png deleted file mode 100644 index 1f0bbf9e26774..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--empty--light.png and /dev/null differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--dark.png b/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--dark.png deleted file mode 100644 index e1cbd70ff5009..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--dark.png and /dev/null differ diff --git a/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--light.png b/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--light.png deleted file mode 100644 index 6f2c08e7aba68..0000000000000 Binary files a/frontend/__snapshots__/layout-feature-previews-modal--with-constrained-feature--light.png and /dev/null differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled--light.png b/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled--light.png index 4d1e6546cad32..b953663cb43ea 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled--light.png and b/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled-with-reason--light.png b/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled-with-reason--light.png index 4d1e6546cad32..b953663cb43ea 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled-with-reason--light.png and b/frontend/__snapshots__/lemon-ui-lemon-checkbox--disabled-with-reason--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--dark.png b/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--dark.png index 6672835b2b74f..d56bb3b8b5951 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--light.png b/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--light.png index cff1760f7f6d3..e4ff4460dcf2e 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--light.png and b/frontend/__snapshots__/lemon-ui-lemon-checkbox--no-label--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-forms-and-fields--fields-with-kea-form--dark.png b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png similarity index 100% rename from frontend/__snapshots__/lemon-ui-forms-and-fields--fields-with-kea-form--dark.png rename to frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png diff --git a/frontend/__snapshots__/lemon-ui-forms-and-fields--fields-with-kea-form--light.png b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png similarity index 100% rename from frontend/__snapshots__/lemon-ui-forms-and-fields--fields-with-kea-form--light.png rename to frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png diff --git a/frontend/__snapshots__/lemon-ui-forms-and-fields--pure-fields--dark.png b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png similarity index 100% rename from frontend/__snapshots__/lemon-ui-forms-and-fields--pure-fields--dark.png rename to frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png diff --git a/frontend/__snapshots__/lemon-ui-forms-and-fields--pure-fields--light.png b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png similarity index 100% rename from frontend/__snapshots__/lemon-ui-forms-and-fields--pure-fields--light.png rename to frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png index 87a76f1653c40..5ba95095f4a18 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png index ea13f3250e585..43981441292e7 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png index d2643fc390633..e6507b0269eff 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png index 36a0466e6c1f7..76d4b7bc85b67 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png index 897331d9aaf6f..f173b278ad65f 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png index cfa99376fa2e2..9afb43cee1fdb 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png index 31cb858d40508..b21fdc8a2fe88 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png index cfa99376fa2e2..9afb43cee1fdb 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png index 78bf1462dc8b8..e6dd3b53ae662 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png index 8af57a632597d..441e34109f7a1 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png index 57711a72c77c7..709cceb9886a7 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png differ diff --git a/frontend/src/exporter/Exporter.scss b/frontend/src/exporter/Exporter.scss index df0d456df55d7..76f92199f5689 100644 --- a/frontend/src/exporter/Exporter.scss +++ b/frontend/src/exporter/Exporter.scss @@ -1,9 +1,7 @@ @import '../styles/mixins'; body.ExporterBody { - &.posthog-3000 { - overflow: initial; - } + overflow: initial; } .Exporter { diff --git a/frontend/src/initKea.ts b/frontend/src/initKea.ts index 936fb12e7d84a..d500b27bf441c 100644 --- a/frontend/src/initKea.ts +++ b/frontend/src/initKea.ts @@ -114,7 +114,8 @@ export function initKea({ routerHistory, routerLocation, beforePlugins }: InitKe waitForPlugin, ] - if (window.JS_KEA_VERBOSE_LOGGING) { + // To enable logging, run localStorage.setItem("debug", true) in the console + if (window.JS_KEA_VERBOSE_LOGGING || ('localStorage' in window && window.localStorage.getItem('debug'))) { plugins.push(loggerPlugin) } diff --git a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx b/frontend/src/layout/FeaturePreviews/FeaturePreviews.stories.tsx similarity index 92% rename from frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx rename to frontend/src/layout/FeaturePreviews/FeaturePreviews.stories.tsx index 8853e4812c90a..9946888196d9c 100644 --- a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx +++ b/frontend/src/layout/FeaturePreviews/FeaturePreviews.stories.tsx @@ -4,8 +4,8 @@ import { EarlyAccessFeature } from 'posthog-js' import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' +import { FeaturePreviews } from './FeaturePreviews' import { CONSTRAINED_PREVIEWS } from './featurePreviewsLogic' -import { FeaturePreviewsModal as FeaturePreviewsModalComponent } from './FeaturePreviewsModal' interface StoryProps { earlyAccessFeatures: EarlyAccessFeature[] @@ -14,7 +14,7 @@ interface StoryProps { type Story = StoryObj<(props: StoryProps) => JSX.Element> const meta: Meta<(props: StoryProps) => JSX.Element> = { - title: 'Layout/Feature Previews Modal', + title: 'Layout/Feature Previews', parameters: { layout: 'fullscreen', viewMode: 'story', @@ -33,8 +33,8 @@ const Template: StoryFn = ({ earlyAccessFeatures, enabledFeatureFlag setFeatureFlags(enabledFeatureFlags) return ( -
- +
+
) } diff --git a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx b/frontend/src/layout/FeaturePreviews/FeaturePreviews.tsx similarity index 82% rename from frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx rename to frontend/src/layout/FeaturePreviews/FeaturePreviews.tsx index 0a2667b2fd14a..00984bf0cd627 100644 --- a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx +++ b/frontend/src/layout/FeaturePreviews/FeaturePreviews.tsx @@ -1,42 +1,11 @@ -import { LemonButton, LemonDivider, LemonModal, LemonSwitch, LemonTextArea, Link } from '@posthog/lemon-ui' +import { LemonButton, LemonDivider, LemonSwitch, LemonTextArea, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useAsyncActions, useValues } from 'kea' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner' import { useLayoutEffect, useState } from 'react' -import { sidePanelStateLogic } from '../navigation-3000/sidepanel/sidePanelStateLogic' import { EnrichedEarlyAccessFeature, featurePreviewsLogic } from './featurePreviewsLogic' -export function FeaturePreviewsModal({ - inline, -}: { - /** @deprecated This is only for Storybook. */ - inline?: boolean -}): JSX.Element | null { - const { featurePreviewsModalVisible } = useValues(featurePreviewsLogic) - const { hideFeaturePreviewsModal, loadEarlyAccessFeatures } = useActions(featurePreviewsLogic) - const { sidePanelAvailable } = useValues(sidePanelStateLogic) - - useLayoutEffect(() => loadEarlyAccessFeatures(), []) - - if (sidePanelAvailable) { - return null - } - - return ( - - - - ) -} - export function FeaturePreviews(): JSX.Element { const { earlyAccessFeatures, rawEarlyAccessFeaturesLoading } = useValues(featurePreviewsLogic) const { loadEarlyAccessFeatures } = useActions(featurePreviewsLogic) diff --git a/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx b/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx index 602bdf72eafc3..60e3e3a931918 100644 --- a/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx +++ b/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx @@ -1,6 +1,5 @@ import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { actionToUrl, router, urlToAction } from 'kea-router' import { supportLogic } from 'lib/components/Support/supportLogic' import { FeatureFlagKey } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' @@ -24,8 +23,6 @@ export const featurePreviewsLogic = kea([ actions: [supportLogic, ['submitZendeskTicket']], }), actions({ - showFeaturePreviewsModal: true, - hideFeaturePreviewsModal: true, updateEarlyAccessFeatureEnrollment: (flagKey: string, enabled: boolean) => ({ flagKey, enabled }), beginEarlyAccessFeatureFeedback: (flagKey: string) => ({ flagKey }), cancelEarlyAccessFeatureFeedback: true, @@ -67,17 +64,9 @@ export const featurePreviewsLogic = kea([ ], })), reducers({ - featurePreviewsModalVisible: [ - false, - { - showFeaturePreviewsModal: () => true, - hideFeaturePreviewsModal: () => false, - }, - ], activeFeedbackFlagKey: { beginEarlyAccessFeatureFeedback: (_, { flagKey }) => flagKey, cancelEarlyAccessFeatureFeedback: () => null, - hideFeaturePreviewsModal: () => null, }, }), listeners(() => ({ @@ -111,25 +100,4 @@ export const featurePreviewsLogic = kea([ }), ], }), - urlToAction(({ actions }) => ({ - '*': (_, _search, hashParams) => { - if (hashParams['panel'] === 'feature-previews') { - actions.showFeaturePreviewsModal() - } - }, - })), - actionToUrl(() => { - return { - showFeaturePreviewsModal: () => { - const hashParams = router.values.hashParams - hashParams['panel'] = 'feature-previews' - return [router.values.location.pathname, router.values.searchParams, hashParams] - }, - hideFeaturePreviewsModal: () => { - const hashParams = router.values.hashParams - delete hashParams['panel'] - return [router.values.location.pathname, router.values.searchParams, hashParams] - }, - } - }), ]) diff --git a/frontend/src/layout/FeaturePreviews/index.ts b/frontend/src/layout/FeaturePreviews/index.ts deleted file mode 100644 index 6101040e9e9f2..0000000000000 --- a/frontend/src/layout/FeaturePreviews/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { featurePreviewsLogic } from './featurePreviewsLogic' -export { FeaturePreviewsModal } from './FeaturePreviewsModal' diff --git a/frontend/src/layout/GlobalModals.tsx b/frontend/src/layout/GlobalModals.tsx index d58ad3ee84d33..11f87bb3a3a69 100644 --- a/frontend/src/layout/GlobalModals.tsx +++ b/frontend/src/layout/GlobalModals.tsx @@ -12,7 +12,6 @@ import { InviteModal } from 'scenes/settings/organization/InviteModal' import { UpgradeModal } from 'scenes/UpgradeModal' import { userLogic } from 'scenes/userLogic' -import { FeaturePreviewsModal } from './FeaturePreviews' import type { globalModalsLogicType } from './GlobalModalsType' export const globalModalsLogic = kea([ @@ -53,7 +52,6 @@ export function GlobalModals(): JSX.Element { - {user && user.organization?.enforce_2fa && !user.is_2fa_enabled && ( diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index 110c8f068fdf6..6e4392eb18d84 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -62,7 +62,7 @@ export function Navigation({ {children}
- {!mobileLayout && } + ) diff --git a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss index f2796b41f109e..74965e73091cc 100644 --- a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss +++ b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss @@ -8,21 +8,17 @@ justify-content: center; min-width: 1.25rem; height: 1.25rem; - padding: 0 0.1875rem; + padding: 0.125rem 0.25rem; + font-size: 0.75rem; color: var(--default); text-transform: capitalize; user-select: none; background: var(--accent-3000); + border-color: var(--secondary-3000-button-border-hover); border-width: 1px; + border-bottom-width: 2px; border-radius: 0.25rem; - .posthog-3000 & { - padding: 0.125rem 0.25rem; - font-size: 0.75rem; - border-color: var(--secondary-3000-button-border-hover); - border-bottom-width: 2px; - } - .KeyboardShortcut--muted > & { color: var(--muted); background: none; diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index 9a46da2424e8f..46164e0270db1 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -1,7 +1,7 @@ import './SidePanel.scss' import { IconEllipsis, IconFeatures, IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons' -import { LemonButton, LemonMenu, LemonMenuItems } from '@posthog/lemon-ui' +import { LemonButton, LemonMenu, LemonMenuItems, LemonModal } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Resizer } from 'lib/components/Resizer/Resizer' @@ -22,11 +22,15 @@ import { SidePanelSupport } from './panels/SidePanelSupport' import { sidePanelLogic } from './sidePanelLogic' import { sidePanelStateLogic } from './sidePanelStateLogic' -export const SIDE_PANEL_TABS: Record = { +export const SIDE_PANEL_TABS: Record< + SidePanelTab, + { label: string; Icon: any; Content: any; noModalSupport?: boolean } +> = { [SidePanelTab.Notebooks]: { label: 'Notebooks', Icon: IconNotebook, Content: NotebookPanel, + noModalSupport: true, }, [SidePanelTab.Support]: { label: 'Support', @@ -37,6 +41,7 @@ export const SIDE_PANEL_TABS: Record + {PanelConent ? : null} + + ) + } + return (
+
{title ? ( -

{title}

+

+ {title} +

) : null} {children} - + } onClick={() => closeSidePanel()} />
diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelFeaturePreviews.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelFeaturePreviews.tsx index a0be65ed4ceb7..cf5c67c209ed5 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelFeaturePreviews.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelFeaturePreviews.tsx @@ -1,6 +1,6 @@ import { LemonBanner } from '@posthog/lemon-ui' -import { FeaturePreviews } from '~/layout/FeaturePreviews/FeaturePreviewsModal' +import { FeaturePreviews } from '~/layout/FeaturePreviews/FeaturePreviews' import { SidePanelPaneHeader } from '../components/SidePanelPaneHeader' diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx index 43c40e2d2ef3b..381cd59181267 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx @@ -19,29 +19,34 @@ export const SidePanelSupport = (): JSX.Element => { <> -
- - - Submit - - - Cancel - +
+
+ + +
+ + Submit + + + Cancel + +
+
) diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx index 4d65e0770d69c..6d1360cdf90ac 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx @@ -1,5 +1,6 @@ import { actions, kea, listeners, path, reducers } from 'kea' import { actionToUrl, router, urlToAction } from 'kea-router' +import { windowValues } from 'kea-window-values' import { SidePanelTab } from '~/types' @@ -48,6 +49,9 @@ export const sidePanelStateLogic = kea([ }, ], })), + windowValues(() => ({ + modalMode: (window: Window) => window.innerWidth < 992, // Sync width threshold with Sass variable $lg! + })), listeners(({ actions, values }) => ({ // NOTE: We explicitly reference the actions instead of connecting so that people don't accidentally // use this logic instead of sidePanelStateLogic diff --git a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx index b668beb05b42d..9e44414bf1c05 100644 --- a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx +++ b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx @@ -21,7 +21,6 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { ThemeSwitcher } from 'scenes/settings/user/ThemeSwitcher' -import { featurePreviewsLogic } from '~/layout/FeaturePreviews/featurePreviewsLogic' import { AccessLevelIndicator, NewOrganizationButton, @@ -161,13 +160,13 @@ function InstanceSettings(): JSX.Element | null { function FeaturePreviewsButton(): JSX.Element { const { closeAccountPopover } = useActions(navigationLogic) - const { showFeaturePreviewsModal } = useActions(featurePreviewsLogic) + const { openSidePanel } = useActions(sidePanelStateLogic) return ( { closeAccountPopover() - showFeaturePreviewsModal() + openSidePanel(SidePanelTab.FeaturePreviews) }} icon={} fullWidth diff --git a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx index d6c50cc8edea6..1ebe6bb27a29e 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx +++ b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx @@ -1,10 +1,10 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { IconDelete, IconEdit, IconOpenInApp, IconPlus } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' @@ -56,13 +56,13 @@ function AuthorizedUrlForm({ actionId, type }: AuthorizedUrlListProps): JSX.Elem enableFormOnSubmit className="w-full space-y-2" > - + - +
Cancel diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index 684a6d758d5d8..0a83c8efc683b 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -112,7 +112,7 @@ .InsightDetails__footer { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); .profile-package { vertical-align: middle; diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx index 66751fc6e2ffb..ffc72eae269cc 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx @@ -353,6 +353,14 @@ function InsightDetailsInternal({ insight }: { insight: InsightModel }, ref: Rea
+ {insight.last_refresh && ( +
+
Last computed
+
+ +
+
+ )}
) diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss index c42b01543f804..85e6f6422f177 100644 --- a/frontend/src/lib/components/CommandBar/index.scss +++ b/frontend/src/lib/components/CommandBar/index.scss @@ -1,4 +1,4 @@ -.CommandBar__input { +.LemonInput.CommandBar__input { height: 2.75rem; padding-right: 0.375rem; padding-left: 0.75rem; diff --git a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx index 71cac99c617fc..6a56553aae0d1 100644 --- a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx +++ b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx @@ -157,7 +157,6 @@ function DebugCHQueries(): JSX.Element { dataSource={filteredQueries} loading={queriesLoading} loadingSkeletonRows={5} - size="small" pagination={undefined} /> diff --git a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss index 244d3ccc5c711..6ccb132efdc5f 100644 --- a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss +++ b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss @@ -1,7 +1,7 @@ .RollingDateRangeFilter { display: flex; align-items: center; - height: 2rem; + height: 1.6875rem; min-height: 2rem; padding: 1.25rem 0.5rem; font-size: 0.875rem; @@ -11,10 +11,6 @@ cursor: pointer; transition: background 0.3s ease; - .posthog-3000 & { - height: 1.6875rem; - } - &:hover { background-color: var(--mid); } @@ -40,69 +36,47 @@ box-sizing: border-box; display: flex; align-items: center; - height: 2rem; + height: 1.6875rem; margin: 0; margin-right: 0.25rem; margin-left: 0.25rem; - line-height: 1.25rem; + line-height: 1.5rem; background-color: var(--bg-light); border: 1px solid var(--border); border-radius: var(--radius); - .posthog-3000 & { - height: 1.6875rem; - line-height: 1.5rem; - } - .LemonInput { width: 3rem; + height: unset; min-height: 0; padding: 0; border: none; - .posthog-3000 & { - height: unset; - } - input { text-align: center; } } .RollingDateRangeFilter__counter__step { - padding: 0.25rem; - margin: 0 0.25rem; - border-radius: var(--radius); + width: 1.25rem; + height: 100%; + padding: 0; + margin: 0; + text-align: center; + border-radius: calc(var(--radius) - 1px); &:first-child { - .posthog-3000 & { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + border-top-right-radius: 0; + border-bottom-right-radius: 0; } &:last-child { - .posthog-3000 & { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - - .posthog-3000 & { - width: 1.25rem; - height: 100%; - padding: 0; - margin: 0; - text-align: center; - border-radius: calc(var(--radius) - 1px); + border-top-left-radius: 0; + border-bottom-left-radius: 0; } &:hover { - background-color: var(--primary-highlight); - - .posthog-3000 & { - background-color: var(--accent-3000); - } + background-color: var(--accent-3000); } } } diff --git a/frontend/src/lib/components/DatePicker.scss b/frontend/src/lib/components/DatePicker.scss index 083127c9dff86..43ecd38624633 100644 --- a/frontend/src/lib/components/DatePicker.scss +++ b/frontend/src/lib/components/DatePicker.scss @@ -1,130 +1,101 @@ .ant-picker { + color: var(--default); + background: var(--lemon-button-bg-color); + border-color: var(--secondary-3000-button-border); box-shadow: none !important; -} -.posthog-3000 { - .ant-picker { + .ant-picker-suffix { color: var(--default); - background: var(--lemon-button-bg-color); - border-color: var(--secondary-3000-button-border); - - .ant-picker-suffix { - color: var(--default); - } } - .ant-picker:hover { + &:hover { border-color: var(--secondary-3000-button-border-hover); } +} - .ant-picker-panel-container { - color: var(--default); - background: var(--bg-3000); - border: 1px solid var(--border); +.ant-picker-panel-container { + color: var(--default); + background: var(--bg-3000); + border: 1px solid var(--border); - * { - border-color: var(--border); - } + * { + border-color: var(--border); } +} - .ant-picker-time-panel-column > li.ant-picker-time-panel-cell-selected .ant-picker-time-panel-cell-inner { - background: var(--primary-highlight); - } +.ant-picker-time-panel-column > li.ant-picker-time-panel-cell-selected .ant-picker-time-panel-cell-inner { + background: var(--primary-highlight); - .ant-picker-time-panel .ant-picker-time-panel-column:nth-child(3)::after { - // :HACKY: fix to keep the whole am/pm section in view - display: none; + [theme='dark'] & { + background: rgba(#f7a503, 0.4); } +} - .ant-picker-cell .ant-picker-cell-inner { - border-radius: var(--radius); - } +.ant-picker-time-panel .ant-picker-time-panel-column:nth-child(3)::after { + // :HACKY: fix to keep the whole am/pm section in view + display: none; +} - .ant-picker-cell.ant-picker-cell-selected .ant-picker-cell-inner { - color: var(--default); - background: var(--primary-highlight); - } +.ant-picker-cell .ant-picker-cell-inner { + border-radius: var(--radius); +} - .ant-picker-cell.ant-picker-cell-today .ant-picker-cell-inner::before { - background: none; - border-color: var(--text-secondary-3000); - } +.ant-picker-cell.ant-picker-cell-selected .ant-picker-cell-inner { + color: var(--default); + background: var(--primary-highlight); - .ant-picker-cell:hover:not( - .ant-picker-cell-selected, - .ant-picker-cell-range-start, - .ant-picker-cell-range-end, - .ant-picker-cell-range-hover-start, - .ant-picker-cell-range-hover-end - ) - .ant-picker-cell-inner { - background: var(--secondary-3000); + [theme='dark'] & { + color: var(--default); + background: rgba(#f7a503, 0.4); } +} - .ant-picker-cell:hover:not(.ant-picker-cell-in-view) .ant-picker-cell-inner, - .ant-picker-cell:hover:not( - .ant-picker-cell-today, - .ant-picker-cell-selected, - .ant-picker-cell-range-start, - .ant-picker-cell-range-end, - .ant-picker-cell-range-hover-start, - .ant-picker-cell-range-hover-end - ) - .ant-picker-cell-inner, - .ant-picker-time-panel-column > li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner:hover { - background: var(--secondary-3000); - } +.ant-picker-cell.ant-picker-cell-today .ant-picker-cell-inner::before { + background: none; + border-color: var(--text-secondary-3000); - .ant-picker-footer .ant-btn-primary { - color: var(--primary); - text-shadow: none; + [theme='dark'] & { background: none; - border-color: var(--primary); - border-radius: 0.25rem; - box-shadow: none; - } - - .ant-picker-footer .ant-btn-primary:not(:disabled):hover { - color: #fff; - background: var(--primary); - } - - .ant-picker-footer .ant-picker-now-btn:hover { - color: var(--primary); - } - - .ant-picker-ok .ant-btn-primary span { - text-transform: uppercase; + border-color: var(--text-secondary-3000); } } -.posthog-3000[theme='dark'] { - .ant-picker-time-panel-column > li.ant-picker-time-panel-cell-selected .ant-picker-time-panel-cell-inner { - background: rgba(#f7a503, 0.4); - } - - .ant-picker-cell:hover:not( - .ant-picker-cell-selected, - .ant-picker-cell-range-start, - .ant-picker-cell-range-end, - .ant-picker-cell-range-hover-start, - .ant-picker-cell-range-hover-end - ) - .ant-picker-cell-inner { +.ant-picker-cell:hover:not(.ant-picker-cell-in-view) .ant-picker-cell-inner, +.ant-picker-cell:hover:not( + .ant-picker-cell-today, + .ant-picker-cell-selected, + .ant-picker-cell-range-start, + .ant-picker-cell-range-end, + .ant-picker-cell-range-hover-start, + .ant-picker-cell-range-hover-end + ) + .ant-picker-cell-inner, +.ant-picker-time-panel-column > li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner:hover { + background: var(--secondary-3000); + + [theme='dark'] & { background: var(--muted-3000-dark); } +} - .ant-picker-cell.ant-picker-cell-selected .ant-picker-cell-inner { - color: var(--default); - background: rgba(#f7a503, 0.4); - } +.ant-picker-footer .ant-btn-primary { + color: var(--primary); + text-shadow: none; + background: none; + border-color: var(--primary); + border-radius: 0.25rem; + box-shadow: none; +} - .ant-picker-time-panel-column > li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner:hover { - background: var(--muted-3000-dark); - } +.ant-picker-footer .ant-btn-primary:not(:disabled):hover { + color: #fff; + background: var(--primary); +} - .ant-picker-cell.ant-picker-cell-today .ant-picker-cell-inner::before { - background: none; - border-color: var(--text-secondary-3000); - } +.ant-picker-footer .ant-picker-now-btn:hover { + color: var(--primary); +} + +.ant-picker-ok .ant-btn-primary span { + text-transform: uppercase; } diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss b/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss index d219c76b01c8f..d1f0ae7c48181 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss @@ -22,21 +22,15 @@ } .editable { - text-decoration: underline dotted; - text-decoration-color: var(--primary-3000); + padding: 0.125rem 0.25rem; + margin-left: -0.25rem; cursor: pointer; + border: 1px solid transparent; + border-radius: calc(var(--radius) * 0.75); - .posthog-3000 & { - padding: 0.125rem 0.25rem; - margin-left: -0.25rem; - text-decoration: none; - border: 1px solid transparent; - border-radius: calc(var(--radius) * 0.75); - - &:hover { - background: var(--bg-light); - border: 1px solid var(--border-light); - } + &:hover { + background: var(--bg-light); + border: 1px solid var(--border-light); } } } diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx index 021a9eca39145..563ab6b8f370a 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx @@ -402,7 +402,6 @@ export function PropertiesTable({
- + {({ value, onChange }) => ( )} - + {insight && ( - + {({ value, onChange }) => ( )} - + )} {showLegendCheckbox && ( - + {({ value, onChange }) => ( )} - + )} {recordingId && ( - + {({ value, onChange }) => ( )} - + )} {previewIframe && ( diff --git a/frontend/src/lib/components/SignupReferralSource.tsx b/frontend/src/lib/components/SignupReferralSource.tsx index c56ff9931b7ff..053bbaeec04f1 100644 --- a/frontend/src/lib/components/SignupReferralSource.tsx +++ b/frontend/src/lib/components/SignupReferralSource.tsx @@ -1,15 +1,15 @@ import { LemonInput } from '@posthog/lemon-ui' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' export default function SignupReferralSource({ disabled }: { disabled: boolean }): JSX.Element { return ( - + - + ) } diff --git a/frontend/src/lib/components/SignupRoleSelect.tsx b/frontend/src/lib/components/SignupRoleSelect.tsx index 4cf324f001d00..555c74181d9db 100644 --- a/frontend/src/lib/components/SignupRoleSelect.tsx +++ b/frontend/src/lib/components/SignupRoleSelect.tsx @@ -1,9 +1,9 @@ -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' export default function SignupRoleSelect({ className }: { className?: string }): JSX.Element { return ( - + - + ) } diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index 9da0f337a58d9..2002aa16d6318 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -4,10 +4,10 @@ import { Form } from 'kea-forms' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { dayjs } from 'lib/dayjs' -import { Field } from 'lib/forms/Field' import { IconChevronLeft } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' @@ -166,13 +166,13 @@ export function EditSubscription({ )} - + - + - + - + {subscription.target_type === 'email' ? ( <> @@ -193,7 +193,7 @@ export function EditSubscription({ )} - )} - + - + - + ) : null} @@ -262,7 +262,7 @@ export function EditSubscription({ ) : ( <> - )} - + {showSlackMembershipWarning ? ( - +
@@ -315,7 +315,7 @@ export function EditSubscription({
-
+ ) : null} )} @@ -324,9 +324,9 @@ export function EditSubscription({ {subscription.target_type === 'webhook' ? ( <> - + - +
Webhooks will be called with a HTTP POST request. The webhook endpoint should respond with a healthy HTTP code (2xx). @@ -338,10 +338,10 @@ export function EditSubscription({ Recurrence
Send every - + - - + + - + {subscription.frequency === 'weekly' && ( <> on - + {({ value, onChange }) => ( onChange([val])} /> )} - + )} {subscription.frequency === 'monthly' && ( <> on the - + {({ value, onChange }) => ( )} - - + + {({ value, onChange }) => ( )} - + )} by - + {({ value, onChange }) => ( )} - +
diff --git a/frontend/src/lib/components/Support/SupportForm.tsx b/frontend/src/lib/components/Support/SupportForm.tsx index b33bab9974cf0..585d4a07b0cdc 100644 --- a/frontend/src/lib/components/Support/SupportForm.tsx +++ b/frontend/src/lib/components/Support/SupportForm.tsx @@ -1,12 +1,20 @@ -import { LemonInput, LemonSegmentedButton, LemonSegmentedButtonOption, lemonToast, Link } from '@posthog/lemon-ui' +import { + LemonBanner, + LemonInput, + LemonSegmentedButton, + LemonSegmentedButtonOption, + lemonToast, + Link, +} from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { useUploadFiles } from 'lib/hooks/useUploadFiles' import { IconBugReport, IconFeedback, IconHelpOutline } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' import { LemonSelect } from 'lib/lemon-ui/LemonSelect/LemonSelect' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import posthog from 'posthog-js' import { useEffect, useRef } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { userLogic } from 'scenes/userLogic' @@ -73,27 +81,62 @@ export function SupportForm(): JSX.Element | null { > {!user && ( <> - + - - + + - + )} - + - - + + ({ label: value, value: key, + 'data-attr': `support-form-target-area-${key}`, }))} /> - - + + {posthog.getFeatureFlag('show-troubleshooting-docs-in-support-form') === 'test-replay-banner' && + sendSupportRequest.target_area === 'session_replay' && ( + + <> + We're pretty proud of our docs. Check out these helpful links: +
    +
  • + + Session replay troubleshooting + +
  • +
  • + + How to control which sessions you record + +
  • +
+ +
+ )} + {posthog.getFeatureFlag('show-troubleshooting-docs-in-support-form') === 'test-replay-banner' && + sendSupportRequest.target_area === 'toolbar' && ( + + <> + We're pretty proud of our docs.{' '} + + Check out this troubleshooting guide + + + + )} + ({ @@ -101,7 +144,7 @@ export function SupportForm(): JSX.Element | null { value: key, }))} /> -
+ Check out the{' '} @@ -109,7 +152,7 @@ export function SupportForm(): JSX.Element | null { . - @@ -132,7 +175,7 @@ export function SupportForm(): JSX.Element | null { )} )} - +
) } diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss index 5c4d5c0d5fedc..727a7761b4fdd 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss @@ -66,12 +66,8 @@ } &.hover { - background-color: var(--primary-bg-hover); + background-color: var(--bg-3000); border-radius: var(--radius); - - .posthog-3000 & { - background-color: var(--bg-3000); - } } &.selected { diff --git a/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx b/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx index e3e4226744377..c8a5f2f5ac094 100644 --- a/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx +++ b/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx @@ -1,6 +1,6 @@ import { HandleUnitChange } from 'lib/components/UnitPicker/UnitPicker' -import { PureField } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { capitalizeFirstLetter } from 'lib/utils' @@ -72,7 +72,7 @@ export function CustomUnitModal({ } > - @@ -87,7 +87,7 @@ export function CustomUnitModal({ } > - + ) } diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index cdafc82f8a9c5..b0201f08e2f86 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -147,7 +147,6 @@ export const FEATURE_FLAGS = { QUERY_TIMINGS: 'query-timings', // owner: @mariusandra QUERY_ASYNC: 'query-async', // owner: @webjunkie POSTHOG_3000_NAV: 'posthog-3000-nav', // owner: @Twixes - POSTHOG_3000_WELCOME_ANNOUNCEMENT: 'posthog-3000-welcome-announcement', // owner: #posthog-3000 ENABLE_PROMPTS: 'enable-prompts', // owner: @lharries HEDGEHOG_MODE: 'hedgehog-mode', // owner: @benjackwhite HEDGEHOG_MODE_DEBUG: 'hedgehog-mode-debug', // owner: @benjackwhite diff --git a/frontend/src/lib/forms/Errors.tsx b/frontend/src/lib/forms/Errors.tsx deleted file mode 100644 index 5dcbb4c786e4a..0000000000000 --- a/frontend/src/lib/forms/Errors.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export interface FormErrorsProps { - errors: Record -} -export function FormErrors({ errors }: FormErrorsProps): JSX.Element { - return ( - <> - {Object.entries(errors) - .filter(([, error]) => !!error) - .map(([key, error]) => ( -
- {key}: - {typeof error === 'object' ? : String(error)} -
- ))} - - ) -} diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss index 6613a79bde94f..ed6baaf7e8955 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss @@ -47,9 +47,7 @@ } } - .posthog-3000 & { - .LemonCalendar__range--boundary { - background-color: var(--glass-border-3000); - } + .LemonCalendar__range--boundary { + background-color: var(--glass-border-3000); } } diff --git a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss index 6b64da3a4091f..d83d4f5e898c1 100644 --- a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss +++ b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss @@ -13,7 +13,7 @@ label { --tick-length: 12.73; // Approximation of tick length, which is (3 + 6) * sqrt(2) - --box-color: var(--primary); + --box-color: var(--primary-3000); display: flex; gap: 0.5rem; @@ -21,23 +21,15 @@ min-height: 1.5rem; cursor: pointer; - .posthog-3000 & { - --box-color: var(--primary-3000); - } - > .LemonCheckbox__box { flex-shrink: 0; width: 1rem; height: 1rem; background: var(--bg-light); border: 1.5px solid var(--border-bold); - border-radius: 0.1875rem; // Intentionally a bit smaller than --radius + border-radius: 0.25rem; // Intentionally a bit smaller than --radius transition: border 200ms ease, background 200ms ease; - &.posthog-3000 { - border-radius: 0.25rem; // Intentionally a bit smaller than --radius - } - path { stroke: var(--bg-light); stroke-dasharray: var(--tick-length); diff --git a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss index 72c00bb67513a..f5b6972d48465 100644 --- a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss +++ b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss @@ -24,7 +24,7 @@ font-weight: 500 !important; // Override status="stealth"'s font-weight border-radius: 0 !important; - .posthog-3000 &.LemonButton:active { + &.LemonButton:active { transform: inherit; } } @@ -33,12 +33,9 @@ box-sizing: content-box; height: 0; overflow: hidden; + background: var(--bg-light); border-top-width: 1px; transition: height 200ms ease; - - .posthog-3000 & { - background: var(--bg-light); - } } .LemonCollapsePanel__content { diff --git a/frontend/src/lib/forms/Field.stories.tsx b/frontend/src/lib/lemon-ui/LemonField/LemonField.stories.tsx similarity index 84% rename from frontend/src/lib/forms/Field.stories.tsx rename to frontend/src/lib/lemon-ui/LemonField/LemonField.stories.tsx index 4f13055dccb13..34a25ffa50243 100644 --- a/frontend/src/lib/forms/Field.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonField/LemonField.stories.tsx @@ -3,12 +3,12 @@ import { Meta } from '@storybook/react' import { kea, path, useAllValues } from 'kea' import { Form, forms } from 'kea-forms' -import { Field, PureField } from './Field' -import type { formLogicType } from './Field.storiesType' +import { LemonField } from './LemonField' +import type { formLogicType } from './LemonField.storiesType' -const meta: Meta = { - title: 'Lemon UI/Forms and Fields', - component: PureField, +const meta: Meta = { + title: 'Lemon UI/Lemon Field', + component: LemonField, parameters: { docs: { description: { @@ -30,7 +30,7 @@ export default meta export const _PureFields = (): JSX.Element => { return (
- { } > - + - With info!}> + With info!}> - + - + - - + + - +
Cancel @@ -112,7 +112,7 @@ export const _FieldsWithKeaForm = (): JSX.Element => { return (
- @@ -127,18 +127,18 @@ export const _FieldsWithKeaForm = (): JSX.Element => { } > - + - With info!}> + With info!}> - + - + - - + + - +
Cancel diff --git a/frontend/src/lib/forms/Field.tsx b/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx similarity index 83% rename from frontend/src/lib/forms/Field.tsx rename to frontend/src/lib/lemon-ui/LemonField/LemonField.tsx index b0924ae43ae5b..dcde1c3e4e95a 100644 --- a/frontend/src/lib/forms/Field.tsx +++ b/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx @@ -3,7 +3,7 @@ import { Field as KeaField, FieldProps as KeaFieldProps } from 'kea-forms/lib/co import { IconErrorOutline } from 'lib/lemon-ui/icons' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -export type PureFieldProps = { +export type LemonPureFieldProps = { /** The label name to be displayed */ label?: React.ReactNode /** Will show a muted (optional) next to the label */ @@ -25,8 +25,7 @@ export type PureFieldProps = { htmlFor?: string } -/** A "Pure" field - used when you want the Field styles without the Kea form functionality */ -export const PureField = ({ +const LemonPureField = ({ label, info, error, @@ -38,7 +37,7 @@ export const PureField = ({ children, inline, onClick, -}: PureFieldProps): JSX.Element => { +}: LemonPureFieldProps): JSX.Element => { return (
& Pick +export type LemonFieldProps = Omit & Pick -export const Field = ({ +/** A field for use within a Kea form. Outside a form use `LemonField.Pure`. */ +export const LemonField = ({ name, help, className, @@ -78,11 +78,10 @@ export const Field = ({ inline, info, ...keaFieldProps -}: FieldProps): JSX.Element => { - /** Drop-in replacement antd template for kea forms */ +}: LemonFieldProps): JSX.Element => { const template: KeaFieldProps['template'] = ({ label, kids, error }) => { return ( - {kids} - + ) } return } + +/** A field without Kea form functionality. Within a form use `LemonField`. */ +LemonField.Pure = LemonPureField diff --git a/frontend/src/lib/lemon-ui/LemonField/index.ts b/frontend/src/lib/lemon-ui/LemonField/index.ts new file mode 100644 index 0000000000000..2776e83c43e46 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonField/index.ts @@ -0,0 +1 @@ +export { LemonField, type LemonFieldProps, type LemonPureFieldProps } from './LemonField' diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss index 6018b10f41bb7..28a84357dadeb 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss @@ -3,7 +3,7 @@ gap: 0.25rem; align-items: center; justify-content: center; - height: 2.5rem; + height: calc(2.125rem + 3px); // Medium size button height + button shadow height padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; @@ -15,24 +15,12 @@ border: 1px solid var(--border); border-radius: var(--radius); - .posthog-3000 & { - height: calc(2.125rem + 3px); // Medium size button height + button shadow height - } - &:hover:not([aria-disabled='true']) { - border-color: var(--primary-3000-hover); - - .posthog-3000 & { - border-color: var(--border-bold); - } + border-color: var(--border-bold); } &.LemonInput--focused:not([aria-disabled='true']) { - border-color: var(--primary-3000); - - .posthog-3000 & { - border-color: var(--border-active); - } + border-color: var(--border-active); } &.LemonInput--transparent-background { diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss index d7c3b21b6ba00..a10cc87433af0 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss @@ -33,7 +33,7 @@ max-height: 90%; margin: 1rem auto; background-color: var(--bg-light); - border: 1px solid var(--border-3000); + border: 1px solid var(--secondary-3000-button-border); border-radius: var(--radius); box-shadow: var(--modal-shadow-elevation); opacity: 0; @@ -91,10 +91,6 @@ height: 100%; overflow: hidden; } - - .posthog-3000 & { - border-color: var(--secondary-3000-button-border); - } } .LemonModal__header { diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx index 022785fa7e56b..e464561b76fbb 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx @@ -33,6 +33,7 @@ export interface LemonModalProps { /** When enabled, the modal content will only include children allowing greater customisation */ simple?: boolean closable?: boolean + hideCloseButton?: boolean /** If there is unsaved input that's not persisted, the modal can't be closed closed on overlay click. */ hasUnsavedInput?: boolean /** Expands the modal to fill the entire screen */ @@ -78,6 +79,7 @@ export function LemonModal({ forceAbovePopovers = false, contentRef, overlayRef, + hideCloseButton = false, }: LemonModalProps): JSX.Element { const nodeRef = useRef(null) const [ignoredOverlayClickCount, setIgnoredOverlayClickCount] = useState(0) @@ -86,7 +88,7 @@ export function LemonModal({ const modalContent = (
- {closable && ( + {closable && !hideCloseButton && ( // The key causes the div to be re-rendered, which restarts the animation, // providing immediate visual feedback on click
) : ( + // eslint-disable-next-line posthog/warn-elements { diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss index 087aa0f39c5d8..67200ac17bdf3 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss @@ -4,7 +4,7 @@ .ant-select-selector, &.ant-select-single .ant-select-selector { - min-height: 2.5rem; + min-height: 2.125rem; padding: 0.25rem; font-size: 0.875rem; line-height: 1.25rem; @@ -13,10 +13,6 @@ border: 1px solid var(--border); border-radius: var(--radius); - .posthog-3000 & { - min-height: 2.125rem; - } - .ant-select-selection-overflow { gap: 0.25rem; } @@ -71,13 +67,9 @@ padding: 0.5rem; margin: -4px 0; // Counteract antd wrapper background: var(--bg-light); - border: 1px solid var(--primary); + border: 1px solid var(--primary-3000); border-radius: var(--radius); - .posthog-3000 & { - border: 1px solid var(--primary-3000); - } - .ant-select-item { padding: 0; padding-bottom: 0.2rem; diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss index eccd270e72e1f..a63c3e5fdc63c 100644 --- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss +++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss @@ -1,6 +1,6 @@ .LemonSwitch { - --lemon-switch-height: 1.25rem; - --lemon-switch-width: 2.25rem; + --lemon-switch-height: 1.125rem; + --lemon-switch-width: calc(11 / 6 * var(--lemon-switch-height)); // Same proportion as in IconToggle display: flex; gap: 0.5rem; @@ -23,17 +23,13 @@ } &.LemonSwitch--bordered { - min-height: 2.5rem; + min-height: calc(2.125rem + 3px); // Medium size button height + button shadow height padding: 0 0.75rem; line-height: 1.4; background: var(--bg-light); border: 1px solid var(--border); border-radius: var(--radius); - .posthog-3000 & { - min-height: calc(2.125rem + 3px); // Medium size button height + button shadow height - } - &.LemonSwitch--small { gap: 0.5rem; min-height: 2rem; @@ -54,11 +50,6 @@ cursor: not-allowed; // A label with for=* also toggles the switch, so it shouldn't have the text select cursor } } - - .posthog-3000 & { - --lemon-switch-height: 1.125rem; - --lemon-switch-width: calc(11 / 6 * var(--lemon-switch-height)); // Same proportion as in IconToggle - } } .LemonSwitch__button { @@ -79,91 +70,57 @@ .LemonSwitch__slider { position: absolute; - top: 5px; + top: 0; left: 0; display: inline-block; - width: 2.25rem; - height: 0.625rem; - background-color: var(--border); - border-radius: 0.625rem; + width: 100%; + height: 100%; + pointer-events: none; + background-color: var(--border-bold); + border-radius: var(--lemon-switch-height); transition: background-color 100ms ease; - .posthog-3000 & { - top: 0; - width: 100%; - height: 100%; - pointer-events: none; - background-color: var(--border-bold); - border-radius: var(--lemon-switch-height); - } - .LemonSwitch--checked & { - background-color: var(--primary-highlight); - - .posthog-3000 & { - background-color: var(--primary-3000); - } + background-color: var(--primary-3000); } } .LemonSwitch__handle { + --lemon-switch-handle-ratio: calc(3 / 4); // Same proportion as in IconToggle + --lemon-switch-handle-gutter: calc(var(--lemon-switch-height) * calc(1 - var(--lemon-switch-handle-ratio)) / 2); + --lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio)); + --lemon-switch-active-translate: translateX( + calc(var(--lemon-switch-width) - var(--lemon-switch-handle-width) - var(--lemon-switch-handle-gutter) * 2) + ); + position: absolute; - top: 0; - left: 0; + top: var(--lemon-switch-handle-gutter); + left: var(--lemon-switch-handle-gutter); display: flex; align-items: center; justify-content: center; - width: 1.25rem; - height: 1.25rem; + width: var(--lemon-switch-handle-width); + height: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio)); + pointer-events: none; cursor: inherit; background-color: #fff; - border: 2px solid var(--border); + border: none; border-radius: 0.625rem; transition: background-color 100ms ease, transform 100ms ease, width 100ms ease, border-color 100ms ease; - .posthog-3000 & { - --lemon-switch-handle-ratio: calc(3 / 4); // Same proportion as in IconToggle - --lemon-switch-handle-gutter: calc(var(--lemon-switch-height) * calc(1 - var(--lemon-switch-handle-ratio)) / 2); - --lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio)); - --lemon-switch-active-translate: translateX( - calc(var(--lemon-switch-width) - var(--lemon-switch-handle-width) - var(--lemon-switch-handle-gutter) * 2) - ); - - top: var(--lemon-switch-handle-gutter); - left: var(--lemon-switch-handle-gutter); - width: var(--lemon-switch-handle-width); - height: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio)); - pointer-events: none; - background-color: #fff; - border: none; - } - .LemonSwitch--checked & { - background-color: var(--primary-3000); + background-color: #fff; border-color: var(--primary-3000); - transform: translateX(1rem); - - .posthog-3000 & { - background-color: #fff; - transform: var(--lemon-switch-active-translate); - } + transform: var(--lemon-switch-active-translate); } .LemonSwitch--active & { - transform: scale(1.1); - - .posthog-3000 & { - --lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio) * 1.2); + --lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio) * 1.2); - transform: none; - } + transform: none; } .LemonSwitch--active.LemonSwitch--checked & { - transform: translateX(1rem) scale(1.1); - - .posthog-3000 & { - transform: var(--lemon-switch-active-translate); - } + transform: var(--lemon-switch-active-translate); } } diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss index 2bf5449f0b4aa..b944d2f17635e 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss @@ -1,5 +1,5 @@ .LemonTable { - --row-base-height: 3rem; + --row-base-height: auto; --row-horizontal-padding: 1rem; --lemon-table-background-color: var(--bg-table); @@ -7,6 +7,7 @@ flex: 1; width: 100%; overflow: hidden; + font-size: 13px; background: var(--lemon-table-background-color); border: 1px solid var(--border); border-radius: var(--radius); @@ -24,12 +25,6 @@ border: none; } - .posthog-3000 & { - --row-base-height: auto; - - font-size: 13px; - } - &.LemonTable--with-ribbon { --row-ribbon-width: 0.25rem; @@ -49,18 +44,12 @@ } &--xs { - --row-base-height: 2rem; - .LemonTable__content > table > tbody > tr > td { padding-top: 0.25rem; padding-bottom: 0.25rem; } } - &--small { - --row-base-height: 2.5rem; - } - &--embedded { background: none; border: none; @@ -72,11 +61,8 @@ .LemonTable__content > table { > thead { + background: none; border-bottom: none; - - .posthog-3000 & { - background: none; - } } > thead, @@ -127,12 +113,10 @@ } a.Link { - .posthog-3000 & { - color: var(--default); + color: var(--default); - &:not(:disabled):hover { - color: var(--primary-3000-hover); - } + &:not(:disabled):hover { + color: var(--primary-3000-hover); } } } @@ -147,14 +131,12 @@ font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03125rem; - background: var(--mid); - - .posthog-3000 & { - background: var(--lemon-table-background-color); - } + background: var(--lemon-table-background-color); > tr { > th { + padding-top: 0.5rem; + padding-bottom: 0.5rem; font-weight: 700; text-align: left; @@ -162,15 +144,8 @@ // Also it needs to be on the th - any higher and safari will not render the shadow box-shadow: inset 0 -1px var(--border); - .posthog-3000 & { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - } - .LemonButton { - .posthog-3000 & { - margin: -0.5rem 0; - } + margin: -0.5rem 0; } } @@ -293,30 +268,26 @@ .LemonTable__header { cursor: default; - .posthog-3000 & { - .LemonTable__header-content { - color: var(--text-secondary); - } + .LemonTable__header-content { + color: var(--text-secondary); } &.LemonTable__header--actionable { cursor: pointer; - .posthog-3000 & { - &:hover { - &:not(:has(.LemonTable__header--no-hover:hover)) { - .LemonTable__header-content { - color: var(--default); - } - } - } - - &:active { + &:hover { + &:not(:has(.LemonTable__header--no-hover:hover)) { .LemonTable__header-content { color: var(--default); } } } + + &:active { + .LemonTable__header-content { + color: var(--default); + } + } } } @@ -346,11 +317,7 @@ } .LemonTable__header--sticky::before { - background: var(--mid); - - .posthog-3000 & { - background: var(--lemon-table-background-color); - } + background: var(--lemon-table-background-color); } // Stickiness is disabled in snapshots due to flakiness diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx index b0c604b5e2cf7..a9c360b431cfb 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx @@ -176,9 +176,6 @@ WithExpandableRows.args = { export const Small: Story = BasicTemplate.bind({}) Small.args = { size: 'small' } -export const XSmall: Story = BasicTemplate.bind({}) -XSmall.args = { size: 'xs' } - export const Embedded: Story = BasicTemplate.bind({}) Embedded.args = { embedded: true } diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx index 7cba42509ac08..8fb3ee58a65ae 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx @@ -47,7 +47,7 @@ export interface LemonTableProps> { /** Function that for each row determines what props should its `tr` element have based on the row's record. */ onRow?: (record: T) => Omit, 'key'> /** How tall should rows be. The default value is `"middle"`. */ - size?: 'xs' | 'small' | 'middle' + size?: 'small' | 'middle' /** Whether this table already is inset, meaning it needs reduced horizontal padding (0.5rem instead of 1rem). */ inset?: boolean /** An embedded table has no border around it and no background. This way it blends better into other components. */ diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss index c75c739b37653..49a382f7bd061 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss @@ -7,14 +7,10 @@ height: 0; padding: 0.05rem !important; overflow: hidden; - background: var(--primary-bg-active); + background: var(--primary-3000-highlight); border: none !important; transition: height 200ms ease, top 200ms ease; - .posthog-3000 & { - background: var(--primary-3000-highlight); - } - &::after { position: absolute; top: 0; @@ -22,12 +18,8 @@ width: 50%; height: 100%; content: ''; - background: var(--primary); + background: var(--primary-3000); animation: LemonTableLoader__swooping 1.5s linear infinite; - - .posthog-3000 & { - background: var(--primary-3000); - } } &.LemonTableLoader--enter-active, diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss index 93f2d8a133165..807b7765e3420 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss @@ -25,14 +25,9 @@ } &.LemonTag--primary { - color: #fff; - background-color: var(--primary-3000); - - .posthog-3000 & { - color: var(--primary-3000); - background: none; - border-color: var(--primary-3000); - } + color: var(--primary-3000); + background: none; + border-color: var(--primary-3000); } &.LemonTag--option { @@ -41,69 +36,39 @@ } &.LemonTag--highlight { - color: var(--bg-charcoal); - background-color: var(--mark); - - .posthog-3000 & { - color: var(--highlight); - background: none; - border-color: var(--highlight); - } + color: var(--highlight); + background: none; + border-color: var(--highlight); } &.LemonTag--warning { - color: var(--bg-charcoal); - background-color: var(--warning); - - .posthog-3000 & { - color: var(--warning); - background: none; - border-color: var(--warning); - } + color: var(--warning); + background-color: none; + border-color: var(--warning); } &.LemonTag--danger { - color: #fff; - background-color: var(--danger); - - .posthog-3000 & { - color: var(--danger); - background: none; - border-color: var(--danger); - } + color: var(--danger); + background: none; + border-color: var(--danger); } &.LemonTag--success { - color: #fff; - background-color: var(--success); - - .posthog-3000 & { - color: var(--success); - background: none; - border-color: var(--success); - } + color: var(--success); + background: none; + border-color: var(--success); } &.LemonTag--completion { - color: var(--bg-charcoal); - background-color: var(--purple-light); - - .posthog-3000 & { - color: var(--purple); - background: none; - border-color: var(--purple); - } + color: var(--purple); + background: none; + border-color: var(--purple); } &.LemonTag--caution { - color: var(--bg-charcoal); - background-color: var(--danger-lighter); - - .posthog-3000 & { - color: var(--danger-lighter); - background: none; - border-color: var(--danger-lighter); - } + color: var(--danger-lighter); + background: none; + border-color: var(--danger-lighter); } &.LemonTag--muted { diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss index 3c24f908503e9..9c89b575b4a0b 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss @@ -17,11 +17,7 @@ transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease; &:not(:disabled):hover { - border: 1px solid var(--primary-3000-hover); - - .posthog-3000 & { - border-color: var(--border-bold); - } + border: 1px solid var(--border-bold); } &:disabled { @@ -30,11 +26,7 @@ } &:focus:not(:disabled) { - border: 1px solid var(--primary-3000); - - .posthog-3000 & { - border-color: var(--border-active); - } + border: 1px solid var(--border-active); } .Field--error & { diff --git a/frontend/src/lib/lemon-ui/Link/Link.scss b/frontend/src/lib/lemon-ui/Link/Link.scss index 13969c9df18b1..24a0bf5f65522 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.scss +++ b/frontend/src/lib/lemon-ui/Link/Link.scss @@ -28,12 +28,10 @@ } &--subtle { - .posthog-3000 & { - color: var(--default); + color: var(--default); - &:not(:disabled):hover { - color: var(--primary-3000-hover); - } + &:not(:disabled):hover { + color: var(--primary-3000-hover); } } } diff --git a/frontend/src/lib/lemon-ui/Spinner/Spinner.scss b/frontend/src/lib/lemon-ui/Spinner/Spinner.scss index e5ddf2a3cb175..3ab31c08e9be6 100644 --- a/frontend/src/lib/lemon-ui/Spinner/Spinner.scss +++ b/frontend/src/lib/lemon-ui/Spinner/Spinner.scss @@ -78,7 +78,7 @@ position: relative; } - .posthog-3000 &.SpinnerOverlay--scene-level::before { + &.SpinnerOverlay--scene-level::before { background: var(--bg-3000); } } diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx b/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx index 0e5066658245e..a7c5e4d9237f7 100644 --- a/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx +++ b/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx @@ -203,7 +203,6 @@ export const LineGraph = (): JSX.Element => { }, }, ]} - size="small" uppercaseHeader={false} rowRibbonColor={(_datum, index) => getSeriesColor(index)} showHeader diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index 443e357cac026..30df1eb1c3424 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -265,6 +265,7 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { return { suggestions, + incomplete: response.incomplete_list, } }, }) diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx index b7b3ec8bf4e2c..df2c131fdc499 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx @@ -1,9 +1,9 @@ import './EditorFilterGroup.scss' -import { PureField } from 'lib/forms/Field' import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { LemonBadge } from 'lib/lemon-ui/LemonBadge/LemonBadge' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { slugify } from 'lib/utils' import { Fragment, useState } from 'react' @@ -49,13 +49,13 @@ export function EditorFilterGroup({ insightProps, editorFilterGroup }: EditorFil } return ( - : Label} info={tooltip} showOptional={showOptional} > {Component ? : null} - + ) })} diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss index 618d3c39bca1f..8eface52ec75f 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss @@ -2,11 +2,8 @@ .property-group { padding: 0.5rem; background-color: var(--side); + border-width: 1px; border-radius: var(--radius); - - .posthog-3000 & { - border-width: 1px; - } } .property-group-and-or-separator { diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 2c10b751be7f7..65324bebb1349 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1946,6 +1946,10 @@ "HogQLAutocompleteResponse": { "additionalProperties": false, "properties": { + "incomplete_list": { + "description": "Whether or not the suggestions returned are complete", + "type": "boolean" + }, "suggestions": { "items": { "$ref": "#/definitions/AutocompleteCompletionItem" @@ -1953,7 +1957,7 @@ "type": "array" } }, - "required": ["suggestions"], + "required": ["suggestions", "incomplete_list"], "type": "object" }, "HogQLExpression": { @@ -3583,6 +3587,10 @@ { "additionalProperties": false, "properties": { + "incomplete_list": { + "description": "Whether or not the suggestions returned are complete", + "type": "boolean" + }, "suggestions": { "items": { "$ref": "#/definitions/AutocompleteCompletionItem" @@ -3590,7 +3598,7 @@ "type": "array" } }, - "required": ["suggestions"], + "required": ["suggestions", "incomplete_list"], "type": "object" }, { @@ -5000,6 +5008,9 @@ "WebOverviewQuery": { "additionalProperties": false, "properties": { + "compare": { + "type": "boolean" + }, "dateRange": { "$ref": "#/definitions/DateRange" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index d6a7bba63e99e..39777ac23a726 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -297,6 +297,8 @@ export interface AutocompleteCompletionItem { export interface HogQLAutocompleteResponse { suggestions: AutocompleteCompletionItem[] + /** Whether or not the suggestions returned are complete */ + incomplete_list: boolean } export interface HogQLMetadata extends DataNode { @@ -971,6 +973,7 @@ export interface WebAnalyticsQueryBase { export interface WebOverviewQuery extends WebAnalyticsQueryBase { kind: NodeKind.WebOverviewQuery response?: WebOverviewQueryResponse + compare?: boolean } export interface WebOverviewItem { diff --git a/frontend/src/scenes/actions/ActionEdit.tsx b/frontend/src/scenes/actions/ActionEdit.tsx index 8da08b54dcca1..c1cbcc4145db4 100644 --- a/frontend/src/scenes/actions/ActionEdit.tsx +++ b/frontend/src/scenes/actions/ActionEdit.tsx @@ -5,10 +5,10 @@ import { combineUrl, router } from 'kea-router' import { EditableField } from 'lib/components/EditableField/EditableField' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' -import { Field } from 'lib/forms/Field' import { IconInfo, IconPlayCircle, IconPlus, IconWarning } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { Link } from 'lib/lemon-ui/Link' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' @@ -69,7 +69,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): - + {({ value, onChange }) => ( )} - - + + {({ value, onChange }) => ( !action.tags?.includes(tag))} /> )} - + } buttons={ @@ -175,7 +175,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
- + {({ onChange }) => (
)} -
- + + {({ value: stepsValue, onChange }) => (
{stepsValue.map((step: ActionStepType, index: number) => { @@ -239,10 +239,10 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
)} - +
- + {({ value, onChange }) => (
)} -
+ {!action.bytecode_error && action.post_to_slack && ( <> - + {({ value, onChange }) => ( <> Message format @@ -294,7 +294,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): )} - + )}
diff --git a/frontend/src/scenes/annotations/AnnotationModal.tsx b/frontend/src/scenes/annotations/AnnotationModal.tsx index ad4edf2b00b23..4492b82231dc7 100644 --- a/frontend/src/scenes/annotations/AnnotationModal.tsx +++ b/frontend/src/scenes/annotations/AnnotationModal.tsx @@ -2,8 +2,8 @@ import { LemonButton, LemonModal, LemonModalProps, LemonSelect, LemonTextArea, L import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { DatePicker } from 'lib/components/DatePicker' -import { Field } from 'lib/forms/Field' import { IconWarning } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { shortTimeZone } from 'lib/utils' import { urls } from 'scenes/urls' @@ -81,7 +81,7 @@ export function AnnotationModal({ className="space-y-4" >
- @@ -101,8 +101,8 @@ export function AnnotationModal({ showSecond={false} format={ANNOTATION_DAYJS_FORMAT} /> - - + + - +
- + - + ) diff --git a/frontend/src/scenes/authentication/InviteSignup.tsx b/frontend/src/scenes/authentication/InviteSignup.tsx index 0fce0f5aaeda7..d36fb9df47efa 100644 --- a/frontend/src/scenes/authentication/InviteSignup.tsx +++ b/frontend/src/scenes/authentication/InviteSignup.tsx @@ -6,8 +6,8 @@ import { BridgePage } from 'lib/components/BridgePage/BridgePage' import PasswordStrength from 'lib/components/PasswordStrength' import SignupRoleSelect from 'lib/components/SignupRoleSelect' import { SocialLoginButtons } from 'lib/components/SocialLoginButton/SocialLoginButton' -import { Field, PureField } from 'lib/forms/Field' import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { Link } from 'lib/lemon-ui/Link' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' @@ -218,10 +218,10 @@ function UnauthenticatedAcceptInvite({ invite }: { invite: PrevalidatedInvite }) >

Create your PostHog account

- + - - + @@ -241,9 +241,9 @@ function UnauthenticatedAcceptInvite({ invite }: { invite: PrevalidatedInvite }) autoFocus={window.screen.width >= 768} // do not autofocus on small-width screens disabled={isSignupSubmitting} /> - + - - + - + {({ value, onChange }) => { return ( ) }} - + - + - +
- @@ -138,7 +138,7 @@ export function Login(): JSX.Element { placeholder="••••••••••" autoComplete="current-password" /> - +
{precheckResponse.status === 'pending' || !precheckResponse.sso_enforcement ? ( {generalError && {generalError.detail}} - + - + Enter your email address. If an account exists, you’ll receive an email with a password reset link soon.
- + - + )} - @@ -72,9 +72,9 @@ function NewPasswordForm(): JSX.Element { placeholder="••••••••••" data-attr="password" /> - + - + - + { } return ( <> - setRegionModalOpen(true)}> + setRegionModalOpen(true)}> { if (!region) { @@ -98,7 +98,7 @@ const RegionSelect = (): JSX.Element | null => { ]} fullWidth /> - + diff --git a/frontend/src/scenes/authentication/Setup2FA.tsx b/frontend/src/scenes/authentication/Setup2FA.tsx index d8ec16c187a8f..c1f84f3f1e03a 100644 --- a/frontend/src/scenes/authentication/Setup2FA.tsx +++ b/frontend/src/scenes/authentication/Setup2FA.tsx @@ -3,8 +3,8 @@ import './Setup2FA.scss' import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonField } from 'lib/lemon-ui/LemonField' import { setup2FALogic } from './setup2FALogic' @@ -21,7 +21,7 @@ export function Setup2FA({ onSuccess }: { onSuccess: () => void }): JSX.Element
{generalError && {generalError.detail}} - + void }): JSX.Element inputMode="numeric" autoComplete="one-time-code" /> - + Login diff --git a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx index 5687d7490c8bb..237ab96e27df5 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx +++ b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx @@ -3,7 +3,7 @@ import { useValues } from 'kea' import { Form } from 'kea-forms' import PasswordStrength from 'lib/components/PasswordStrength' import { SocialLoginButtons } from 'lib/components/SocialLoginButton/SocialLoginButton' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { Link } from 'lib/lemon-ui/Link' import { useEffect, useRef } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' @@ -31,7 +31,7 @@ export function SignupPanel1(): JSX.Element | null { )} - + - + {!preflight?.demo && ( - @@ -62,7 +62,7 @@ export function SignupPanel1(): JSX.Element | null { placeholder="••••••••••" disabled={isSignupPanel1Submitting} /> - + )} - + - - + + - +
diff --git a/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx b/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx index 1c5ae84c4f2cb..5a2cc5c9263a1 100644 --- a/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx @@ -1,8 +1,8 @@ import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCalendarSelectInput } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { userLogic } from 'scenes/userLogic' @@ -59,13 +59,13 @@ export function BatchExportBackfillModal(): JSX.Element { enableFormOnSubmit className="space-y-2" > - + {({ value, onChange }) => ( )} - + - + {({ value, onChange }) => ( )} - + ) diff --git a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx index 2ec791c86aebb..601dbc2475182 100644 --- a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx @@ -2,10 +2,10 @@ import { LemonButton, LemonCheckbox, LemonDivider, LemonInput, LemonSelect } fro import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { FEATURE_FLAGS } from 'lib/constants' -import { Field } from 'lib/forms/Field' import { IconInfo } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonCalendarSelectInput } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' @@ -81,13 +81,13 @@ export function BatchExportsEditFields({ <>
{!isPipeline && ( - + - + )}
- - + {(!isPipeline || batchExportConfigForm.end_at) && ( // Not present in the new UI unless grandfathered in - )} - + )}
@@ -140,7 +140,7 @@ export function BatchExportsEditFields({ {isNew && !isPipeline ? ( - + } /> - + ) : null}
- + - + {!batchExportConfigForm.destination ? (

Select a destination to continue configuring

) : batchExportConfigForm.destination === 'S3' ? ( <>
- + - - + + - +
- + - +
- + - + - + - +
- + - + - + - + {batchExportConfigForm.encryption == 'aws:kms' && ( - + - + )}
- + - - + + - + ) : batchExportConfigForm.destination === 'Snowflake' ? ( <> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + ) : batchExportConfigForm.destination === 'Postgres' ? ( <> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + } /> - + - + - - + + - + ) : batchExportConfigForm.destination === 'Redshift' ? ( <> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + ) : batchExportConfigForm.destination === 'BigQuery' ? ( <> - + - + - + - + - + - + {isNew ? ( - + } /> - + ) : null} - + - - + + - + ) : null}
diff --git a/frontend/src/scenes/batch_exports/BatchExportScene.tsx b/frontend/src/scenes/batch_exports/BatchExportScene.tsx index c3c0bb68228f0..039a47a104354 100644 --- a/frontend/src/scenes/batch_exports/BatchExportScene.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportScene.tsx @@ -125,7 +125,6 @@ export function RunsTab(): JSX.Element { diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx index 088fc2a11a2ea..25a6d6dcaefba 100644 --- a/frontend/src/scenes/billing/BillingProduct.tsx +++ b/frontend/src/scenes/billing/BillingProduct.tsx @@ -536,7 +536,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): } /> -
- + - +
- + {({ value, onChange }) => ( )} - +
{hasAvailableFeature(AvailableFeature.DASHBOARD_COLLABORATION) && (
- + - +
)}
{cohort.is_static ? (
- + {({ onChange }) => ( <> @@ -199,7 +202,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { /> )} - +
) : ( <> diff --git a/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx b/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx index 5929b58873108..4909ab0391453 100644 --- a/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx @@ -1,8 +1,8 @@ import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' @@ -47,7 +47,7 @@ export function DeleteDashboardModal(): JSX.Element { enableFormOnSubmit className="space-y-2" > - @@ -59,7 +59,7 @@ export function DeleteDashboardModal(): JSX.Element { onChange={onChange} /> )} - + ) diff --git a/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx b/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx index 46beee57edbee..a03841992a717 100644 --- a/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx @@ -1,8 +1,8 @@ import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' @@ -55,14 +55,14 @@ export function DuplicateDashboardModal(): JSX.Element { enableFormOnSubmit className="space-y-2" > - {({ value, onChange }) => ( )} - + ) diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss b/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss index b8e3359b89362..a96eade91b6d6 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss @@ -11,13 +11,11 @@ overflow: hidden; &::after { + --bg-light: var(--bg-3000); // Make the fade blend in with the 3000 background smoothly + width: 100%; height: 150px; - .posthog-3000 & { - --bg-light: var(--bg-3000); // Make the fade blend in with the 3000 background smoothly - } - @extend %mixin-gradient-overlay; } } diff --git a/frontend/src/scenes/dashboard/dashboardLogic.tsx b/frontend/src/scenes/dashboard/dashboardLogic.tsx index a956bd2fc0dd5..5c8d9977c4d23 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboardLogic.tsx @@ -1091,6 +1091,7 @@ export const dashboardLogic = kea([ if (values.autoRefresh.enabled) { // Refresh right now after enabling if we haven't refreshed recently if ( + !values.itemsLoading && values.lastRefreshed && values.lastRefreshed.isBefore(now().subtract(values.autoRefresh.interval, 'seconds')) ) { diff --git a/frontend/src/scenes/data-management/database/DatabaseTable.tsx b/frontend/src/scenes/data-management/database/DatabaseTable.tsx index bbf5c67db647e..83b70c8abb41c 100644 --- a/frontend/src/scenes/data-management/database/DatabaseTable.tsx +++ b/frontend/src/scenes/data-management/database/DatabaseTable.tsx @@ -13,7 +13,6 @@ interface DatabaseTableProps { export function DatabaseTable({ table, tables }: DatabaseTableProps): JSX.Element { return ( name === table)?.columns ?? []} columns={[ { diff --git a/frontend/src/scenes/data-management/definition/DefinitionEdit.tsx b/frontend/src/scenes/data-management/definition/DefinitionEdit.tsx index 350724df79410..50dd73bddb3bd 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionEdit.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionEdit.tsx @@ -4,9 +4,9 @@ import { VerifiedDefinitionCheckbox } from 'lib/components/DefinitionPopover/Def import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' import { getFilterLabel, isCoreFilter } from 'lib/taxonomy' @@ -64,14 +64,14 @@ export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element {
{hasTaxonomyFeatures && (
- + - +
)} {showVerifiedCheckbox && (
- + {({ value, onChange }) => ( )} - +
)} {hasTaxonomyFeatures && 'tags' in definition && (
- + {({ value, onChange }) => ( )} - +
)} {isProperty && (
- + {({ value, onChange }) => ( onChange(val)} @@ -115,7 +115,7 @@ export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element { ]} /> )} - +
)}
diff --git a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx index bb635d6e8fd55..f9dd1830383b8 100644 --- a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx +++ b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx @@ -239,7 +239,6 @@ function RenderNestedWarnings(warningSummary: IngestionWarningSummary): JSX.Elem }, ]} embedded - size="small" showHeader={false} /> ) diff --git a/frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx b/frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx index df83d79972b5d..a7a858b9d0858 100644 --- a/frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx +++ b/frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx @@ -1,6 +1,6 @@ import { LemonInput } from '@posthog/lemon-ui' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { ExternalDataSourceType } from '~/types' @@ -21,13 +21,13 @@ export default function SourceForm({ sourceType }: SourceFormProps): JSX.Element enableFormOnSubmit > {SOURCE_DETAILS[sourceType].fields.map((field) => ( - + - + ))} - + - + ) } diff --git a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx index bd00fa492c5ae..a6a3a2a40bcfc 100644 --- a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx +++ b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx @@ -1,6 +1,6 @@ import { LemonInput, LemonSelect } from '@posthog/lemon-ui' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { dataWarehouseTableLogic } from './dataWarehouseTableLogic' @@ -8,7 +8,7 @@ export function DatawarehouseTableForm(): JSX.Element { return (
- + - +
This will be the table name used when writing queries
- + - +
You can use * to select multiple files.
- + - - + + - - + + - +
) diff --git a/frontend/src/scenes/data-warehouse/redirect/DataWarehouseRedirectScene.tsx b/frontend/src/scenes/data-warehouse/redirect/DataWarehouseRedirectScene.tsx index 89b23c525c094..0d7b433cbc7cb 100644 --- a/frontend/src/scenes/data-warehouse/redirect/DataWarehouseRedirectScene.tsx +++ b/frontend/src/scenes/data-warehouse/redirect/DataWarehouseRedirectScene.tsx @@ -1,6 +1,6 @@ import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { sourceFormLogic } from 'scenes/data-warehouse/external/forms/sourceFormLogic' import { SceneExport } from 'scenes/sceneTypes' @@ -20,9 +20,9 @@ export function DataWarehouseRedirectScene(): JSX.Element { className="space-y-4 max-w-100" enableFormOnSubmit > - + - + Submit diff --git a/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx b/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx index def6034aceb44..65b34a32ad3fa 100644 --- a/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx +++ b/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx @@ -8,9 +8,9 @@ import { NotFound } from 'lib/components/NotFound' import { PageHeader } from 'lib/components/PageHeader' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { Field, PureField } from 'lib/forms/Field' import { IconClose, IconFlag, IconHelpOutline } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { personsLogic, PersonsLogicProps } from 'scenes/persons/personsLogic' @@ -187,12 +187,12 @@ export function EarlyAccessFeature({ id }: { id?: string } = {}): JSX.Element { >
{isNewEarlyAccessFeature && ( - + - + )} {'feature_flag' in earlyAccessFeature ? ( - +
-
+ ) : ( - A feature flag will be generated from feature name if not provided} @@ -226,7 +226,7 @@ export function EarlyAccessFeature({ id }: { id?: string } = {}): JSX.Element { )}
)} - + )} {isEditingFeature || isNewEarlyAccessFeature ? ( <> @@ -250,12 +250,12 @@ export function EarlyAccessFeature({ id }: { id?: string } = {}): JSX.Element {
)} {isEditingFeature || isNewEarlyAccessFeature ? ( - + - + ) : (
Description @@ -269,9 +269,9 @@ export function EarlyAccessFeature({ id }: { id?: string } = {}): JSX.Element {
)} {isEditingFeature || isNewEarlyAccessFeature ? ( - + - + ) : (
Documentation URL diff --git a/frontend/src/scenes/events/Events.tsx b/frontend/src/scenes/events/Events.tsx index ae21bad463961..4ae39dc817fa1 100644 --- a/frontend/src/scenes/events/Events.tsx +++ b/frontend/src/scenes/events/Events.tsx @@ -14,7 +14,6 @@ export function Events(): JSX.Element { return ( <> -
diff --git a/frontend/src/scenes/experiments/Experiment.tsx b/frontend/src/scenes/experiments/Experiment.tsx index 7625d2d9484ee..13cb2139e75a2 100644 --- a/frontend/src/scenes/experiments/Experiment.tsx +++ b/frontend/src/scenes/experiments/Experiment.tsx @@ -20,12 +20,12 @@ import { EditableField } from 'lib/components/EditableField/EditableField' import { NotFound } from 'lib/components/NotFound' import { PageHeader } from 'lib/components/PageHeader' import { dayjs } from 'lib/dayjs' -import { Field } from 'lib/forms/Field' import { IconDelete, IconPlusMini, IconWarning } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonCollapse } from 'lib/lemon-ui/LemonCollapse' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonProgress } from 'lib/lemon-ui/LemonProgress' import { Link } from 'lib/lemon-ui/Link' import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' @@ -212,16 +212,16 @@ export function Experiment(): JSX.Element { <>
- + - - + + - - + @@ -234,7 +234,7 @@ export function Experiment(): JSX.Element { className="ph-ignore-input" placeholder="Adding a helpful description can ensure others know what this experiment is about." /> - + {experiment.parameters.feature_flag_variants && (
- + - +
{experimentId === 'new' && @@ -460,7 +460,7 @@ export function Experiment(): JSX.Element {
- + {({ value, onChange }) => (
@@ -483,7 +483,7 @@ export function Experiment(): JSX.Element {
)} -
+
- + - - + + - + {experimentId == 'new' || editingExistingExperiment ? ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 14dc419296a87..ee658db1b2061 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -12,13 +12,13 @@ import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { FEATURE_FLAGS } from 'lib/constants' -import { Field } from 'lib/forms/Field' import { IconDelete, IconLock, IconPlus, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' @@ -275,7 +275,6 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
} /> - {featureFlag.experiment_set && featureFlag.experiment_set?.length > 0 && ( This feature flag is linked to an experiment. Edit settings here only for advanced @@ -285,9 +284,9 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { )} -
+
- Feature flag keys must be unique )} - + - + - + {hasAvailableFeature(AvailableFeature.TAGGING) && ( - + {({ value, onChange }) => { return ( ) }} - + )} - + {({ value, onChange }) => (
)} -
- + + {({ value, onChange }) => (
)} -
+
@@ -876,12 +875,12 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
- + - +
)} @@ -916,7 +915,7 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
- + - +
- + - +
- + {({ value, onChange }) => { return ( ) }} - +
- + {({ value, onChange }) => (
)} - +
{variants.length > 1 && ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx index 07016ce4b4e7a..1954103fcff96 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx @@ -2,8 +2,8 @@ import { LemonButton, LemonDivider, LemonInput, LemonSelect, LemonTag, Link } fr import { useActions, useValues } from 'kea' import { Group } from 'kea-forms' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { Field } from 'lib/forms/Field' import { IconDelete } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { capitalizeFirstLetter, genericOperatorMap, humanFriendlyNumber } from 'lib/utils' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' @@ -87,7 +87,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro
Rollback Condition Type
- + {({ value, onChange }) => ( )} - +
} @@ -111,7 +111,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro {featureFlag.rollback_conditions[index].threshold_type == 'insight' ? (
- + {({ value, onChange }) => ( )} - + trailing 7 day average is  {insightRollingAverages[index] !== undefined ? ( @@ -147,7 +147,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro )} . Trigger when trailing average is - + {({ value, onChange }) => ( )} - - + + - +
) : sentryIntegrationEnabled ? (
@@ -175,7 +175,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro )}  Trigger when there is a - + {({ value, onChange }) => ( )} - - + + {({ value, onChange }) => ( )} - + to{' '} {sentryErrorCount ? ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx index 0df3f02935852..ea12260ab2583 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx @@ -8,13 +8,13 @@ import { allOperatorsToHumanName } from 'lib/components/DefinitionPopover/utils' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { isPropertyFilterWithOperator } from 'lib/components/PropertyFilters/utils' import { FEATURE_FLAGS, INSTANTLY_AVAILABLE_PROPERTIES } from 'lib/constants' -import { Field } from 'lib/forms/Field' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' import { GroupsIntroductionOption } from 'lib/introductions/GroupsIntroductionOption' import { IconCopy, IconDelete, IconErrorOutline, IconOpenInNew, IconPlus, IconSubArrowRight } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonSlider } from 'lib/lemon-ui/LemonSlider' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' @@ -279,7 +279,7 @@ export function FeatureFlagReleaseConditions({ className="ml-1.5 w-20" /> - + %} /> - + {' '} of {aggregationTargetName} in this set.{' '}
diff --git a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx index 4e5ca185d4fed..06037bb23c2ea 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx @@ -343,7 +343,7 @@ export function FlutterSnippet({ flagKey, multivariant, payload }: FeatureFlagSn if (payload) { return ( - {`${clientSuffix}getFeatureFlagPayload('${flagKey}')`} + {`${clientSuffix}getFeatureFlagPayload('${flagKey}');`} ) } diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss index 92510c254be38..a2349cfa463a0 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss +++ b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss @@ -115,13 +115,9 @@ $glyph_height: 23px; // Based on .funnel-step-glyph flex-direction: row; height: 32px; margin: 4px 0; - background-color: var(--border-light); + background-color: var(--border-3000); border-radius: var(--radius); - .posthog-3000 & { - background-color: var(--border-3000); - } - .funnel-bar { position: relative; height: 100%; diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx index 4eb2ebd8c4c99..6d34791efaf1c 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx @@ -1,6 +1,6 @@ import { useActions, useValues } from 'kea' -import { PureField } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { Noun } from '~/models/groupsModel' @@ -16,14 +16,14 @@ export function FunnelsAdvanced({ insightProps }: EditorFilterProps): JSX.Elemen return (
- }> + }> - - + + - + - - + {!!advancedOptionsUsedCount && (
diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx index dcd9f19fffdac..7a0893ca724e0 100644 --- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx +++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx @@ -178,7 +178,6 @@ export function InsightTooltip({ dataSource={dataSource.slice(0, rowCutoff)} columns={columns} rowKey="id" - size="small" uppercaseHeader={false} rowRibbonColor={hideColorCol ? undefined : (datum) => datum.color || null} showHeader={showHeader} @@ -239,7 +238,6 @@ export function InsightTooltip({ dataSource={dataSource.slice(0, rowCutoff)} columns={columns} rowKey="id" - size="small" className="ph-no-capture" uppercaseHeader={false} rowRibbonColor={hideColorCol ? undefined : (datum: SeriesDatum) => datum.color || null} diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index b249415792a49..50cc8fc63ec8c 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -263,7 +263,7 @@ export function formatBreakdownLabel( return cohorts?.filter((c) => c.id == breakdown_value)[0]?.name ?? (breakdown_value || '').toString() } else if (typeof breakdown_value == 'number') { return isOtherBreakdown(breakdown_value) - ? 'Other' + ? 'Other (Groups all remaining values)' : isNullBreakdown(breakdown_value) ? 'None' : formatPropertyValueForDisplay @@ -271,7 +271,7 @@ export function formatBreakdownLabel( : String(breakdown_value) } else if (typeof breakdown_value == 'string') { return isOtherBreakdown(breakdown_value) || breakdown_value === 'nan' - ? 'Other' + ? 'Other (Groups all remaining values)' : isNullBreakdown(breakdown_value) || breakdown_value === '' ? 'None' : breakdown_value diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 1940085873012..0dc78e6eaaf47 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -10,7 +10,6 @@ import { AvailableFeature, ProductKey } from '~/types' import { OnboardingBillingStep } from './OnboardingBillingStep' import { OnboardingInviteTeammates } from './OnboardingInviteTeammates' import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' -import { OnboardingOtherProductsStep } from './OnboardingOtherProductsStep' import { OnboardingProductConfiguration } from './OnboardingProductConfiguration' import { ProductConfigOption } from './onboardingProductConfigurationLogic' import { OnboardingVerificationStep } from './OnboardingVerificationStep' @@ -29,7 +28,7 @@ export const scene: SceneExport = { * Wrapper for custom onboarding content. This automatically includes billing, other products, and invite steps. */ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => { - const { currentOnboardingStep, shouldShowBillingStep, shouldShowOtherProductsStep } = useValues(onboardingLogic) + const { currentOnboardingStep, shouldShowBillingStep } = useValues(onboardingLogic) const { setAllOnboardingSteps } = useActions(onboardingLogic) const { product } = useValues(onboardingLogic) const [allSteps, setAllSteps] = useState([]) @@ -60,10 +59,6 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele const BillingStep = steps = [...steps, BillingStep] } - if (shouldShowOtherProductsStep) { - const OtherProductsStep = - steps = [...steps, OtherProductsStep] - } const inviteTeammatesStep = steps = [...steps, inviteTeammatesStep] setAllSteps(steps) diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx deleted file mode 100644 index caac1d4f6cc0a..0000000000000 --- a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useActions, useValues } from 'kea' -import { useWindowSize } from 'lib/hooks/useWindowSize' -import { ProductCard } from 'scenes/products/Products' - -import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' -import { OnboardingStep } from './OnboardingStep' - -export const OnboardingOtherProductsStep = ({ - stepKey = OnboardingStepKey.OTHER_PRODUCTS, -}: { - stepKey?: OnboardingStepKey -}): JSX.Element => { - const { product, suggestedProducts } = useValues(onboardingLogic) - const { completeOnboarding } = useActions(onboardingLogic) - const { width } = useWindowSize() - const horizontalCard = width && width >= 640 - - return ( - } - stepKey={stepKey} - > -
- {suggestedProducts?.map((suggestedProduct) => ( - completeOnboarding(suggestedProduct.type)} - orientation={horizontalCard ? 'horizontal' : 'vertical'} - className="w-full" - /> - ))} -
-
- ) -} diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index f6d46eee16751..d714491a5fc48 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -143,22 +143,6 @@ export const onboardingLogic = kea([ return !product?.subscribed || !hasAllAddons || subscribedDuringOnboarding }, ], - shouldShowOtherProductsStep: [ - (s) => [s.suggestedProducts, s.isFirstProductOnboarding], - (suggestedProducts: BillingProductV2Type[], isFirstProductOnboarding: boolean) => - suggestedProducts.length > 0 && isFirstProductOnboarding, - ], - suggestedProducts: [ - (s) => [s.billing, s.product, s.currentTeam], - (billing, product, currentTeam) => - billing?.products?.filter( - (p) => - p.type !== product?.type && - !p.contact_support && - !p.inclusion_only && - !currentTeam?.has_completed_onboarding_for?.[p.type] - ) || [], - ], isStepKeyInvalid: [ (s) => [s.stepKey, s.allOnboardingSteps, s.currentOnboardingStep], (stepKey: string, allOnboardingSteps: AllOnboardingSteps, currentOnboardingStep: React.ReactNode | null) => diff --git a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx index 24c1ab43ec1a1..300d5825846d6 100644 --- a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx +++ b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx @@ -5,9 +5,9 @@ import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' import { BridgePage } from 'lib/components/BridgePage/BridgePage' import SignupReferralSource from 'lib/components/SignupReferralSource' import SignupRoleSelect from 'lib/components/SignupRoleSelect' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { organizationLogic } from 'scenes/organizationLogic' import { SceneExport } from 'scenes/sceneTypes' @@ -62,21 +62,21 @@ export function ConfirmOrganization(): JSX.Element { enableFormOnSubmit className="space-y-4" > - + - + - + - + - - + diff --git a/frontend/src/scenes/organization/CreateOrganizationModal.tsx b/frontend/src/scenes/organization/CreateOrganizationModal.tsx index 68375eda4308b..f0aa3482f534f 100644 --- a/frontend/src/scenes/organization/CreateOrganizationModal.tsx +++ b/frontend/src/scenes/organization/CreateOrganizationModal.tsx @@ -1,6 +1,6 @@ import { LemonButton, LemonInput, LemonModal, Link } from '@posthog/lemon-ui' import { useActions } from 'kea' -import { PureField } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { useState } from 'react' import { organizationLogic } from 'scenes/organizationLogic' @@ -63,7 +63,7 @@ export function CreateOrganizationModal({ isOpen={isVisible} inline={inline} > - + - + ) } diff --git a/frontend/src/scenes/pipeline/Destinations.tsx b/frontend/src/scenes/pipeline/Destinations.tsx index 9f251fe942d1e..149f8a2cc60fb 100644 --- a/frontend/src/scenes/pipeline/Destinations.tsx +++ b/frontend/src/scenes/pipeline/Destinations.tsx @@ -52,7 +52,7 @@ function DestinationsTable(): JSX.Element { <> @@ -99,7 +99,7 @@ function PluginConfigurationFields({ showOptional={!requiredFields.includes(fieldConfig.key)} > - + ) : ( <> {fieldConfig.type ? ( diff --git a/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx b/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx index 6c2d5e5374f2a..cdb72aecf8d29 100644 --- a/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx +++ b/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx @@ -56,7 +56,6 @@ export function PipelineNodeLogs({ id, stage }: PipelineNodeLogicProps): JSX.Ele dataSource={logs} columns={columns} loading={logsLoading} - size="small" className="ph-no-capture" rowKey="timestamp" pagination={{ pageSize: 200, hideOnSinglePage: true }} diff --git a/frontend/src/scenes/pipeline/Transformations.tsx b/frontend/src/scenes/pipeline/Transformations.tsx index 9194166f4d161..cee7f5dced7e8 100644 --- a/frontend/src/scenes/pipeline/Transformations.tsx +++ b/frontend/src/scenes/pipeline/Transformations.tsx @@ -84,7 +84,7 @@ export function Transformations(): JSX.Element { )} 0 ? (
{shownFields.map(([key, options]) => ( - + {(props) => } - + ))}
) : null} diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index 9a5f7e1b876be..7b531e7cbfdda 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -7,8 +7,8 @@ import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { CodeEditor } from 'lib/components/CodeEditors' import { Drawer } from 'lib/components/Drawer' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { useEffect } from 'react' import { createDefaultPluginSource } from 'scenes/plugins/source/createDefaultPluginSource' import { pluginSourceLogic } from 'scenes/plugins/source/pluginSourceLogic' @@ -112,7 +112,7 @@ export function PluginSource({ ) : ( <> - + {({ value, onChange }) => ( <> )} - + )} diff --git a/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx b/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx index cd332a54143f3..a2ebd3a5a8e2b 100644 --- a/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx +++ b/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx @@ -3,7 +3,6 @@ import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { beforeUnload } from 'kea-router' import api from 'lib/api' -import { FormErrors } from 'lib/forms/Errors' import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { validateJson } from 'lib/utils' import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' @@ -153,7 +152,6 @@ export const pluginSourceLogic = kea([ <>
Please fix the following errors:
{String(error?.message || error)}
- , { position: 'top-right' } ) diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.scss b/frontend/src/scenes/project-homepage/ProjectHomepage.scss index 90d80d46c02ea..759f253b69aa6 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.scss +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.scss @@ -20,13 +20,11 @@ } } - .posthog-3000 & { - a { - color: var(--default); + a { + color: var(--default); - &:hover { - color: var(--primary-3000); - } + &:hover { + color: var(--primary-3000); } } } diff --git a/frontend/src/scenes/project/CreateProjectModal.tsx b/frontend/src/scenes/project/CreateProjectModal.tsx index 6739952f31b04..b1212bf53aeea 100644 --- a/frontend/src/scenes/project/CreateProjectModal.tsx +++ b/frontend/src/scenes/project/CreateProjectModal.tsx @@ -1,6 +1,6 @@ import { LemonButton, LemonInput, LemonModal, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { PureField } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { useEffect, useState } from 'react' import { teamLogic } from 'scenes/teamLogic' @@ -90,7 +90,7 @@ export function CreateProjectModal({ inline={inline} closable={!currentTeamLoading} > - + - + ) } diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrame.scss b/frontend/src/scenes/session-recordings/player/PlayerFrame.scss index 924b067190978..9eb6bad67b2ba 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrame.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerFrame.scss @@ -8,11 +8,7 @@ width: 100%; height: 100%; overflow: hidden; - background-color: var(--bg-charcoal); - - .posthog-3000 & { - background-color: var(--bg-3000-dark); - } + background-color: var(--bg-3000-dark); .PlayerFrame__content { position: absolute; diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss b/frontend/src/scenes/session-recordings/player/PlayerMeta.scss index d572eda28955c..a2a89a0af5103 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerMeta.scss @@ -81,12 +81,10 @@ } .Link { - .posthog-3000 & { - color: var(--default); + color: var(--default); - &:hover { - color: var(--primary-3000); - } + &:hover { + color: var(--primary-3000); } } } diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss index 0a5e430bf08e4..362eb9aceac3a 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss @@ -7,12 +7,8 @@ height: 0.5rem; margin-top: 0.25rem; pointer-events: none; - background-color: var(--primary); + background-color: var(--primary-3000); border-radius: var(--radius) 0 0 var(--radius); transition: transform 200ms linear; will-change: transform; - - .posthog-3000 & { - background-color: var(--primary-3000); - } } diff --git a/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx b/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx index 1d19209eabcfc..fef33b2967cae 100644 --- a/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx +++ b/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx @@ -1,9 +1,9 @@ import { LemonCheckbox, LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { IconOpenInNew, IconPlus, IconWithCount } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { Popover } from 'lib/lemon-ui/Popover' @@ -46,9 +46,9 @@ export function PlaylistPopoverButton(props: LemonButtonProps): JSX.Element { enableFormOnSubmit className="space-y-1" > - + - +
- + - - + + - +
diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss index f0a6e629424ea..d2fcab212dbcb 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss @@ -22,9 +22,7 @@ overflow: hidden; .text-link { - .posthog-3000 & { - color: var(--default); - } + color: var(--default); } } @@ -56,12 +54,8 @@ .SessionRecordingPlaylistHeightWrapper { // NOTE: Somewhat random way to offset the various headers and tabs above the playlist - height: calc(100vh - 15rem); + height: calc(100vh - 9rem); min-height: 41rem; - - .posthog-3000 & { - height: calc(100vh - 9rem); - } } .SessionRecordingPreview { diff --git a/frontend/src/scenes/settings/Settings.scss b/frontend/src/scenes/settings/Settings.scss index 37b8f3daf2e94..b749083d2ca68 100644 --- a/frontend/src/scenes/settings/Settings.scss +++ b/frontend/src/scenes/settings/Settings.scss @@ -2,20 +2,16 @@ display: flex; gap: 2rem; align-items: start; - margin-top: 1rem; + margin-top: 0; .Settings__sections { position: sticky; - top: 0.5rem; + top: 4rem; flex-shrink: 0; width: 20%; min-width: 14rem; max-width: 20rem; - .posthog-3000 & { - top: 4rem; - } - .SidePanel3000 & { top: 0; } @@ -33,7 +29,6 @@ } } - .posthog-3000 &, .LemonModal & { margin-top: 0; } diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx index 770b125d6f861..651e1a3aa2585 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx @@ -2,9 +2,9 @@ import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { Field } from 'lib/forms/Field' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' @@ -37,28 +37,28 @@ export function ConfigureSAMLModal(): JSX.Element { Read the docs

- + {`${siteUrl}/complete/saml/`} - - + + {configureSAMLModalId || 'unknown'} - - + + {siteUrl} - - + + - - + + - - + + - + {!samlReady && ( SAML will not be enabled unless you enter all attributes above. However you can still diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx index bf38e5d9133ad..6b502f4c402bc 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { PureField } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' @@ -40,14 +40,14 @@ export function VerifyDomainModal(): JSX.Element {
  • Add the following TXT record.
    - +
    {challengeName}
    -
    + - +
    {domainBeingVerified?.verification_challenge} @@ -59,13 +59,13 @@ export function VerifyDomainModal(): JSX.Element { /> )}
    - - + +
    Default or 3600
    -
    +
  • Press verify below.
  • diff --git a/frontend/src/scenes/settings/project/AddMembersModal.tsx b/frontend/src/scenes/settings/project/AddMembersModal.tsx index 51febcc209c0f..b94bae43d7b9b 100644 --- a/frontend/src/scenes/settings/project/AddMembersModal.tsx +++ b/frontend/src/scenes/settings/project/AddMembersModal.tsx @@ -4,8 +4,8 @@ import { Form } from 'kea-forms' import { RestrictedComponentProps } from 'lib/components/RestrictedArea' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { TeamMembershipLevel } from 'lib/constants' -import { Field } from 'lib/forms/Field' import { IconPlus } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { membershipLevelToName, teamMembershipLevelIntegers } from 'lib/utils/permissioning' import { useState } from 'react' @@ -42,7 +42,7 @@ export function AddMembersModalWithButton({ isRestricted }: RestrictedComponentP

    {`Adding members${currentTeam?.name ? ` to project ${currentTeam.name}` : ''}`}

    - + - - + + ) )} /> - + diff --git a/frontend/src/scenes/settings/user/ChangePassword.tsx b/frontend/src/scenes/settings/user/ChangePassword.tsx index 1e284f1696316..42549b954407c 100644 --- a/frontend/src/scenes/settings/user/ChangePassword.tsx +++ b/frontend/src/scenes/settings/user/ChangePassword.tsx @@ -2,7 +2,7 @@ import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' import PasswordStrength from 'lib/components/PasswordStrength' -import { Field } from 'lib/forms/Field' +import { LemonField } from 'lib/lemon-ui/LemonField' import { changePasswordLogic } from './changePasswordLogic' @@ -11,16 +11,16 @@ export function ChangePassword(): JSX.Element { return (
    - + - + - @@ -37,7 +37,7 @@ export function ChangePassword(): JSX.Element { className="ph-ignore-input" placeholder="••••••••••" /> - + Change password diff --git a/frontend/src/scenes/settings/user/UserDetails.tsx b/frontend/src/scenes/settings/user/UserDetails.tsx index 0ad4587948d61..587b54161b91d 100644 --- a/frontend/src/scenes/settings/user/UserDetails.tsx +++ b/frontend/src/scenes/settings/user/UserDetails.tsx @@ -1,8 +1,8 @@ import { LemonTag } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { userLogic } from 'scenes/userLogic' @@ -19,23 +19,23 @@ export function UserDetails(): JSX.Element { maxWidth: '28rem', }} > - + - + - + - + {user?.pending_email && Pending verification for {user.pending_email}}
    - + - - + + - + { @@ -82,7 +82,7 @@ export default function SurveyEdit(): JSX.Element { key: SurveyEditSection.Presentation, header: 'Presentation', content: ( - + {({ onChange, value }) => { return (
    @@ -146,7 +146,7 @@ export default function SurveyEdit(): JSX.Element {
    ) }} -
    + ), }, { @@ -227,7 +227,7 @@ export default function SurveyEdit(): JSX.Element { ), content: ( <> - + - - + @@ -266,8 +266,8 @@ export default function SurveyEdit(): JSX.Element { } textPlaceholder="ex: We really appreciate it." /> - - + + - + ), }, @@ -348,7 +348,7 @@ export default function SurveyEdit(): JSX.Element { key: SurveyEditSection.Customization, header: 'Customization', content: ( - + {({ value, onChange }) => ( <> {survey.type === SurveyType.Widget && ( @@ -375,7 +375,7 @@ export default function SurveyEdit(): JSX.Element { /> )} - + ), }, ] @@ -384,7 +384,7 @@ export default function SurveyEdit(): JSX.Element { key: SurveyEditSection.Targeting, header: 'Targeting', content: ( - + { if (value) { @@ -408,7 +408,7 @@ export default function SurveyEdit(): JSX.Element { ) : ( <> - )} - - + + {({ value, onChange }) => ( <> -
    - - + + @@ -480,8 +480,8 @@ export default function SurveyEdit(): JSX.Element { } placeholder="ex: .className or #id" /> - - + +
    {' '} days.
    -
    + )} - - + + )} - + )} - + ), }, ]} diff --git a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx index d3420e1a81749..bb6482e18598b 100644 --- a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx +++ b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx @@ -6,8 +6,8 @@ import { CSS } from '@dnd-kit/utilities' import { LemonButton, LemonCheckbox, LemonInput, LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Group } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { IconDelete, IconPlusMini, SortableDragIcon } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' import { Survey, SurveyQuestionType } from '~/types' @@ -88,7 +88,7 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu return (
    - + { @@ -216,11 +216,11 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu ], ]} /> - - + + - - + + {({ value, onChange }) => ( )} - + {survey.questions.length > 1 && ( - + - + )} {question.type === SurveyQuestionType.Link && ( - + - + )} {question.type === SurveyQuestionType.Rating && (
    - + - - + + - +
    - + - - + + - +
    )} {(question.type === SurveyQuestionType.SingleChoice || question.type === SurveyQuestionType.MultipleChoice) && (
    - + {({ value: hasOpenChoice, onChange: toggleHasOpenChoice }) => ( - + {({ value, onChange }) => (
    {(value || []).map((choice: string, index: number) => { @@ -366,18 +366,18 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu
    )} - + )} - +
    )} - + - +
    ) diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index fea26dcf8fa8e..7a6dcd00332c2 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -402,7 +402,7 @@ export const webAnalyticsLogic = kea([ date_from: dateFrom, date_to: dateTo, } - const compare = !!(dateRange.date_from && dateRange.date_to) + const compare = !!dateRange.date_from && dateRange.date_from !== 'all' const sampling = { enabled: !!values.featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_SAMPLING], @@ -428,6 +428,7 @@ export const webAnalyticsLogic = kea([ properties: webAnalyticsFilters, dateRange, sampling, + compare, }, insightProps: createInsightProps(TileId.OVERVIEW), canOpenModal: false, diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index 2140b3aad166d..80ad3b4e660e5 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -53,16 +53,12 @@ input[type='radio'] { gap: 0.5rem 1rem; align-items: center; min-height: 2.5rem; - margin: 1.25rem 0 0.25rem; + margin: 1rem 0 0.25rem; .ant-form-item { margin-bottom: 0 !important; } - .posthog-3000 & { - margin-top: 1rem; - } - @include screen($md) { flex-wrap: nowrap; } @@ -177,14 +173,10 @@ input::-ms-clear { font-family: var(--font-sans); font-size: 1rem; cursor: unset; - border: 1px solid var(--border); + border: 1px solid var(--secondary-3000-button-border); border-radius: var(--radius); box-shadow: var(--shadow-elevation); opacity: 1 !important; - - .posthog-3000 & { - border-color: var(--secondary-3000-button-border); - } } .Toastify__toast-container { @@ -481,6 +473,9 @@ body { --link: var(--primary-3000); --tooltip-bg: var(--bg-charcoal); --data-color-1-hover: #1d4affe5; + --shadow-elevation: var(--shadow-elevation-3000); + --primary: var(--primary-3000); + --primary-highlight: var(--primary-3000-highlight); // Remove below once we're using Tailwind's base --tw-ring-offset-width: 0px; @@ -496,69 +491,50 @@ body { --tw-scale-y: 1; touch-action: manipulation; // Disable double-tap-to-zoom on mobile, making taps slightly snappier + background: var(--bg-3000); &.posthog-3000[theme='light'] { @include light-mode-3000-variables; } &.posthog-3000[theme='dark'] { - @include dark-mode-3000-variables; - } - - &.posthog-3000 { - --shadow-elevation: var(--shadow-elevation-3000); - --primary: var(--primary-3000); - --primary-highlight: var(--primary-3000-highlight); - - background: var(--bg-3000); + .ant-empty-img-simple-path { + fill: var(--border-3000); + } - .non-3000 { - // Helper to hide non-3000 elements without JS - display: none; + .ant-empty-img-simple-ellipse { + fill: var(--border-3000); } - .LemonButton, - .Link { - .text-link { - color: var(--text-3000); - } + @include dark-mode-3000-variables; + } - &:hover { - .text-link { - color: var(--primary-3000); - } - } + * > { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; } - * > { - ::-webkit-scrollbar { - width: 0.5rem; - height: 0.5rem; - } - - ::-webkit-scrollbar-track { - background: var(--accent-3000); - } + ::-webkit-scrollbar-track { + background: var(--accent-3000); + } - ::-webkit-scrollbar-thumb { - background: var(--trace-3000); - border-radius: var(--radius); + ::-webkit-scrollbar-thumb { + background: var(--trace-3000); + border-radius: var(--radius); - &:hover { - background: var(--muted-3000); - } + &:hover { + background: var(--muted-3000); } } + } - h1, - h2, - h3, - h4, - h5 { - font-family: var(--font-title); - } - - @include posthog-3000-variables; + h1, + h2, + h3, + h4, + h5 { + font-family: var(--font-title); } h1, @@ -589,6 +565,19 @@ body { color: var(--link); } + .LemonButton, + .Link { + .text-link { + color: var(--text-3000); + } + + &:hover { + .text-link { + color: var(--primary-3000); + } + } + } + // AntD uses its own border color for the bottom of tab lists, but we want to use `var(--border)` .ant-tabs-top, .ant-tabs-bottom { @@ -707,6 +696,8 @@ body { .ant-table-tbody > tr.ant-table-placeholder:hover > td { background: inherit; } + + @include posthog-3000-variables; } .storybook-test-runner { @@ -721,7 +712,7 @@ body { } // Hide some parts of the UI that were causing flakiness - ::-webkit-scrollbar, // Scrollbar in WebKit/Blink browsers + ::-webkit-scrollbar, * > ::-webkit-scrollbar, // Scrollbar in WebKit/Blink browsers .LemonTabs__bar::after, // Active tab slider .scrollable::after, // Scrollability indicators .scrollable::before { @@ -729,21 +720,9 @@ body { } } -.posthog-3000 { - .ant-radio-button-wrapper { - background: var(--secondary-3000); - border-color: transparent; - } -} - -.posthog-3000[theme='dark'] { - .ant-empty-img-simple-path { - fill: var(--border-3000); - } - - .ant-empty-img-simple-ellipse { - fill: var(--border-3000); - } +.ant-radio-button-wrapper { + background: var(--secondary-3000); + border-color: transparent; } .ligatures-none { diff --git a/frontend/src/toolbar/elements/Elements.tsx b/frontend/src/toolbar/elements/Elements.tsx index 904cd148e8ab8..0e8c0a278d67f 100644 --- a/frontend/src/toolbar/elements/Elements.tsx +++ b/frontend/src/toolbar/elements/Elements.tsx @@ -2,7 +2,7 @@ import './Elements.scss' import { useActions, useValues } from 'kea' import { compactNumber } from 'lib/utils' -import React from 'react' +import { Fragment } from 'react' import { ElementInfoWindow } from '~/toolbar/elements/ElementInfoWindow' import { elementsLogic } from '~/toolbar/elements/elementsLogic' @@ -75,7 +75,7 @@ export function Elements(): JSX.Element { {heatmapElements.map(({ rect, count, clickCount, rageclickCount, element }, index) => { return ( - + )} - + ) })} diff --git a/frontend/src/toolbar/utils.ts b/frontend/src/toolbar/utils.ts index 8bb0562c04be8..368e569bb84b5 100644 --- a/frontend/src/toolbar/utils.ts +++ b/frontend/src/toolbar/utils.ts @@ -2,7 +2,6 @@ import { finder } from '@medv/finder' import { CLICK_TARGET_SELECTOR, CLICK_TARGETS, escapeRegex, TAGS_TO_IGNORE } from 'lib/actionUtils' import { cssEscape } from 'lib/utils/cssEscape' import { querySelectorAllDeep } from 'query-selector-shadow-dom' -import wildcardMatch from 'wildcard-match' import { ActionStepForm, BoxColor, ElementRect } from '~/toolbar/types' import { ActionStepType, StringMatching } from '~/types' @@ -43,9 +42,13 @@ export function elementToQuery(element: HTMLElement, dataAttributes: string[]): try { return finder(element, { - attr: (name) => dataAttributes.some((dataAttribute) => wildcardMatch(dataAttribute)(name)), tagName: (name) => !TAGS_TO_IGNORE.includes(name), seedMinLength: 5, // include several selectors e.g. prefer .project-homepage > .project-header > .project-title over .project-title + attr: (name) => { + // preference to data attributes if they exist + // that aren't in the PostHog preferred list - they were returned early above + return name.startsWith('data-') + }, }) } catch (error) { console.warn('Error while trying to find a selector for element', element, error) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index a2680b59011b3..e64ff9ef15c81 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -364,11 +364,10 @@ posthog/hogql/query.py:0: error: Incompatible types in assignment (expression ha posthog/hogql/query.py:0: error: Argument 1 to "get_default_limit_for_context" has incompatible type "LimitContext | None"; expected "LimitContext" [arg-type] posthog/hogql/query.py:0: error: "SelectQuery" has no attribute "select_queries" [attr-defined] posthog/hogql/query.py:0: error: Subclass of "SelectQuery" and "SelectUnionQuery" cannot exist: would have incompatible method signatures [unreachable] +posthog/hogql/autocomplete.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/hogql/autocomplete.py:0: error: Unused "type: ignore" comment [unused-ignore] posthog/hogql_queries/insights/trends/breakdown_values.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] posthog/hogql_queries/insights/trends/breakdown_values.py:0: error: Value of type "list[Any] | None" is not indexable [index] -posthog/hogql_queries/insights/funnels/base.py:0: error: Incompatible types in assignment (expression has type "FunnelExclusionEventsNode | FunnelExclusionActionsNode", variable has type "EventsNode | ActionsNode") [assignment] -posthog/hogql_queries/insights/funnels/base.py:0: error: Item "EventsNode" of "EventsNode | ActionsNode" has no attribute "funnelFromStep" [union-attr] -posthog/hogql_queries/insights/funnels/base.py:0: error: Item "ActionsNode" of "EventsNode | ActionsNode" has no attribute "funnelFromStep" [union-attr] posthog/hogql_queries/sessions_timeline_query_runner.py:0: error: Statement is unreachable [unreachable] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_histogram_bin_count" [union-attr] @@ -403,6 +402,9 @@ posthog/hogql_queries/events_query_runner.py:0: error: Statement is unreachable posthog/hogql/metadata.py:0: error: Argument "metadata_source" to "translate_hogql" has incompatible type "SelectQuery | SelectUnionQuery"; expected "SelectQuery | None" [arg-type] posthog/hogql/metadata.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] posthog/queries/breakdown_props.py:0: error: Argument 1 to "translate_hogql" has incompatible type "str | int"; expected "str" [arg-type] +posthog/hogql_queries/insights/funnels/base.py:0: error: Incompatible types in assignment (expression has type "FunnelExclusionEventsNode | FunnelExclusionActionsNode", variable has type "EventsNode | ActionsNode") [assignment] +posthog/hogql_queries/insights/funnels/base.py:0: error: Item "EventsNode" of "EventsNode | ActionsNode" has no attribute "funnelFromStep" [union-attr] +posthog/hogql_queries/insights/funnels/base.py:0: error: Item "ActionsNode" of "EventsNode | ActionsNode" has no attribute "funnelFromStep" [union-attr] posthog/queries/funnels/base.py:0: error: "HogQLContext" has no attribute "person_on_events_mode" [attr-defined] posthog/queries/funnels/base.py:0: error: Argument 1 to "translate_hogql" has incompatible type "str | int"; expected "str" [arg-type] ee/clickhouse/queries/funnels/funnel_correlation.py:0: error: Statement is unreachable [unreachable] diff --git a/package.json b/package.json index 156d5116aeb27..62d4f9f2d41e8 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@dnd-kit/utilities": "^3.2.1", "@floating-ui/react": "^0.16.0", "@lottiefiles/react-lottie-player": "^3.4.7", - "@medv/finder": "^2.1.0", + "@medv/finder": "^3.1.0", "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.4.6", "@posthog/icons": "0.5.1", @@ -173,7 +173,6 @@ "tailwindcss": "^3.4.0", "use-debounce": "^9.0.3", "use-resize-observer": "^8.0.0", - "wildcard-match": "^5.1.2", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5d76b16d0a40..180a3a515a9d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ dependencies: specifier: ^3.4.7 version: 3.4.7(react@18.2.0) '@medv/finder': - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^3.1.0 + version: 3.1.0 '@microlink/react-json-view': specifier: ^1.21.3 version: 1.22.2(@types/react@17.0.52)(react-dom@18.2.0)(react@18.2.0) @@ -340,9 +340,6 @@ dependencies: use-resize-observer: specifier: ^8.0.0 version: 8.0.0(react-dom@18.2.0)(react@18.2.0) - wildcard-match: - specifier: ^5.1.2 - version: 5.1.2 zxcvbn: specifier: ^4.4.2 version: 4.4.2 @@ -4921,8 +4918,8 @@ packages: react: 18.2.0 dev: true - /@medv/finder@2.1.0: - resolution: {integrity: sha512-Egrg5XO4kLol24b1Kv50HDfi5hW0yQ6aWSsO0Hea1eJ4rogKElIN0M86FdVnGF4XIGYyA7QWx0MgbOzVPA0qkA==} + /@medv/finder@3.1.0: + resolution: {integrity: sha512-ojkXjR3K0Zz3jnCR80tqPL+0yvbZk/lEodb6RIVjLz7W8RVA2wrw8ym/CzCpXO9SYVUIKHFUpc7jvf8UKfIM3w==} dev: false /@microlink/react-json-view@1.22.2(@types/react@17.0.52)(react-dom@18.2.0)(react@18.2.0): @@ -21383,10 +21380,6 @@ packages: isexe: 2.0.0 dev: true - /wildcard-match@5.1.2: - resolution: {integrity: sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==} - dev: false - /wildcard@2.0.0: resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} dev: true diff --git a/posthog/api/insight.py b/posthog/api/insight.py index 7c15373d5c456..8f4c6f9963a12 100644 --- a/posthog/api/insight.py +++ b/posthog/api/insight.py @@ -574,6 +574,7 @@ class InsightViewSet( filter_backends = [DjangoFilterBackend] filterset_fields = ["short_id", "created_by"] include_in_docs = True + sharing_enabled_actions = ["retrieve", "list"] retention_query_class = Retention stickiness_query_class = Stickiness @@ -588,23 +589,11 @@ def get_serializer_class(self) -> Type[serializers.BaseSerializer]: return InsightBasicSerializer return super().get_serializer_class() - def get_authenticators(self): - return [SharingAccessTokenAuthentication(), *super().get_authenticators()] - def get_serializer_context(self) -> Dict[str, Any]: context = super().get_serializer_context() context["is_shared"] = isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication) return context - def get_permissions(self): - if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication) and self.action in ( - "retrieve", - "list", - ): - # Anonymous users authenticated via SharingAccessTokenAuthentication get read-only access to insights - return [] - return super().get_permissions() - def get_queryset(self) -> QuerySet: queryset: QuerySet if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): diff --git a/posthog/api/routing.py b/posthog/api/routing.py index 2a00ea53fcfa4..35b9f10aeb352 100644 --- a/posthog/api/routing.py +++ b/posthog/api/routing.py @@ -9,11 +9,11 @@ from rest_framework_extensions.settings import extensions_api_settings from posthog.api.utils import get_token -from posthog.auth import JwtAuthentication, PersonalAPIKeyAuthentication +from posthog.auth import JwtAuthentication, PersonalAPIKeyAuthentication, SharingAccessTokenAuthentication from posthog.models.organization import Organization from posthog.models.team import Team from posthog.models.user import User -from posthog.permissions import OrganizationMemberPermissions, TeamMemberAccessPermission +from posthog.permissions import OrganizationMemberPermissions, SharingTokenPermission, TeamMemberAccessPermission from posthog.user_permissions import UserPermissions if TYPE_CHECKING: @@ -45,9 +45,14 @@ class TeamAndOrgViewSetMixin(_GenericViewSet): authentication_classes = [] permission_classes = [] + sharing_enabled_actions: list[str] = [] + # We want to try and ensure that the base permission and authentication are always used # so we offer a way to add additional classes def get_permissions(self): + if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): + return [SharingTokenPermission()] + # NOTE: We define these here to make it hard _not_ to use them. If you want to override them, you have to # override the entire method. permission_classes: list = [IsAuthenticated] @@ -64,11 +69,19 @@ def get_authenticators(self): # NOTE: Custom authentication_classes go first as these typically have extra initial checks authentication_classes: list = [ *self.authentication_classes, - JwtAuthentication, - PersonalAPIKeyAuthentication, - authentication.SessionAuthentication, ] + if self.sharing_enabled_actions: + authentication_classes.append(SharingAccessTokenAuthentication) + + authentication_classes.extend( + [ + JwtAuthentication, + PersonalAPIKeyAuthentication, + authentication.SessionAuthentication, + ] + ) + return [auth() for auth in authentication_classes] def get_queryset(self): diff --git a/posthog/api/test/dashboards/test_dashboard.py b/posthog/api/test/dashboards/test_dashboard.py index 45aebf355776b..4165f6b680687 100644 --- a/posthog/api/test/dashboards/test_dashboard.py +++ b/posthog/api/test/dashboards/test_dashboard.py @@ -894,17 +894,18 @@ def test_dashboard_duplication_can_duplicate_tiles_without_editing_name_if_there def test_dashboard_duplication(self): existing_dashboard = Dashboard.objects.create(team=self.team, name="existing dashboard", created_by=self.user) insight1 = Insight.objects.create(filters={"name": "test1"}, team=self.team, last_refresh=now()) - DashboardTile.objects.create(dashboard=existing_dashboard, insight=insight1) + tile1 = DashboardTile.objects.create(dashboard=existing_dashboard, insight=insight1) insight2 = Insight.objects.create(filters={"name": "test2"}, team=self.team, last_refresh=now()) - DashboardTile.objects.create(dashboard=existing_dashboard, insight=insight2) + tile2 = DashboardTile.objects.create(dashboard=existing_dashboard, insight=insight2) _, response = self.dashboard_api.create_dashboard({"name": "another", "use_dashboard": existing_dashboard.pk}) self.assertEqual(response["creation_mode"], "duplicate") self.assertEqual(len(response["tiles"]), len(existing_dashboard.insights.all())) - existing_dashboard_item_id_set = set(map(lambda x: x.id, existing_dashboard.insights.all())) + existing_dashboard_item_id_set = {tile1.pk, tile2.pk} response_item_id_set = set(map(lambda x: x.get("id", None), response["tiles"])) # check both sets are disjoint to verify that the new items' ids are different than the existing items + self.assertTrue(existing_dashboard_item_id_set.isdisjoint(response_item_id_set)) for item in response["tiles"]: diff --git a/posthog/hogql/autocomplete.py b/posthog/hogql/autocomplete.py index 5cb7c17fa5e52..c1152a1a60cab 100644 --- a/posthog/hogql/autocomplete.py +++ b/posthog/hogql/autocomplete.py @@ -1,4 +1,4 @@ -from copy import copy +from copy import copy, deepcopy from typing import Callable, Dict, List, Optional, cast from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import create_hogql_database @@ -82,7 +82,7 @@ def constant_type_to_database_field(constant_type: ConstantType, name: str) -> D return DatabaseField(name=name) -def convert_field_or_table_to_type_string(field_or_table: FieldOrTable) -> str: +def convert_field_or_table_to_type_string(field_or_table: FieldOrTable) -> str | None: if isinstance(field_or_table, ast.BooleanDatabaseField): return "Boolean" if isinstance(field_or_table, ast.IntegerDatabaseField): @@ -97,10 +97,12 @@ def convert_field_or_table_to_type_string(field_or_table: FieldOrTable) -> str: return "Date" if isinstance(field_or_table, ast.StringJSONDatabaseField): return "Object" + if isinstance(field_or_table, ast.ExpressionField): + return "Expression" if isinstance(field_or_table, (ast.Table, ast.LazyJoin)): return "Table" - return "" + return None def get_table(context: HogQLContext, join_expr: ast.JoinExpr, ctes: Optional[Dict[str, CTE]]) -> None | Table: @@ -181,6 +183,75 @@ def to_printed_hogql(self): return None +def get_tables_aliases(query: ast.SelectQuery, context: HogQLContext) -> Dict[str, ast.Table]: + tables: Dict[str, ast.Table] = {} + + if query.select_from is not None and query.select_from.alias is not None: + table = get_table(context, query.select_from, query.ctes) + if table is not None: + tables[query.select_from.alias] = table + + if query.select_from is not None and query.select_from.next_join is not None: + next_join: ast.JoinExpr | None = query.select_from.next_join + while next_join is not None: + if next_join.alias is not None: + table = get_table(context, next_join, query.ctes) + if table is not None: + tables[next_join.alias] = table + next_join = next_join.next_join + + return tables + + +# Replaces all ast.FieldTraverser with the underlying node +def resolve_table_field_traversers(table: Table) -> Table: + new_table = deepcopy(table) + new_fields: Dict[str, FieldOrTable] = {} + for key, field in list(new_table.fields.items()): + if not isinstance(field, ast.FieldTraverser): + new_fields[key] = field + continue + + current_table_or_field: FieldOrTable = new_table + for chain in field.chain: + if isinstance(current_table_or_field, Table): + chain_field = current_table_or_field.fields.get(chain) + elif isinstance(current_table_or_field, LazyJoin): + chain_field = current_table_or_field.join_table.fields.get(chain) + elif isinstance(current_table_or_field, DatabaseField): + chain_field = current_table_or_field + else: + # Cant find the field, default back + new_fields[key] = field + break + + if chain_field is not None: + current_table_or_field = chain_field + new_fields[key] = chain_field + + new_table.fields = new_fields + return new_table + + +def append_table_field_to_response(table: Table, suggestions: List[AutocompleteCompletionItem]) -> None: + keys: List[str] = [] + details: List[str | None] = [] + table_fields = list(table.fields.items()) + for field_name, field_or_table in table_fields: + keys.append(field_name) + details.append(convert_field_or_table_to_type_string(field_or_table)) + + extend_responses(keys=keys, suggestions=suggestions, details=details) + + available_functions = ALL_EXPOSED_FUNCTION_NAMES + extend_responses( + available_functions, + suggestions, + Kind.Function, + insert_text=lambda key: f"{key}()", + ) + + def extend_responses( keys: List[str], suggestions: List[AutocompleteCompletionItem], @@ -207,7 +278,7 @@ def extend_responses( # TODO: Support ast.SelectUnionQuery nodes def get_hogql_autocomplete(query: HogQLAutocomplete, team: Team) -> HogQLAutocompleteResponse: - response = HogQLAutocompleteResponse(suggestions=[]) + response = HogQLAutocompleteResponse(suggestions=[], incomplete_list=False) database = create_hogql_database(team_id=team.pk, team_arg=team) context = HogQLContext(team_id=team.pk, team=team, database=database) @@ -259,8 +330,6 @@ def get_hogql_autocomplete(query: HogQLAutocomplete, team: Team) -> HogQLAutocom and nearest_select.select_from is not None and not isinstance(parent_node, ast.JoinExpr) ): - # TODO: add logic for FieldTraverser field types - # Handle fields table = get_table(context, nearest_select.select_from, ctes) if table is None: @@ -271,33 +340,31 @@ def get_hogql_autocomplete(query: HogQLAutocomplete, team: Team) -> HogQLAutocom for index, chain_part in enumerate(node.chain): # Return just the table alias if table_has_alias and index == 0 and chain_len == 1: - extend_responses([str(chain_part)], response.suggestions, Kind.Folder) + table_aliases = list(get_tables_aliases(nearest_select, context).keys()) + extend_responses( + keys=table_aliases, + suggestions=response.suggestions, + kind=Kind.Folder, + details=["Table"] * len(table_aliases), # type: ignore + ) break if table_has_alias and index == 0: + tables = get_tables_aliases(nearest_select, context) + aliased_table = tables.get(str(chain_part)) + if aliased_table is not None: + last_table = aliased_table continue # Ignore last chain part, it's likely an incomplete word or added characters is_last_part = index >= (chain_len - 2) + # Replaces all ast.FieldTraverser with the underlying node + last_table = resolve_table_field_traversers(last_table) + if is_last_part: if last_table.fields.get(str(chain_part)) is None: - keys: List[str] = [] - details: List[str | None] = [] - table_fields = list(table.fields.items()) - for field_name, field_or_table in table_fields: - keys.append(field_name) - details.append(convert_field_or_table_to_type_string(field_or_table)) - - extend_responses(keys=keys, suggestions=response.suggestions, details=details) - - available_functions = ALL_EXPOSED_FUNCTION_NAMES - extend_responses( - available_functions, - response.suggestions, - Kind.Function, - insert_text=lambda key: f"{key}()", - ) + append_table_field_to_response(table=last_table, suggestions=response.suggestions) break field = last_table.fields[str(chain_part)] @@ -328,12 +395,22 @@ def get_hogql_autocomplete(query: HogQLAutocomplete, team: Team) -> HogQLAutocom suggestions=response.suggestions, details=[prop["property_type"] for prop in properties], ) + response.incomplete_list = True elif isinstance(field, VirtualTable) or isinstance(field, LazyTable): - fields = list(last_table.fields.keys()) - extend_responses(fields, response.suggestions) + fields = list(last_table.fields.items()) + extend_responses( + keys=[key for key, field in fields], + suggestions=response.suggestions, + details=[convert_field_or_table_to_type_string(field) for key, field in fields], + ) elif isinstance(field, LazyJoin): - fields = list(field.join_table.fields.keys()) - extend_responses(fields, response.suggestions) + fields = list(field.join_table.fields.items()) + + extend_responses( + keys=[key for key, field in fields], + suggestions=response.suggestions, + details=[convert_field_or_table_to_type_string(field) for key, field in fields], + ) break else: field = last_table.fields[str(chain_part)] @@ -345,7 +422,12 @@ def get_hogql_autocomplete(query: HogQLAutocomplete, team: Team) -> HogQLAutocom # Handle table names if len(node.chain) == 1: table_names = database.get_all_tables() - extend_responses(table_names, response.suggestions, Kind.Folder) + extend_responses( + keys=table_names, + suggestions=response.suggestions, + kind=Kind.Folder, + details=["Table"] * len(table_names), # type: ignore + ) except Exception: pass diff --git a/posthog/hogql/test/test_autocomplete.py b/posthog/hogql/test/test_autocomplete.py index f644ae7337e4d..0f5ac0a464129 100644 --- a/posthog/hogql/test/test_autocomplete.py +++ b/posthog/hogql/test/test_autocomplete.py @@ -148,3 +148,54 @@ def test_autocomplete_cte_constant_type(self): assert results.suggestions[0].label == "potato" assert "event" not in [suggestion.label for suggestion in results.suggestions] assert "properties" not in [suggestion.label for suggestion in results.suggestions] + + def test_autocomplete_field_traversers(self): + query = "select person. from events" + results = self._query_response(query=query, start=14, end=14) + assert len(results.suggestions) != 0 + + def test_autocomplete_table_alias(self): + query = "select from events e" + results = self._query_response(query=query, start=7, end=7) + assert len(results.suggestions) != 0 + assert results.suggestions[0].label == "e" + + def test_autocomplete_complete_list(self): + query = "select event from events" + results = self._query_response(query=query, start=7, end=12) + assert results.incomplete_list is False + + def test_autocomplete_incomplete_list(self): + query = "select properties. from events" + results = self._query_response(query=query, start=18, end=18) + assert results.incomplete_list is True + + def test_autocomplete_joined_tables(self): + query = "select p. from events e left join persons p on e.person_id = p.id" + results = self._query_response(query=query, start=9, end=9) + + assert len(results.suggestions) != 0 + + keys = list(PERSONS_FIELDS.keys()) + + for index, key in enumerate(keys): + assert results.suggestions[index].label == key + + def test_autocomplete_joined_table_contraints(self): + query = "select p.id from events e left join persons p on e.person_id = p." + results = self._query_response(query=query, start=65, end=65) + + assert len(results.suggestions) != 0 + + keys = list(PERSONS_FIELDS.keys()) + + for index, key in enumerate(keys): + assert results.suggestions[index].label == key + + def test_autocomplete_joined_tables_aliases(self): + query = "select from events e left join persons p on e.person_id = p.id" + results = self._query_response(query=query, start=7, end=7) + + assert len(results.suggestions) == 2 + assert results.suggestions[0].label == "e" + assert results.suggestions[1].label == "p" diff --git a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr index d45052c06889a..9ff7f8ee0ab49 100644 --- a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr +++ b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr @@ -31,7 +31,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [16]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [12]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -42,7 +42,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [16])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [12])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' @@ -55,7 +55,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [17]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [13]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -66,7 +66,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [17])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [13])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' diff --git a/posthog/hogql_queries/insights/funnels/__init__.py b/posthog/hogql_queries/insights/funnels/__init__.py index d6cddab2ba293..37061f5d8a71b 100644 --- a/posthog/hogql_queries/insights/funnels/__init__.py +++ b/posthog/hogql_queries/insights/funnels/__init__.py @@ -1,3 +1,4 @@ from .base import FunnelBase from .funnel import Funnel from .funnel_strict import FunnelStrict +from .funnel_unordered import FunnelUnordered diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered.py b/posthog/hogql_queries/insights/funnels/funnel_unordered.py new file mode 100644 index 0000000000000..03745309f9321 --- /dev/null +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered.py @@ -0,0 +1,247 @@ +from typing import Any, Dict, List, Optional +import uuid + +from rest_framework.exceptions import ValidationError +from posthog.hogql import ast +from posthog.hogql.parser import parse_expr +from posthog.hogql_queries.insights.funnels.base import FunnelBase +from posthog.hogql_queries.insights.funnels.utils import funnel_window_interval_unit_to_sql +from posthog.schema import ActionsNode, EventsNode +from posthog.queries.util import correct_result_for_sampling + + +class FunnelUnordered(FunnelBase): + """ + Unordered Funnel is a funnel where the order of steps doesn't matter. + + ## Query Intuition + + Imagine a funnel with three events: A, B, and C. + This query splits the problem into two parts: + 1. Given the first event is A, find the furthest everyone went starting from A. + This finds any B's and C's that happen after A (without ordering them) + 2. Repeat the above, assuming first event to be B, and then C. + + Then, the outer query unions the result of (2) and takes the maximum of these. + + ## Results + + The result format is the same as the basic funnel, i.e. [step, count]. + Here, `step_i` (0 indexed) signifies the number of people that did at least `i+1` steps. + + ## Exclusion Semantics + For unordered funnels, exclusion is a bit weird. It means, given all ordering of the steps, + how far can you go without seeing an exclusion event. + If you see an exclusion event => you're discarded. + See test_advanced_funnel_multiple_exclusions_between_steps for details. + """ + + def get_query(self): + max_steps = self.context.max_steps + + for exclusion in self.context.funnelsFilter.exclusions or []: + if exclusion.funnelFromStep != 0 or exclusion.funnelToStep != max_steps - 1: + raise ValidationError("Partial Exclusions not allowed in unordered funnels") + + breakdown_exprs = self._get_breakdown_prop_expr() + + select: List[ast.Expr] = [ + *self._get_count_columns(max_steps), + *self._get_step_time_avgs(max_steps), + *self._get_step_time_median(max_steps), + *breakdown_exprs, + ] + + return ast.SelectQuery( + select=select, + select_from=ast.JoinExpr(table=self.get_step_counts_query()), + group_by=[ast.Field(chain=["prop"])] if len(breakdown_exprs) > 0 else None, + ) + + def get_step_counts_query(self): + max_steps = self.context.max_steps + breakdown_exprs = self._get_breakdown_prop_expr() + inner_timestamps, outer_timestamps = self._get_timestamp_selects() + person_and_group_properties = self._get_person_and_group_properties() + + group_by_columns: List[ast.Expr] = [ + ast.Field(chain=["aggregation_target"]), + ast.Field(chain=["steps"]), + *breakdown_exprs, + ] + + outer_select: List[ast.Expr] = [ + *group_by_columns, + *self._get_step_time_avgs(max_steps, inner_query=True), + *self._get_step_time_median(max_steps, inner_query=True), + *outer_timestamps, + *person_and_group_properties, + ] + + max_steps_expr = parse_expr( + f"max(steps) over (PARTITION BY aggregation_target {self._get_breakdown_prop()}) as max_steps" + ) + + inner_select: List[ast.Expr] = [ + *group_by_columns, + max_steps_expr, + *self._get_step_time_names(max_steps), + *inner_timestamps, + *person_and_group_properties, + ] + + return ast.SelectQuery( + select=outer_select, + select_from=ast.JoinExpr( + table=ast.SelectQuery( + select=inner_select, + select_from=ast.JoinExpr(table=self.get_step_counts_without_aggregation_query()), + ) + ), + group_by=group_by_columns, + having=ast.CompareOperation( + left=ast.Field(chain=["steps"]), right=ast.Field(chain=["max_steps"]), op=ast.CompareOperationOp.Eq + ), + ) + + def get_step_counts_without_aggregation_query(self): + max_steps = self.context.max_steps + union_queries: List[ast.SelectQuery] = [] + entities_to_use = list(self.context.query.series) + + for i in range(max_steps): + inner_query = ast.SelectQuery( + select=[ + ast.Field(chain=["aggregation_target"]), + ast.Field(chain=["timestamp"]), + *self._get_partition_cols(1, max_steps), + *self._get_breakdown_prop_expr(group_remaining=True), + *self._get_person_and_group_properties(), + ], + select_from=ast.JoinExpr(table=self._get_inner_event_query(entities_to_use, f"events_{i}")), + ) + + where_exprs = [ + ast.CompareOperation( + left=ast.Field(chain=["step_0"]), right=ast.Constant(value=1), op=ast.CompareOperationOp.Eq + ), + ( + ast.CompareOperation( + left=ast.Field(chain=["exclusion"]), right=ast.Constant(value=0), op=ast.CompareOperationOp.Eq + ) + if self._get_exclusion_condition() != [] + else None + ), + ] + where = ast.And(exprs=[expr for expr in where_exprs if expr is not None]) + + formatted_query = ast.SelectQuery( + select=[ + ast.Field(chain=["*"]), + *self.get_sorting_condition(max_steps), + *self._get_exclusion_condition(), + *self._get_step_times(max_steps), + *self._get_person_and_group_properties(), + ], + select_from=ast.JoinExpr(table=inner_query), + where=where, + ) + + #  rotate entities by 1 to get new first event + entities_to_use.append(entities_to_use.pop(0)) + union_queries.append(formatted_query) + + return ast.SelectUnionQuery(select_queries=union_queries) + + def _get_step_times(self, max_steps: int) -> List[ast.Expr]: + windowInterval = self.context.funnelWindowInterval + windowIntervalUnit = funnel_window_interval_unit_to_sql(self.context.funnelWindowIntervalUnit) + + exprs: List[ast.Expr] = [] + + conversion_times_elements = [] + for i in range(max_steps): + conversion_times_elements.append(f"latest_{i}") + + exprs.append(parse_expr(f"arraySort([{','.join(conversion_times_elements)}]) as conversion_times")) + + for i in range(1, max_steps): + exprs.append( + parse_expr( + f"if(isNotNull(conversion_times[{i+1}]) AND conversion_times[{i+1}] <= conversion_times[{i}] + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', conversion_times[{i}], conversion_times[{i+1}]), NULL) step_{i}_conversion_time" + ) + ) + # array indices in ClickHouse are 1-based :shrug: + + return exprs + + def get_sorting_condition(self, max_steps: int) -> List[ast.Expr]: + windowInterval = self.context.funnelWindowInterval + windowIntervalUnit = funnel_window_interval_unit_to_sql(self.context.funnelWindowIntervalUnit) + + conditions = [] + + event_times_elements = [] + for i in range(max_steps): + event_times_elements.append(f"latest_{i}") + + conditions.append(parse_expr(f"arraySort([{','.join(event_times_elements)}]) as event_times")) + # replacement of latest_i for whatever query part requires it, just like conversion_times + basic_conditions: List[str] = [] + for i in range(1, max_steps): + basic_conditions.append( + f"if(latest_0 < latest_{i} AND latest_{i} <= latest_0 + INTERVAL {windowInterval} {windowIntervalUnit}, 1, 0)" + ) + + if basic_conditions: + conditions.append(ast.Alias(alias="steps", expr=parse_expr(f"arraySum([{','.join(basic_conditions)}, 1])"))) + return conditions + else: + return [ast.Alias(alias="steps", expr=ast.Constant(value=1))] + + def _get_exclusion_condition(self) -> List[ast.Expr]: + funnelsFilter = self.context.funnelsFilter + windowInterval = self.context.funnelWindowInterval + windowIntervalUnit = funnel_window_interval_unit_to_sql(self.context.funnelWindowIntervalUnit) + + if not funnelsFilter.exclusions: + return [] + + conditions: List[ast.Expr] = [] + + for exclusion_id, exclusion in enumerate(funnelsFilter.exclusions): + from_time = f"latest_{exclusion.funnelFromStep}" + to_time = f"event_times[{exclusion.funnelToStep + 1}]" + exclusion_time = f"exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}" + condition = parse_expr( + f"if( {exclusion_time} > {from_time} AND {exclusion_time} < if(isNull({to_time}), {from_time} + INTERVAL {windowInterval} {windowIntervalUnit}, {to_time}), 1, 0)" + ) + conditions.append(condition) + + if conditions: + return [ + ast.Alias( + alias="exclusion", + expr=ast.Call(name="arraySum", args=[ast.Array(exprs=conditions)]), + ) + ] + else: + return [] + + def _serialize_step( + self, + step: ActionsNode | EventsNode, + count: int, + index: int, + people: Optional[List[uuid.UUID]] = None, + sampling_factor: Optional[float] = None, + ) -> Dict[str, Any]: + return { + "action_id": None, + "name": f"Completed {index+1} step{'s' if index != 0 else ''}", + "custom_name": None, + "order": index, + "people": people if people else [], + "count": correct_result_for_sampling(count, sampling_factor), + "type": "events" if isinstance(step, EventsNode) else "actions", + } diff --git a/posthog/hogql_queries/insights/funnels/test/conversion_time_cases.py b/posthog/hogql_queries/insights/funnels/test/conversion_time_cases.py index 63cf914e84cc8..5ff9a7385fc0a 100644 --- a/posthog/hogql_queries/insights/funnels/test/conversion_time_cases.py +++ b/posthog/hogql_queries/insights/funnels/test/conversion_time_cases.py @@ -28,7 +28,7 @@ def test_funnel_with_multiple_incomplete_tries(self): {"id": "$pageview", "type": "events", "order": 1}, {"id": "something else", "type": "events", "order": 2}, ], - "funnel_window_days": 1, + "funnel_window_interval": 1, "date_from": "2021-05-01 00:00:00", "date_to": "2021-05-14 00:00:00", } diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py new file mode 100644 index 0000000000000..ae72ba3ab37b3 --- /dev/null +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py @@ -0,0 +1,1611 @@ +# from datetime import datetime +from typing import cast + +from rest_framework.exceptions import ValidationError + +from posthog.constants import INSIGHT_FUNNELS, FunnelOrderType +from posthog.hogql_queries.insights.funnels.funnels_query_runner import FunnelsQueryRunner +from posthog.hogql_queries.legacy_compatibility.filter_to_query import filter_to_query + +# from posthog.models.action import Action +# from posthog.models.action_step import ActionStep +from posthog.models.filters import Filter +from posthog.models.property_definition import PropertyDefinition +from posthog.queries.funnels.funnel_unordered_persons import ( + ClickhouseFunnelUnorderedActors, +) +from posthog.hogql_queries.insights.funnels.test.conversion_time_cases import ( + funnel_conversion_time_test_factory, +) +from posthog.schema import FunnelsQuery + +# from posthog.hogql_queries.insights.funnels.test.breakdown_cases import ( +# assert_funnel_results_equal, +# funnel_breakdown_test_factory, +# ) +from posthog.test.base import ( + APIBaseTest, + ClickhouseTestMixin, + _create_event, + _create_person, + # snapshot_clickhouse_queries, +) + +# from posthog.test.test_journeys import journeys_for + +FORMAT_TIME = "%Y-%m-%d 00:00:00" + + +# def _create_action(**kwargs): +# team = kwargs.pop("team") +# name = kwargs.pop("name") +# properties = kwargs.pop("properties", {}) +# action = Action.objects.create(team=team, name=name) +# ActionStep.objects.create(action=action, event=name, properties=properties) +# return action + + +# class TestFunnelUnorderedStepsBreakdown( +# ClickhouseTestMixin, +# funnel_breakdown_test_factory( # type: ignore +# FunnelUnordered, +# ClickhouseFunnelUnorderedActors, +# _create_event, +# _create_action, +# _create_person, +# ), +# ): +# maxDiff = None + +# def test_funnel_step_breakdown_event_single_person_events_with_multiple_properties(self): +# # overriden from factory + +# filters = { +# "events": [{"id": "sign up", "order": 0}, {"id": "play movie", "order": 1}], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": "$browser", +# "breakdown_attribution_type": "all_events", +# } + +# # event +# person1 = _create_person(distinct_ids=["person1"], team_id=self.team.pk) +# _create_event( +# team=self.team, +# event="sign up", +# distinct_id="person1", +# properties={"key": "val", "$browser": "Chrome"}, +# timestamp="2020-01-01T12:00:00Z", +# ) +# _create_event( +# team=self.team, +# event="sign up", +# distinct_id="person1", +# properties={"key": "val", "$browser": "Safari"}, +# timestamp="2020-01-02T13:00:00Z", +# ) +# _create_event( +# team=self.team, +# event="play movie", +# distinct_id="person1", +# properties={"key": "val", "$browser": "Safari"}, +# timestamp="2020-01-02T14:00:00Z", +# ) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + +# assert_funnel_results_equal( +# results[0], +# [ +# { +# "action_id": None, +# "name": "Completed 1 step", +# "custom_name": None, +# "order": 0, +# "people": [], +# "count": 1, +# "type": "events", +# "average_conversion_time": None, +# "median_conversion_time": None, +# "breakdown": ["Chrome"], +# "breakdown_value": ["Chrome"], +# }, +# { +# "action_id": None, +# "name": "Completed 2 steps", +# "custom_name": None, +# "order": 1, +# "people": [], +# "count": 0, +# "type": "events", +# "average_conversion_time": None, +# "median_conversion_time": None, +# "breakdown": ["Chrome"], +# "breakdown_value": ["Chrome"], +# }, +# ], +# ) +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, ["Chrome"]), [person1.uuid]) +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 2, ["Chrome"]), []) + +# assert_funnel_results_equal( +# results[1], +# [ +# { +# "action_id": None, +# "name": "Completed 1 step", +# "custom_name": None, +# "order": 0, +# "people": [], +# "count": 1, +# "type": "events", +# "average_conversion_time": None, +# "median_conversion_time": None, +# "breakdown": ["Safari"], +# "breakdown_value": ["Safari"], +# }, +# { +# "action_id": None, +# "name": "Completed 2 steps", +# "custom_name": None, +# "order": 1, +# "people": [], +# "count": 1, +# "type": "events", +# "average_conversion_time": 3600, +# "median_conversion_time": 3600, +# "breakdown": ["Safari"], +# "breakdown_value": ["Safari"], +# }, +# ], +# ) +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, ["Safari"]), [person1.uuid]) +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 2, ["Safari"]), [person1.uuid]) + +# def test_funnel_step_breakdown_with_step_attribution(self): +# # overridden from factory, since with no order, step one is step zero, and vice versa + +# filters = { +# "events": [{"id": "sign up", "order": 0}, {"id": "buy", "order": 1}], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": ["$browser"], +# "breakdown_attribution_type": "step", +# "breakdown_attribution_value": "0", +# "funnel_order_type": "unordered", +# } + +# # event +# events_by_person = { +# "person1": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 1, 12), +# "properties": {"$browser": "Chrome"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 1, 13)}, +# ], +# "person2": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 1, 13)}, +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 13), +# "properties": {"$browser": "Safari"}, +# }, +# ], +# "person3": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 14), +# "properties": {"$browser": "Mac"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 2, 15)}, +# ], +# "person4": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 15), +# "properties": {"$browser": 0}, +# }, +# # step attribution means alakazam is valid when step = 1 +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 16), +# "properties": {"$browser": "alakazam"}, +# }, +# ], +# } +# people = journeys_for(events_by_person, self.team) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results +# results = sorted(results, key=lambda res: res[0]["breakdown"]) + +# self.assertEqual(len(results), 6) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "Mac"), [people["person3"].uuid]) + +# def test_funnel_step_breakdown_with_step_one_attribution(self): +# # overridden from factory, since with no order, step one is step zero, and vice versa +# filters = { +# "events": [{"id": "sign up", "order": 0}, {"id": "buy", "order": 1}], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": ["$browser"], +# "breakdown_attribution_type": "step", +# "breakdown_attribution_value": "1", +# "funnel_order_type": "unordered", +# } + +# # event +# events_by_person = { +# "person1": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 1, 12), +# "properties": {"$browser": "Chrome"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 1, 13)}, +# ], +# "person2": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 1, 13)}, +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 13), +# "properties": {"$browser": "Safari"}, +# }, +# ], +# "person3": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 14), +# "properties": {"$browser": "Mac"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 2, 15)}, +# ], +# "person4": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 15), +# "properties": {"$browser": 0}, +# }, +# # step attribution means alakazam is valid when step = 1 +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 16), +# "properties": {"$browser": "alakazam"}, +# }, +# ], +# } +# people = journeys_for(events_by_person, self.team) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results +# results = sorted(results, key=lambda res: res[0]["breakdown"]) + +# self.assertEqual(len(results), 6) +# # unordered, so everything is step one too. + +# self._assert_funnel_breakdown_result_is_correct( +# results[0], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=[""], count=3), +# FunnelStepResult( +# name="Completed 2 steps", +# breakdown=[""], +# count=2, +# average_conversion_time=3600, +# median_conversion_time=3600, +# ), +# ], +# ) + +# self.assertCountEqual( +# self._get_actor_ids_at_step(filters, 1, ""), +# [people["person1"].uuid, people["person2"].uuid, people["person3"].uuid], +# ) +# self.assertCountEqual( +# self._get_actor_ids_at_step(filters, 2, ""), +# [people["person1"].uuid, people["person3"].uuid], +# ) + +# self._assert_funnel_breakdown_result_is_correct( +# results[1], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["0"], count=1), +# FunnelStepResult(name="Completed 2 steps", breakdown=["0"], count=0), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "0"), [people["person4"].uuid]) + +# def test_funnel_step_breakdown_with_step_one_attribution_incomplete_funnel(self): +# # overridden from factory, since with no order, step one is step zero, and vice versa + +# filters = { +# "events": [{"id": "sign up", "order": 0}, {"id": "buy", "order": 1}], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": ["$browser"], +# "breakdown_attribution_type": "step", +# "breakdown_attribution_value": "1", +# "funnel_order_type": "unordered", +# } + +# # event +# events_by_person = { +# "person1": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 1, 12), +# "properties": {"$browser": "Chrome"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 1, 13)}, +# ], +# "person2": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 1, 13)}, +# # {"event": "buy", "timestamp": datetime(2020, 1, 2, 13), "properties": {"$browser": "Safari"}} +# ], +# "person3": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 14), +# "properties": {"$browser": "Mac"}, +# }, +# # {"event": "buy", "timestamp": datetime(2020, 1, 2, 15)} +# ], +# "person4": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 15), +# "properties": {"$browser": 0}, +# }, +# # step attribution means alakazam is valid when step = 1 +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 16), +# "properties": {"$browser": "alakazam"}, +# }, +# ], +# } +# people = journeys_for(events_by_person, self.team) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results +# results = sorted(results, key=lambda res: res[0]["breakdown"]) + +# # Breakdown by step_1 means funnel items that never reach step_1 are NULLed out +# self.assertEqual(len(results), 4) +# # Chrome and Mac and Safari goes away + +# self._assert_funnel_breakdown_result_is_correct( +# results[0], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=[""], count=1), +# FunnelStepResult( +# name="Completed 2 steps", +# breakdown=[""], +# count=1, +# average_conversion_time=3600, +# median_conversion_time=3600, +# ), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, ""), [people["person1"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[1], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["0"], count=1), +# FunnelStepResult(name="Completed 2 steps", breakdown=["0"], count=0), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "0"), [people["person4"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[2], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["Chrome"], count=1), +# FunnelStepResult(name="Completed 2 steps", breakdown=["Chrome"], count=0), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "Chrome"), [people["person1"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[3], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["alakazam"], count=1), +# FunnelStepResult( +# name="Completed 2 steps", +# breakdown=["alakazam"], +# count=1, +# average_conversion_time=3600, +# median_conversion_time=3600, +# ), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "alakazam"), [people["person4"].uuid]) + +# def test_funnel_step_non_array_breakdown_with_step_one_attribution_incomplete_funnel(self): +# # overridden from factory, since with no order, step one is step zero, and vice versa + +# filters = { +# "events": [{"id": "sign up", "order": 0}, {"id": "buy", "order": 1}], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": "$browser", +# "breakdown_attribution_type": "step", +# "breakdown_attribution_value": "1", +# "funnel_order_type": "unordered", +# } + +# # event +# events_by_person = { +# "person1": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 1, 12), +# "properties": {"$browser": "Chrome"}, +# }, +# {"event": "buy", "timestamp": datetime(2020, 1, 1, 13)}, +# ], +# "person2": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 1, 13)}, +# # {"event": "buy", "timestamp": datetime(2020, 1, 2, 13), "properties": {"$browser": "Safari"}} +# ], +# "person3": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 14), +# "properties": {"$browser": "Mac"}, +# }, +# # {"event": "buy", "timestamp": datetime(2020, 1, 2, 15)} +# ], +# "person4": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 15), +# "properties": {"$browser": 0}, +# }, +# # step attribution means alakazam is valid when step = 1 +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 16), +# "properties": {"$browser": "alakazam"}, +# }, +# ], +# } +# people = journeys_for(events_by_person, self.team) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results +# results = sorted(results, key=lambda res: res[0]["breakdown"]) + +# # Breakdown by step_1 means funnel items that never reach step_1 are NULLed out +# self.assertEqual(len(results), 4) +# # Chrome and Mac and Safari goes away + +# self._assert_funnel_breakdown_result_is_correct( +# results[0], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=[""], count=1), +# FunnelStepResult( +# name="Completed 2 steps", +# breakdown=[""], +# count=1, +# average_conversion_time=3600, +# median_conversion_time=3600, +# ), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, ""), [people["person1"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[1], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["0"], count=1), +# FunnelStepResult(name="Completed 2 steps", breakdown=["0"], count=0), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "0"), [people["person4"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[2], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["Chrome"], count=1), +# FunnelStepResult(name="Completed 2 steps", breakdown=["Chrome"], count=0), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "Chrome"), [people["person1"].uuid]) + +# self._assert_funnel_breakdown_result_is_correct( +# results[3], +# [ +# FunnelStepResult(name="Completed 1 step", breakdown=["alakazam"], count=1), +# FunnelStepResult( +# name="Completed 2 steps", +# breakdown=["alakazam"], +# count=1, +# average_conversion_time=3600, +# median_conversion_time=3600, +# ), +# ], +# ) + +# self.assertCountEqual(self._get_actor_ids_at_step(filters, 1, "alakazam"), [people["person4"].uuid]) + +# @snapshot_clickhouse_queries +# def test_funnel_breakdown_correct_breakdown_props_are_chosen_for_step(self): +# # No person querying here, so snapshots are more legible +# # overridden from factory, since we need to add `funnel_order_type` + +# filters = { +# "events": [ +# {"id": "sign up", "order": 0}, +# { +# "id": "buy", +# "properties": [{"type": "event", "key": "$version", "value": "xyz"}], +# "order": 1, +# }, +# ], +# "insight": INSIGHT_FUNNELS, +# "date_from": "2020-01-01", +# "date_to": "2020-01-08", +# "funnel_window_days": 7, +# "breakdown_type": "event", +# "breakdown": "$browser", +# "breakdown_attribution_type": "step", +# "breakdown_attribution_value": "1", +# "funnel_order_type": "unordered", +# } + +# # event +# events_by_person = { +# "person1": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 1, 12), +# "properties": {"$browser": "Chrome", "$version": "xyz"}, +# }, +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 1, 13), +# "properties": {"$browser": "Chrome"}, +# }, +# # discarded because doesn't meet criteria +# ], +# "person2": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 1, 13)}, +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 13), +# "properties": {"$browser": "Safari", "$version": "xyz"}, +# }, +# ], +# "person3": [ +# { +# "event": "sign up", +# "timestamp": datetime(2020, 1, 2, 14), +# "properties": {"$browser": "Mac"}, +# }, +# { +# "event": "buy", +# "timestamp": datetime(2020, 1, 2, 15), +# "properties": {"$version": "xyz", "$browser": "Mac"}, +# }, +# ], +# # no properties dude, doesn't make it to step 1, and since breakdown on step 1, is discarded completely +# "person5": [ +# {"event": "sign up", "timestamp": datetime(2020, 1, 2, 15)}, +# {"event": "buy", "timestamp": datetime(2020, 1, 2, 16)}, +# ], +# } +# journeys_for(events_by_person, self.team) + +# query = cast(FunnelsQuery, filter_to_query(filters)) +# results = FunnelsQueryRunner(query=query, team=self.team).calculate().results +# results = sorted(results, key=lambda res: res[0]["breakdown"]) + +# self.assertEqual(len(results), 3) + +# self.assertCountEqual([res[0]["breakdown"] for res in results], [[""], ["Mac"], ["Safari"]]) + + +class TestFunnelUnorderedStepsConversionTime( + ClickhouseTestMixin, + funnel_conversion_time_test_factory( # type: ignore + FunnelOrderType.UNORDERED, + ClickhouseFunnelUnorderedActors, + ), +): + maxDiff = None + pass + + +class TestFunnelUnorderedSteps(ClickhouseTestMixin, APIBaseTest): + def _get_actor_ids_at_step(self, filter, funnel_step, breakdown_value=None): + filter = Filter(data=filter, team=self.team) + person_filter = filter.shallow_clone({"funnel_step": funnel_step, "funnel_step_breakdown": breakdown_value}) + _, serialized_result, _ = ClickhouseFunnelUnorderedActors(person_filter, self.team).get_actors() + + return [val["id"] for val in serialized_result] + + def test_basic_unordered_funnel(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "order": 0}, + {"id": "$pageview", "order": 1}, + {"id": "insight viewed", "order": 2}, + ], + } + + person1_stopped_after_signup = _create_person(distinct_ids=["stopped_after_signup1"], team_id=self.team.pk) + _create_event(team=self.team, event="user signed up", distinct_id="stopped_after_signup1") + + person2_stopped_after_one_pageview = _create_person( + distinct_ids=["stopped_after_pageview1"], team_id=self.team.pk + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_pageview1") + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_pageview1", + ) + + person3_stopped_after_insight_view = _create_person( + distinct_ids=["stopped_after_insightview"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview", + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview") + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview", + ) + + person4_stopped_after_insight_view_reverse_order = _create_person( + distinct_ids=["stopped_after_insightview2"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview2", + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview2") + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview2", + ) + + person5_stopped_after_insight_view_random = _create_person( + distinct_ids=["stopped_after_insightview3"], team_id=self.team.pk + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview3") + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview3", + ) + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview3") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview3", + ) + + person6_did_only_insight_view = _create_person( + distinct_ids=["stopped_after_insightview4"], team_id=self.team.pk + ) + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview4") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview4", + ) + + person7_did_only_pageview = _create_person(distinct_ids=["stopped_after_insightview5"], team_id=self.team.pk) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview5") + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview5") + + person8_didnot_signup = _create_person(distinct_ids=["stopped_after_insightview6"], team_id=self.team.pk) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview6", + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview6") + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[0]["count"], 8) + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[1]["count"], 5) + self.assertEqual(results[2]["name"], "Completed 3 steps") + self.assertEqual(results[2]["count"], 3) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 1), + [ + person1_stopped_after_signup.uuid, + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + person6_did_only_insight_view.uuid, + person7_did_only_pageview.uuid, + person8_didnot_signup.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 2), + [ + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + person8_didnot_signup.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, -2), + [ + person1_stopped_after_signup.uuid, + person6_did_only_insight_view.uuid, + person7_did_only_pageview.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 3), + [ + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, -3), + [person2_stopped_after_one_pageview.uuid, person8_didnot_signup.uuid], + ) + + def test_big_multi_step_unordered_funnel(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "order": 0}, + {"id": "$pageview", "order": 1}, + {"id": "insight viewed", "order": 2}, + {"id": "crying", "order": 3}, + ], + } + + person1_stopped_after_signup = _create_person(distinct_ids=["stopped_after_signup1"], team_id=self.team.pk) + _create_event(team=self.team, event="user signed up", distinct_id="stopped_after_signup1") + + person2_stopped_after_one_pageview = _create_person( + distinct_ids=["stopped_after_pageview1"], team_id=self.team.pk + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_pageview1") + _create_event(team=self.team, event="crying", distinct_id="stopped_after_pageview1") + + person3_stopped_after_insight_view = _create_person( + distinct_ids=["stopped_after_insightview"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview", + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview") + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview", + ) + + person4_stopped_after_insight_view_reverse_order = _create_person( + distinct_ids=["stopped_after_insightview2"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview2", + ) + _create_event(team=self.team, event="crying", distinct_id="stopped_after_insightview2") + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview2", + ) + + person5_stopped_after_insight_view_random = _create_person( + distinct_ids=["stopped_after_insightview3"], team_id=self.team.pk + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview3") + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview3", + ) + _create_event(team=self.team, event="crying", distinct_id="stopped_after_insightview3") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview3", + ) + + person6_did_only_insight_view = _create_person( + distinct_ids=["stopped_after_insightview4"], team_id=self.team.pk + ) + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview4") + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview4", + ) + + person7_did_only_pageview = _create_person(distinct_ids=["stopped_after_insightview5"], team_id=self.team.pk) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview5") + _create_event(team=self.team, event="blaah blaa", distinct_id="stopped_after_insightview5") + + person8_didnot_signup = _create_person(distinct_ids=["stopped_after_insightview6"], team_id=self.team.pk) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview6", + ) + _create_event(team=self.team, event="$pageview", distinct_id="stopped_after_insightview6") + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[0]["count"], 8) + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[1]["count"], 5) + self.assertEqual(results[2]["name"], "Completed 3 steps") + self.assertEqual(results[2]["count"], 3) + self.assertEqual(results[3]["name"], "Completed 4 steps") + self.assertEqual(results[3]["count"], 1) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 1), + [ + person1_stopped_after_signup.uuid, + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + person6_did_only_insight_view.uuid, + person7_did_only_pageview.uuid, + person8_didnot_signup.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 2), + [ + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + person8_didnot_signup.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 3), + [ + person3_stopped_after_insight_view.uuid, + person4_stopped_after_insight_view_reverse_order.uuid, + person5_stopped_after_insight_view_random.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 4), + [person5_stopped_after_insight_view_random.uuid], + ) + + def test_basic_unordered_funnel_conversion_times(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "order": 0}, + {"id": "$pageview", "order": 1}, + {"id": "insight viewed", "order": 2}, + ], + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-07 23:59:59", + "funnel_window_interval": "1", + } + + person1_stopped_after_signup = _create_person(distinct_ids=["stopped_after_signup1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_signup1", + timestamp="2021-05-02 00:00:00", + ) + + person2_stopped_after_one_pageview = _create_person( + distinct_ids=["stopped_after_pageview1"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="stopped_after_pageview1", + timestamp="2021-05-02 00:00:00", + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_pageview1", + timestamp="2021-05-02 01:00:00", + ) + + person3_stopped_after_insight_view = _create_person( + distinct_ids=["stopped_after_insightview"], team_id=self.team.pk + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview", + timestamp="2021-05-02 00:00:00", + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview", + timestamp="2021-05-02 02:00:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="stopped_after_insightview", + timestamp="2021-05-02 04:00:00", + ) + + _create_event( + team=self.team, + event="$pageview", + distinct_id="stopped_after_insightview", + timestamp="2021-05-03 00:00:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="stopped_after_insightview", + timestamp="2021-05-03 03:00:00", + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_insightview", + timestamp="2021-05-03 06:00:00", + ) + # Person 3 completes the funnel 2 times: + # First time: 2 hours + 2 hours = total 4 hours. + # Second time: 3 hours + 3 hours = total 6 hours. + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[2]["name"], "Completed 3 steps") + self.assertEqual(results[0]["count"], 3) + + self.assertEqual(results[1]["average_conversion_time"], 6300) + # 1 hour for Person 2, (2+3)/2 hours for Person 3, total = 3.5 hours, average = 3.5/2 = 1.75 hours + + self.assertEqual(results[2]["average_conversion_time"], 9000) + # (2+3)/2 hours for Person 3 = 2.5 hours + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 1), + [ + person1_stopped_after_signup.uuid, + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 2), + [ + person2_stopped_after_one_pageview.uuid, + person3_stopped_after_insight_view.uuid, + ], + ) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 3), + [person3_stopped_after_insight_view.uuid], + ) + + def test_single_event_unordered_funnel(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [{"id": "user signed up", "order": 0}], + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-07 23:59:59", + } + + _create_person(distinct_ids=["stopped_after_signup1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_signup1", + timestamp="2021-05-02 00:00:00", + ) + + _create_person(distinct_ids=["stopped_after_pageview1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="$pageview", + distinct_id="stopped_after_pageview1", + timestamp="2021-05-02 00:00:00", + ) + _create_event( + team=self.team, + event="user signed up", + distinct_id="stopped_after_pageview1", + timestamp="2021-05-02 01:00:00", + ) + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[0]["count"], 2) + + def test_funnel_exclusions_invalid_params(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "type": "events", "order": 0}, + {"id": "paid", "type": "events", "order": 1}, + {"id": "blah", "type": "events", "order": 2}, + ], + "funnel_window_days": 14, + "exclusions": [ + { + "id": "x", + "type": "events", + "funnel_from_step": 1, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + self.assertRaises(ValidationError, lambda: FunnelsQueryRunner(query=query, team=self.team).calculate()) + + # partial windows not allowed for unordered + filters = { + **filters, + "exclusions": [ + { + "id": "x", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + self.assertRaises(ValidationError, lambda: FunnelsQueryRunner(query=query, team=self.team).calculate()) + + def test_funnel_exclusions_full_window(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "type": "events", "order": 0}, + {"id": "paid", "type": "events", "order": 1}, + ], + "funnel_window_days": 14, + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-14 00:00:00", + "exclusions": [ + { + "id": "x", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + # event 1 + person1 = _create_person(distinct_ids=["person1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person1", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="paid", + distinct_id="person1", + timestamp="2021-05-01 02:00:00", + ) + + # event 2 + person2 = _create_person(distinct_ids=["person2"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person2", + timestamp="2021-05-01 03:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person2", + timestamp="2021-05-01 03:30:00", + ) + _create_event( + team=self.team, + event="paid", + distinct_id="person2", + timestamp="2021-05-01 04:00:00", + ) + + # event 3 + person3 = _create_person(distinct_ids=["person3"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person3", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="paid", + distinct_id="person3", + timestamp="2021-05-01 06:00:00", + ) + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[0]["count"], 3) + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[1]["count"], 2) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 1), + [person1.uuid, person2.uuid, person3.uuid], + ) + self.assertCountEqual(self._get_actor_ids_at_step(filters, 2), [person1.uuid, person3.uuid]) + + def test_advanced_funnel_multiple_exclusions_between_steps(self): + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + {"id": "user signed up", "type": "events", "order": 0}, + {"id": "$pageview", "type": "events", "order": 1}, + {"id": "insight viewed", "type": "events", "order": 2}, + {"id": "invite teammate", "type": "events", "order": 3}, + {"id": "pageview2", "type": "events", "order": 4}, + ], + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-14 00:00:00", + "exclusions": [ + { + "id": "x", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 4, + }, + { + "id": "y", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 4, + }, + ], + } + + person1 = _create_person(distinct_ids=["person1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person1", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person1", + timestamp="2021-05-01 02:00:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="person1", + timestamp="2021-05-01 03:00:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="person1", + timestamp="2021-05-01 04:00:00", + ) + _create_event( + team=self.team, + event="y", + distinct_id="person1", + timestamp="2021-05-01 04:30:00", + ) + _create_event( + team=self.team, + event="invite teammate", + distinct_id="person1", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="pageview2", + distinct_id="person1", + timestamp="2021-05-01 06:00:00", + ) + + person2 = _create_person(distinct_ids=["person2"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person2", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="y", + distinct_id="person2", + timestamp="2021-05-01 01:30:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="person2", + timestamp="2021-05-01 02:00:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="person2", + timestamp="2021-05-01 04:00:00", + ) + _create_event( + team=self.team, + event="y", + distinct_id="person2", + timestamp="2021-05-01 04:30:00", + ) + _create_event( + team=self.team, + event="invite teammate", + distinct_id="person2", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person2", + timestamp="2021-05-01 05:30:00", + ) + _create_event( + team=self.team, + event="pageview2", + distinct_id="person2", + timestamp="2021-05-01 06:00:00", + ) + + person3 = _create_person(distinct_ids=["person3"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person3", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person3", + timestamp="2021-05-01 01:30:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="person3", + timestamp="2021-05-01 02:00:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="person3", + timestamp="2021-05-01 04:00:00", + ) + _create_event( + team=self.team, + event="invite teammate", + distinct_id="person3", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person3", + timestamp="2021-05-01 05:30:00", + ) + _create_event( + team=self.team, + event="pageview2", + distinct_id="person3", + timestamp="2021-05-01 06:00:00", + ) + + person4 = _create_person(distinct_ids=["person4"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person4", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="person4", + timestamp="2021-05-01 02:00:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="person4", + timestamp="2021-05-01 04:00:00", + ) + _create_event( + team=self.team, + event="invite teammate", + distinct_id="person4", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="pageview2", + distinct_id="person4", + timestamp="2021-05-01 06:00:00", + ) + + person5 = _create_person(distinct_ids=["person5"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person5", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person5", + timestamp="2021-05-01 01:30:00", + ) + _create_event( + team=self.team, + event="$pageview", + distinct_id="person5", + timestamp="2021-05-01 02:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person5", + timestamp="2021-05-01 02:30:00", + ) + _create_event( + team=self.team, + event="insight viewed", + distinct_id="person5", + timestamp="2021-05-01 04:00:00", + ) + _create_event( + team=self.team, + event="y", + distinct_id="person5", + timestamp="2021-05-01 04:30:00", + ) + _create_event( + team=self.team, + event="invite teammate", + distinct_id="person5", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person5", + timestamp="2021-05-01 05:30:00", + ) + _create_event( + team=self.team, + event="pageview2", + distinct_id="person5", + timestamp="2021-05-01 06:00:00", + ) + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["name"], "Completed 1 step") + self.assertEqual(results[0]["count"], 5) + self.assertEqual(results[1]["count"], 2) + self.assertEqual(results[2]["count"], 1) + self.assertEqual(results[3]["count"], 1) + self.assertEqual(results[4]["count"], 1) + + self.assertCountEqual( + self._get_actor_ids_at_step(filters, 1), + [person1.uuid, person2.uuid, person3.uuid, person4.uuid, person5.uuid], + ) + self.assertCountEqual(self._get_actor_ids_at_step(filters, 2), [person1.uuid, person4.uuid]) + self.assertCountEqual(self._get_actor_ids_at_step(filters, 3), [person4.uuid]) + self.assertCountEqual(self._get_actor_ids_at_step(filters, 4), [person4.uuid]) + self.assertCountEqual(self._get_actor_ids_at_step(filters, 5), [person4.uuid]) + + def test_funnel_unordered_all_events_with_properties(self): + _create_person(distinct_ids=["user"], team=self.team) + _create_event(event="user signed up", distinct_id="user", team=self.team) + _create_event( + event="added to card", + distinct_id="user", + properties={"is_saved": True}, + team=self.team, + ) + PropertyDefinition.objects.get_or_create( + team=self.team, + type=PropertyDefinition.Type.EVENT, + name="is_saved", + defaults={"property_type": "Boolean"}, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + { + "type": "events", + "id": "user signed up", + "order": 0, + "name": "user signed up", + "math": "total", + }, + { + "type": "events", + "id": None, + "order": 1, + "name": "All events", + "math": "total", + "properties": [ + { + "key": "is_saved", + "value": ["true"], + "operator": "exact", + "type": "event", + } + ], + }, + ], + "funnel_window_days": 14, + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["count"], 1) + self.assertEqual(results[1]["count"], 1) + + def test_funnel_unordered_entity_filters(self): + _create_person(distinct_ids=["user"], team=self.team) + _create_event( + event="user signed up", + distinct_id="user", + properties={"prop_a": "some value"}, + team=self.team, + ) + _create_event( + event="user signed up", + distinct_id="user", + properties={"prop_b": "another value"}, + team=self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "events": [ + { + "type": "events", + "id": "user signed up", + "order": 0, + "name": "user signed up", + "math": "total", + "properties": [ + { + "key": "prop_a", + "value": ["some value"], + "operator": "exact", + "type": "event", + } + ], + }, + { + "type": "events", + "id": "user signed up", + "order": 1, + "name": "user signed up", + "math": "total", + "properties": [ + { + "key": "prop_b", + "value": "another", + "operator": "icontains", + "type": "event", + } + ], + }, + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["count"], 1) + self.assertEqual(results[1]["count"], 1) diff --git a/posthog/hogql_queries/insights/funnels/utils.py b/posthog/hogql_queries/insights/funnels/utils.py index 2b36b2252cf78..2232ee3f8c7c6 100644 --- a/posthog/hogql_queries/insights/funnels/utils.py +++ b/posthog/hogql_queries/insights/funnels/utils.py @@ -10,13 +10,11 @@ def get_funnel_order_class(funnelsFilter: FunnelsFilter): from posthog.hogql_queries.insights.funnels import ( Funnel, FunnelStrict, - # FunnelUnordered, - FunnelBase, + FunnelUnordered, ) if funnelsFilter.funnelOrderType == StepOrderValue.unordered: - return FunnelBase - # return FunnelUnordered + return FunnelUnordered elif funnelsFilter.funnelOrderType == StepOrderValue.strict: return FunnelStrict return Funnel diff --git a/posthog/hogql_queries/web_analytics/test/test_web_overview.py b/posthog/hogql_queries/web_analytics/test/test_web_overview.py index 0d560ee6c182e..e4fc03121ab1b 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_overview.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_overview.py @@ -35,10 +35,11 @@ def _create_events(self, data, event="$pageview"): ) return person_result - def _run_web_overview_query(self, date_from, date_to): + def _run_web_overview_query(self, date_from, date_to, compare=True): query = WebOverviewQuery( dateRange=DateRange(date_from=date_from, date_to=date_to), properties=[], + compare=compare, ) runner = WebOverviewQueryRunner(team=self.team, query=query) return runner.calculate() @@ -95,24 +96,24 @@ def test_all_time(self): ] ) - results = self._run_web_overview_query("all", "2023-12-15").results + results = self._run_web_overview_query("all", "2023-12-15", compare=False).results visitors = results[0] self.assertEqual("visitors", visitors.key) self.assertEqual(2, visitors.value) - self.assertEqual(0, visitors.previous) + self.assertEqual(None, visitors.previous) self.assertEqual(None, visitors.changeFromPreviousPct) views = results[1] self.assertEqual("views", views.key) self.assertEqual(4, views.value) - self.assertEqual(0, views.previous) + self.assertEqual(None, views.previous) self.assertEqual(None, views.changeFromPreviousPct) sessions = results[2] self.assertEqual("sessions", sessions.key) self.assertEqual(3, sessions.value) - self.assertEqual(0, sessions.previous) + self.assertEqual(None, sessions.previous) self.assertEqual(None, sessions.changeFromPreviousPct) duration_s = results[3] diff --git a/posthog/hogql_queries/web_analytics/web_overview.py b/posthog/hogql_queries/web_analytics/web_overview.py index 26a9255c940cf..2019803faf78a 100644 --- a/posthog/hogql_queries/web_analytics/web_overview.py +++ b/posthog/hogql_queries/web_analytics/web_overview.py @@ -24,8 +24,9 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: mid = self.query_date_range.date_from_as_hogql() end = self.query_date_range.date_to_as_hogql() with self.timings.measure("overview_stats_query"): - query = parse_select( - """ + if self.query.compare: + return parse_select( + """ WITH pages_query AS ( SELECT uniq(if(timestamp >= {mid} AND timestamp < {end}, events.person_id, NULL)) AS unique_users, @@ -86,21 +87,86 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: FROM pages_query CROSS JOIN sessions_query """, - timings=self.timings, - placeholders={ - "start": start, - "mid": mid, - "end": end, - "event_properties": self.event_properties(), - "session_where": self.session_where(include_previous_period=True), - "session_having": self.session_having(include_previous_period=True), - "sample_rate": self._sample_ratio, - "sample_expr": ast.SampleExpr(sample_value=self._sample_ratio), - }, - backend="cpp", - ) + timings=self.timings, + placeholders={ + "start": start, + "mid": mid, + "end": end, + "event_properties": self.event_properties(), + "session_where": self.session_where(include_previous_period=True), + "session_having": self.session_having(include_previous_period=True), + "sample_rate": self._sample_ratio, + }, + ) + else: + return parse_select( + """ +WITH pages_query AS ( + SELECT + uniq(events.person_id) AS unique_users, + count() AS current_pageviews, + uniq(events.properties.$session_id) AS unique_sessions + FROM + events + SAMPLE {sample_rate} + WHERE + event = '$pageview' AND + timestamp >= {mid} AND + timestamp < {end} AND + {event_properties} + ), +sessions_query AS ( + SELECT + avg(duration_s) AS avg_duration_s, + avg(is_bounce) AS bounce_rate + FROM (SELECT + events.properties.`$session_id` AS session_id, + min(events.timestamp) AS min_timestamp, + max(events.timestamp) AS max_timestamp, + dateDiff('second', min_timestamp, max_timestamp) AS duration_s, + countIf(events.event == '$pageview') AS num_pageviews, + countIf(events.event == '$autocapture') AS num_autocaptures, - return query + -- definition of a GA4 bounce from here https://support.google.com/analytics/answer/12195621?hl=en + (num_autocaptures == 0 AND num_pageviews <= 1 AND duration_s < 10) AS is_bounce + FROM + events + SAMPLE {sample_rate} + WHERE + session_id IS NOT NULL + AND (events.event == '$pageview' OR events.event == '$autocapture' OR events.event == '$pageleave') + AND ({session_where}) + GROUP BY + events.properties.`$session_id` + HAVING + ({session_having}) + ) + ) +SELECT + unique_users, + NULL as previous_unique_users, + current_pageviews, + NULL as previous_pageviews, + unique_sessions, + NULL as previous_unique_sessions, + avg_duration_s, + NULL as prev_avg_duration_s, + bounce_rate, + NULL as prev_bounce_rate +FROM pages_query +CROSS JOIN sessions_query + """, + timings=self.timings, + placeholders={ + "start": start, + "mid": mid, + "end": end, + "event_properties": self.event_properties(), + "session_where": self.session_where(include_previous_period=False), + "session_having": self.session_having(include_previous_period=False), + "sample_rate": self._sample_ratio, + }, + ) def calculate(self): response = execute_hogql_query( diff --git a/posthog/models/sharing_configuration.py b/posthog/models/sharing_configuration.py index 44cc70cbb7be4..48ea711f02a1f 100644 --- a/posthog/models/sharing_configuration.py +++ b/posthog/models/sharing_configuration.py @@ -1,8 +1,10 @@ import secrets -from typing import List +from typing import List, cast from django.db import models +from posthog.models.insight import Insight + def get_default_access_token() -> str: return secrets.token_urlsafe(22) @@ -37,6 +39,9 @@ def can_access_object(self, obj: models.Model): if obj.team_id != self.team_id: # type: ignore return False + if obj._meta.object_name == "Insight" and self.dashboard: + return cast(Insight, obj).id in self.get_connected_insight_ids() + for comparison in [self.insight, self.dashboard, self.recording]: if comparison and comparison == obj: return True diff --git a/posthog/permissions.py b/posthog/permissions.py index d1357d46f2959..6e941b32b6d0a 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -1,6 +1,7 @@ from typing import cast from django.db.models import Model +from rest_framework.exceptions import NotFound from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAdminUser from rest_framework.request import Request from rest_framework.views import APIView @@ -231,6 +232,13 @@ def has_permission(self, request, view) -> bool: ), "SharingTokenPermission requires the `sharing_enabled_actions` attribute to be set in the view" if isinstance(request.successful_authenticator, SharingAccessTokenAuthentication): + try: + view.team # noqa: B018 + if request.successful_authenticator.sharing_configuration.team != view.team: + return False + except NotFound: + return False + return view.action in view.sharing_enabled_actions return False diff --git a/posthog/schema.py b/posthog/schema.py index 19357ad626416..4ebc9c675a703 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -328,6 +328,7 @@ class HogQLAutocompleteResponse(BaseModel): model_config = ConfigDict( extra="forbid", ) + incomplete_list: bool = Field(..., description="Whether or not the suggestions returned are complete") suggestions: List[AutocompleteCompletionItem] @@ -667,6 +668,7 @@ class QueryResponseAlternative9(BaseModel): model_config = ConfigDict( extra="forbid", ) + incomplete_list: bool = Field(..., description="Whether or not the suggestions returned are complete") suggestions: List[AutocompleteCompletionItem] @@ -1539,6 +1541,7 @@ class WebOverviewQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) + compare: Optional[bool] = None dateRange: Optional[DateRange] = None kind: Literal["WebOverviewQuery"] = "WebOverviewQuery" properties: List[Union[EventPropertyFilter, PersonPropertyFilter]] diff --git a/posthog/session_recordings/session_recording_api.py b/posthog/session_recordings/session_recording_api.py index 5cbff80d8c944..9780602f53cb4 100644 --- a/posthog/session_recordings/session_recording_api.py +++ b/posthog/session_recordings/session_recording_api.py @@ -27,9 +27,6 @@ from posthog.models.filters.session_recordings_filter import SessionRecordingsFilter from posthog.models.person.person import PersonDistinctId from posthog.session_recordings.models.session_recording import SessionRecording -from posthog.permissions import ( - SharingTokenPermission, -) from posthog.session_recordings.models.session_recording_event import ( SessionRecordingViewed, ) @@ -186,14 +183,6 @@ class SessionRecordingViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet): sharing_enabled_actions = ["retrieve", "snapshots", "snapshot_file"] - def get_permissions(self): - if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): - return [SharingTokenPermission()] - return super().get_permissions() - - def get_authenticators(self): - return [SharingAccessTokenAuthentication(), *super().get_authenticators()] - def get_serializer_class(self) -> Type[serializers.Serializer]: if isinstance(self.request.successful_authenticator, SharingAccessTokenAuthentication): return SessionRecordingSharedSerializer