diff --git a/.eslintrc.js b/.eslintrc.js index 73da9704eee46..5f71d84aebdd8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,7 @@ const globals = { } module.exports = { - ignorePatterns: ['node_modules', 'plugin-server'], + ignorePatterns: ['node_modules', 'plugin-server', 'cypress'], env, settings: { react: { @@ -27,12 +27,12 @@ module.exports = { }, extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:react/recommended', 'plugin:eslint-comments/recommended', 'plugin:storybook/recommended', - 'prettier', 'plugin:compat/recommended', + 'prettier', ], globals, parser: '@typescript-eslint/parser', @@ -42,12 +42,25 @@ module.exports = { }, ecmaVersion: 2018, sourceType: 'module', + project: 'tsconfig.json', }, - plugins: ['prettier', 'react', 'cypress', '@typescript-eslint', 'no-only-tests', 'jest', 'compat', 'posthog'], + plugins: [ + 'prettier', + 'react', + 'cypress', + '@typescript-eslint', + 'no-only-tests', + 'jest', + 'compat', + 'posthog', + 'simple-import-sort', + ], rules: { 'no-console': ['error', { allow: ['warn', 'error'] }], 'no-debugger': 'error', 'no-only-tests/no-only-tests': 'error', + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', 'react/prop-types': [0], 'react/react-in-jsx-scope': [0], 'react/no-unescaped-entities': [0], @@ -72,7 +85,27 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/require-await': 'off', // TODO: Enable - this rule is useful, but doesn't have an autofix + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true, + }, + ], + '@typescript-eslint/explicit-module-boundary-types': [ + 'error', + { + allowArgumentsExplicitlyTypedAsAny: true, + }, + ], curly: 'error', 'no-restricted-imports': [ 'error', @@ -91,6 +124,11 @@ module.exports = { importNames: ['Tooltip'], message: 'Please use Tooltip from @posthog/lemon-ui instead.', }, + { + name: 'antd', + importNames: ['Alert'], + message: 'Please use LemonBanner from @posthog/lemon-ui instead.', + }, ], }, ], @@ -210,6 +248,10 @@ module.exports = { element: 'a', message: 'use instead', }, + { + element: 'Alert', + message: 'use instead', + }, ], }, ], @@ -230,43 +272,37 @@ module.exports = { ...globals, given: 'readonly', }, + rules: { + // The below complains needlessly about expect(api.createInvite).toHaveBeenCalledWith(...) + '@typescript-eslint/unbound-method': 'off', + }, }, { - // disable these rules for files generated by kea-typegen - files: ['*Type.ts', '*Type.tsx'], + files: ['*Type.ts', '*Type.tsx'], // Kea typegen output rules: { - '@typescript-eslint/no-explicit-any': ['off'], - '@typescript-eslint/ban-types': ['off'], + 'no-restricted-imports': 'off', + '@typescript-eslint/ban-types': 'off', + 'simple-import-sort/imports': 'off', + 'simple-import-sort/exports': 'off', }, }, { - // enable the rule specifically for TypeScript files - files: ['*.ts', '*.tsx'], + files: ['frontend/src/scenes/notebooks/Nodes/*'], // Notebooks code weirdly relies on its order of sorting rules: { - '@typescript-eslint/no-explicit-any': ['off'], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - }, - ], - '@typescript-eslint/explicit-module-boundary-types': [ - 'error', - { - allowArgumentsExplicitlyTypedAsAny: true, - }, - ], + 'simple-import-sort/imports': 'off', + 'simple-import-sort/exports': 'off', }, }, { files: ['*.js'], rules: { '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, { files: 'eslint-rules/**/*', - extends: ['eslint:recommended'], rules: { '@typescript-eslint/no-var-requires': 'off', }, diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 3411304b8795a..2ea70218bd608 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -2,10 +2,6 @@ name: Frontend CI on: pull_request: - # NOTE: by running on master, aside from highlight issues on master it also - # ensures we have e.g. node modules cached for master, which can then be - # used for branches. See https://github.com/actions/cache#cache-scopes for - # scope details. push: branches: - master @@ -15,28 +11,71 @@ concurrency: cancel-in-progress: true jobs: + # Job to decide if we should run frontend ci + # See https://github.com/dorny/paths-filter#conditional-execution for more details + # we skip each step individually, so they are still reported as success + # because many of them are required for CI checks to be green + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + name: Determine need to run frontend checks + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + steps: + # For pull requests it's not necessary to check out the code, but we + # also want this to run on master, so we need to check out + - uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + frontend: + # Avoid running frontend tests for irrelevant changes + # NOTE: we are at risk of missing a dependency here. + - 'bin/**' + - 'frontend/**' + # Make sure we run if someone is explicitly change the workflow + - .github/workflows/ci-frontend.yml + # various JS config files + - .eslintrc.js + - .prettier* + - babel.config.js + - jest.*.ts + - tsconfig.json + - tsconfig.*.json + - webpack.config.js + - postcss.config.js + - stylelint* + frontend-code-quality: name: Code quality checks + needs: changes # kea typegen and typescript:check need some more oomph runs-on: ubuntu-latest steps: + # we need at least one thing to run to make sure we include everything for required jobs - uses: actions/checkout@v3 - name: Install pnpm + if: needs.changes.outputs.frontend == 'true' uses: pnpm/action-setup@v2 with: version: 8.x.x - name: Set up Node.js - uses: buildjet/setup-node@v3 + if: needs.changes.outputs.frontend == 'true' + uses: actions/setup-node@v3 with: node-version: 18 - name: Get pnpm cache directory path + if: needs.changes.outputs.frontend == 'true' id: pnpm-cache-dir run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v3 + if: needs.changes.outputs.frontend == 'true' id: pnpm-cache with: path: ${{ steps.pnpm-cache-dir.outputs.PNPM_STORE_PATH }} @@ -44,22 +83,40 @@ jobs: restore-keys: ${{ runner.os }}-pnpm-cypress- - name: Install package.json dependencies with pnpm + if: needs.changes.outputs.frontend == 'true' run: pnpm install --frozen-lockfile - name: Check formatting with prettier + if: needs.changes.outputs.frontend == 'true' run: pnpm prettier:check - - name: Lint with ESLint - run: pnpm eslint + - name: Lint with Stylelint + if: needs.changes.outputs.frontend == 'true' + run: pnpm lint:css - name: Generate logic types and run typescript with strict + if: needs.changes.outputs.frontend == 'true' run: pnpm typegen:write && pnpm typescript:check + - name: Lint with ESLint + if: needs.changes.outputs.frontend == 'true' + run: pnpm lint:js + - name: Check if "schema.json" is up to date + if: needs.changes.outputs.frontend == 'true' run: pnpm schema:build:json && git diff --exit-code + - name: Check toolbar bundle size + if: needs.changes.outputs.frontend == 'true' + uses: preactjs/compressed-size-action@v2 + with: + build-script: 'build' + compression: 'none' + pattern: 'frontend/dist/toolbar.js' + jest: runs-on: ubuntu-latest + needs: changes name: Jest test (${{ matrix.chunk }}) strategy: @@ -69,24 +126,29 @@ jobs: chunk: [1, 2, 3] steps: + # we need at least one thing to run to make sure we include everything for required jobs - uses: actions/checkout@v3 - name: Install pnpm + if: needs.changes.outputs.frontend == 'true' uses: pnpm/action-setup@v2 with: version: 8.x.x - name: Set up Node.js - uses: buildjet/setup-node@v3 + if: needs.changes.outputs.frontend == 'true' + uses: actions/setup-node@v3 with: node-version: 18 cache: pnpm - name: Install package.json dependencies with pnpm + if: needs.changes.outputs.frontend == 'true' run: pnpm install --frozen-lockfile - name: Test with Jest # set maxWorkers or Jest only uses 1 CPU in GitHub Actions run: pnpm test:unit --maxWorkers=2 --shard=${{ matrix.chunk }}/3 + if: needs.changes.outputs.frontend == 'true' env: NODE_OPTIONS: --max-old-space-size=6144 diff --git a/.github/workflows/storybook-chromatic.yml b/.github/workflows/storybook-chromatic.yml index 02397ca2586db..9ae51e2933067 100644 --- a/.github/workflows/storybook-chromatic.yml +++ b/.github/workflows/storybook-chromatic.yml @@ -144,6 +144,7 @@ jobs: HOME: /root # Update snapshots for PRs on the main repo, verify on forks, which don't have access to PostHog Bot VARIANT: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'update' || 'verify' }} + STORYBOOK_SKIP_TAGS: 'test-skip,test-skip-${{ matrix.browser }}' run: | pnpm test:visual-regression:stories:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT diff --git a/.run/Dev.run.xml b/.run/Dev.run.xml new file mode 100644 index 0000000000000..8e0efc8b0e7b3 --- /dev/null +++ b/.run/Dev.run.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.storybook/decorators/withSnapshotsDisabled.tsx b/.storybook/decorators/withSnapshotsDisabled.tsx deleted file mode 100644 index 6e7598c7a9c7e..0000000000000 --- a/.storybook/decorators/withSnapshotsDisabled.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Decorator } from '@storybook/react' -import { inStorybookTestRunner } from 'lib/utils' - -/** Workaround for https://github.com/storybookjs/test-runner/issues/74 */ -// TODO: Smoke-test all the stories by removing this decorator, once all the stories pass -export const withSnapshotsDisabled: Decorator = (Story, { parameters }) => { - if (parameters?.testOptions?.skip && inStorybookTestRunner()) { - return <>Disabled for Test Runner - } - return -} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 091884046929e..fe45ae823c620 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -8,7 +8,6 @@ import { getStorybookAppContext } from './app-context' import { withKea } from './decorators/withKea' import { withMockDate } from './decorators/withMockDate' import { defaultMocks } from '~/mocks/handlers' -import { withSnapshotsDisabled } from './decorators/withSnapshotsDisabled' import { withFeatureFlags } from './decorators/withFeatureFlags' import { withTheme } from './decorators/withTheme' @@ -79,7 +78,6 @@ export const parameters: Parameters = { // Setup storybook global decorators. See https://storybook.js.org/docs/react/writing-stories/decorators#global-decorators export const decorators: Meta['decorators'] = [ - withSnapshotsDisabled, // Make sure the msw service worker is started, and reset the handlers to defaults. withKea, // Allow us to time travel to ensure our stories don't change over time. diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 8decd13e17a65..0ec91135cda24 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -13,11 +13,6 @@ declare module '@storybook/types' { options?: any layout?: 'padded' | 'fullscreen' | 'centered' testOptions?: { - /** - * Whether the test should be a no-op (doesn't jest.skip as @storybook/test-runner doesn't allow that). - * @default false - */ - skip?: boolean /** * Whether we should wait for all loading indicators to disappear before taking a snapshot. * @default true @@ -71,19 +66,20 @@ module.exports = { jest.retryTimes(RETRY_TIMES, { logErrorsBeforeRetry: true }) jest.setTimeout(JEST_TIMEOUT_MS) }, - async postRender(page, context) { + async postVisit(page, context) { const browserContext = page.context() const storyContext = (await getStoryContext(page, context)) as StoryContext - const { skip = false, snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {} + const { snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {} browserContext.setDefaultTimeout(PLAYWRIGHT_TIMEOUT_MS) - if (!skip) { - const currentBrowser = browserContext.browser()!.browserType().name() as SupportedBrowserName - if (snapshotBrowsers.includes(currentBrowser)) { - await expectStoryToMatchSnapshot(page, context, storyContext, currentBrowser) - } + const currentBrowser = browserContext.browser()!.browserType().name() as SupportedBrowserName + if (snapshotBrowsers.includes(currentBrowser)) { + await expectStoryToMatchSnapshot(page, context, storyContext, currentBrowser) } }, + tags: { + skip: ['test-skip'], // NOTE: This is overridden by the CI action storybook-chromatic.yml to include browser specific skipping + }, } as TestRunnerConfig async function expectStoryToMatchSnapshot( diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000000000..46b4d2996945c --- /dev/null +++ b/.stylelintignore @@ -0,0 +1 @@ +frontend/dist/ diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000000000..19bbc999f0f51 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,49 @@ +module.exports = { + extends: 'stylelint-config-standard-scss', // TODO: Enable separately, as the diff will be significant + // TODO: Enable separately, as the diff will be significant "plugins": ["stylelint-order"], + rules: { + 'no-descending-specificity': null, + 'number-max-precision': 5, + 'value-keyword-case': [ + 'lower', + { + // CSS Color Module Level 3 says currentColor, Level 4 candidate says currentcolor + // Sticking to Level 3 for now + camelCaseSvgKeywords: true, + ignoreKeywords: ['BlinkMacSystemFont'], // BlinkMacSystemFont MUST have this particular casing + }, + ], + // Sadly Safari only started supporting the range syntax of media queries in 2023, so let's switch to that + // ('context' value) in 2024, once support is better https://caniuse.com/?search=range%20context + 'media-feature-range-notation': 'prefix', + 'selector-class-pattern': [ + '^[A-Za-z0-9_-]+(__[A-Za-z0-9_-]+)?(--[A-Za-z0-9-]+)?$', + { + message: 'Expected class selector to match Block__Element--Modifier or plain snake-case', + }, + ], + 'selector-id-pattern': [ + '^[A-Za-z0-9_-]+(__[A-Za-z0-9_-]+)?(--[A-Za-z0-9_-]+)?$', + { + message: 'Expected id selector to match Block__Element--Modifier or plain kebak-case', + }, + ], + 'keyframes-name-pattern': [ + '^[A-Za-z0-9_-]+__[A-Za-z0-9_-]+$', + { + message: 'Expected keyframe name to match Block__Animation', + }, + ], + 'scss/dollar-variable-pattern': [ + '^[A-Za-z_]+[A-Za-z0-9_-]+$', + { + message: 'Expected variable to match kebab-case or snake_case', + }, + ], + 'scss/operator-no-newline-after': null, // Doesn't always play well with prettier + 'scss/at-extend-no-missing-placeholder': null, + 'scss/comment-no-empty': null, + // "order/order": ["dollar-variables", "custom-properties", "declarations", "rules", "at-rules"], + // "order/properties-order": ["width", "height"], + }, +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ed14cbd01cc..4047d14a6106b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # Changelog -Updates to the PostHog project can be found on [https://posthog.com/changelog](our changelog). \ No newline at end of file +Updates to the PostHog project can be found on [our changelog](https://posthog.com/changelog). diff --git a/cypress/e2e/actions.cy.ts b/cypress/e2e/actions.cy.ts index 9819b7d02cdab..356607c64bf8f 100644 --- a/cypress/e2e/actions.cy.ts +++ b/cypress/e2e/actions.cy.ts @@ -5,7 +5,7 @@ const createAction = (actionName: string): void => { cy.get('[data-attr=action-name-create]').should('exist') cy.get('[data-attr=action-name-create]').type(actionName) - cy.get('.ant-radio-group > :nth-child(3)').click() + cy.get('.LemonSegmentedButton > ul > :nth-child(3)').click() cy.get('[data-attr=edit-action-url-input]').click().type(Cypress.config().baseUrl) cy.get('[data-attr=save-action-button]').click() diff --git a/cypress/e2e/billingv2.cy.ts b/cypress/e2e/billingv2.cy.ts index 3c01f489e1f78..5fa21e7b99d2f 100644 --- a/cypress/e2e/billingv2.cy.ts +++ b/cypress/e2e/billingv2.cy.ts @@ -9,12 +9,14 @@ describe('Billing', () => { cy.intercept('/api/billing-v2/deactivate?products=product_analytics', { fixture: 'api/billing-v2/billing-v2-unsubscribed-product-analytics.json', }).as('unsubscribeProductAnalytics') + cy.get('[data-attr=more-button]').first().click() cy.contains('.LemonButton', 'Unsubscribe').click() cy.get('.LemonModal__content h3').should( 'contain', 'Why are you unsubscribing from Product analytics + data stack?' ) + cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Product analytics') cy.contains('.LemonModal .LemonButton', 'Unsubscribe').click() cy.get('.LemonModal').should('not.exist') diff --git a/cypress/e2e/early-access-management.cy.ts b/cypress/e2e/early-access-management.cy.ts index 9a594d8d1c34c..8736a39ab945a 100644 --- a/cypress/e2e/early-access-management.cy.ts +++ b/cypress/e2e/early-access-management.cy.ts @@ -6,7 +6,7 @@ describe('Early Access Management', () => { it('Early access feature new and list', () => { // load an empty early access feature page cy.get('h1').should('contain', 'Early Access Management') - cy.title().should('equal', 'Early Access Management • PostHog') + cy.title().should('equal', 'Early access features • PostHog') cy.get('h2').should('contain', 'Create your first feature') cy.get('[data-attr="product-introduction-docs-link"]').should( 'contain', diff --git a/cypress/e2e/featureFlags.cy.ts b/cypress/e2e/featureFlags.cy.ts index 9045a51a4f485..bf37822321ad1 100644 --- a/cypress/e2e/featureFlags.cy.ts +++ b/cypress/e2e/featureFlags.cy.ts @@ -48,6 +48,7 @@ describe('Feature Flags', () => { cy.get('[data-attr=save-feature-flag]').first().click() // after save there should be a delete button + cy.get('[data-attr="more-button"]').click() cy.get('button[data-attr="delete-feature-flag"]').should('have.text', 'Delete feature flag') // make sure the data is there as expected after a page reload! @@ -83,11 +84,13 @@ describe('Feature Flags', () => { cy.get('[data-attr=save-feature-flag]').first().click() // after save there should be a delete button + cy.get('[data-attr="more-button"]').click() cy.get('button[data-attr="delete-feature-flag"]').should('have.text', 'Delete feature flag') cy.clickNavMenu('featureflags') cy.get('[data-attr=feature-flag-table]').should('contain', name) cy.get(`[data-row-key=${name}]`).contains(name).click() + cy.get('[data-attr="more-button"]').click() cy.get('[data-attr=delete-feature-flag]').click() cy.get('.Toastify').contains('Undo').should('be.visible') }) diff --git a/cypress/e2e/insights-navigation-open-directly.cy.ts b/cypress/e2e/insights-navigation-open-directly.cy.ts index 406445d9f6636..4eec9fcd7aba7 100644 --- a/cypress/e2e/insights-navigation-open-directly.cy.ts +++ b/cypress/e2e/insights-navigation-open-directly.cy.ts @@ -30,7 +30,7 @@ describe('Insights', () => { describe('opening a new insight directly', () => { it('can open a new trends insight', () => { insight.newInsight('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') cy.get('tr').should('have.length.gte', 2) }) @@ -52,12 +52,12 @@ describe('Insights', () => { it('can open a new stickiness insight', () => { insight.newInsight('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') }) it('can open a new lifecycle insight', () => { insight.newInsight('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') }) it('can open a new SQL insight', () => { diff --git a/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts index ca286d2e7c765..2b03880515ece 100644 --- a/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts +++ b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts @@ -42,7 +42,7 @@ describe('Insights', () => { it('can open a new trends insight', () => { insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') cy.get('tr').should('have.length.gte', 2) cy.contains('tr', 'No insight results').should('not.exist') }) @@ -65,12 +65,12 @@ describe('Insights', () => { it('can open a new stickiness insight', () => { insight.clickTab('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') }) it('can open a new lifecycle insight', () => { insight.clickTab('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') }) it('can open a new SQL insight', () => { diff --git a/cypress/e2e/insights-navigation.cy.ts b/cypress/e2e/insights-navigation.cy.ts index 7adf79c284cba..a3526833bee81 100644 --- a/cypress/e2e/insights-navigation.cy.ts +++ b/cypress/e2e/insights-navigation.cy.ts @@ -62,7 +62,7 @@ describe('Insights', () => { cy.get('.DataTable tr').should('have.length.gte', 2) insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') cy.get('tr').should('have.length.gte', 2) cy.contains('tr', 'No insight results').should('not.exist') @@ -73,7 +73,7 @@ describe('Insights', () => { cy.get('.DataTable tr').should('have.length.gte', 2) insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') + cy.get('.TrendsInsight canvas').should('exist') cy.get('tr').should('have.length.gte', 2) cy.contains('tr', 'No insight results').should('not.exist') }) diff --git a/cypress/e2e/insights.cy.ts b/cypress/e2e/insights.cy.ts index 0e449825b2194..80b1c06c4398e 100644 --- a/cypress/e2e/insights.cy.ts +++ b/cypress/e2e/insights.cy.ts @@ -1,7 +1,8 @@ import { urls } from 'scenes/urls' -import { randomString } from '../support/random' + import { decideResponse } from '../fixtures/api/decide' -import { savedInsights, createInsight, insight } from '../productAnalytics' +import { createInsight, insight, savedInsights } from '../productAnalytics' +import { randomString } from '../support/random' // For tests related to trends please check trendsElements.js // insight tests were split up because Cypress was struggling with this many tests in one file🙈 @@ -24,7 +25,7 @@ describe('Insights', () => { cy.get('[data-attr=breadcrumb-0]').should('contain', 'Hogflix') cy.get('[data-attr=breadcrumb-1]').should('contain', 'Hogflix Demo App') - cy.get('[data-attr=breadcrumb-2]').should('have.text', 'Insights') + cy.get('[data-attr=breadcrumb-2]').should('have.text', 'Product analytics') cy.get('[data-attr=breadcrumb-3]').should('have.text', 'insight name') }) diff --git a/docker/clickhouse/config.xml b/docker/clickhouse/config.xml index f3f858be7d117..7047c93e5c5d8 100644 --- a/docker/clickhouse/config.xml +++ b/docker/clickhouse/config.xml @@ -20,17 +20,20 @@ - trace - test (not for production usage) - [1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114 + [1]: + https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114 --> trace /var/log/clickhouse-server/clickhouse-server.log /var/log/clickhouse-server/clickhouse-server.err.log 1000M 10 - + - + @@ -217,7 +225,8 @@ /path/to/ssl_ca_cert_file - none @@ -232,10 +241,12 @@ false - + - + /etc/clickhouse-server/server.crt /etc/clickhouse-server/server.key + true true sslv2,sslv3 @@ -264,24 +276,30 @@ - + 100 0 @@ -302,21 +320,25 @@ --> 0.9 - 4194304 - 0 - @@ -341,14 +363,18 @@ - - - + true @@ -644,14 +698,16 @@ - + localhost 9000 - + @@ -666,22 +722,28 @@ Example: "yandex.ru", "yandex.ru." and "www.yandex.ru" are different hosts. If port is explicitly specified in URL, the host:port is checked as a whole. If host specified here without port, any port with this host allowed. - "yandex.ru" -> "yandex.ru:443", "yandex.ru:80" etc. is allowed, but "yandex.ru:80" -> only "yandex.ru:80" is allowed. - If the host is specified as IP address, it is checked as specified in URL. Example: "[2a02:6b8:a::a]". - If there are redirects and support for redirects is enabled, every redirect (the Location field) is checked. + "yandex.ru" -> "yandex.ru:443", "yandex.ru:80" etc. is allowed, but "yandex.ru:80" -> only + "yandex.ru:80" is allowed. + If the host is specified as IP address, it is checked as specified in URL. Example: + "[2a02:6b8:a::a]". + If there are redirects and support for redirects is enabled, every redirect (the Location field) is + checked. Host should be specified using the host xml tag: yandex.ru --> .* - @@ -701,7 +763,8 @@ @@ -710,7 +773,6 @@ - 3600 @@ -788,7 +850,8 @@ system query_log
toYYYYMM(event_date) - @@ -843,7 +909,8 @@ + Part log contains information about all actions with parts in MergeTree tables (creation, deletion, + merges, downloads).--> system part_log
@@ -852,8 +919,10 @@
- + system metric_log
@@ -933,7 +1003,8 @@ --> - + @@ -965,12 +1036,14 @@ --> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -1032,7 +1107,8 @@ - + /var/lib/clickhouse/format_schemas/ - false - + false - + https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 @@ -1183,4 +1267,4 @@ --> - + \ No newline at end of file diff --git a/docker/clickhouse/users-dev.xml b/docker/clickhouse/users-dev.xml index dd6e54d7c5de3..704e99ef9e961 100644 --- a/docker/clickhouse/users-dev.xml +++ b/docker/clickhouse/users-dev.xml @@ -15,7 +15,8 @@ with minimum number of different symbols between replica's hostname and local hostname (Hamming distance). in_order - first live replica is chosen in specified order. - first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors. + first_or_random - if first replica one has higher number of errors, pick a random one from replicas + with minimum number of errors. --> random @@ -45,30 +46,39 @@ Password could be empty. If you want to specify SHA256, place it in 'password_sha256_hex' element. - Example: 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 - Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019). + Example: + 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 + Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July + 2019). If you want to specify double SHA1, place it in 'password_double_sha1_hex' element. - Example: e395796d6546b1b65db9d665cd43f0e858dd4303 + Example: + e395796d6546b1b65db9d665cd43f0e858dd4303 - If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication, + If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for + authentication, place its name in 'server' element inside 'ldap' element. Example: my_ldap_server - If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config), + If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in + the main config), place 'kerberos' element instead of 'password' (and similar) elements. - The name part of the canonical principal name of the initiator must match the user name for authentication to succeed. - You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests + The name part of the canonical principal name of the initiator must match the user name for + authentication to succeed. + You can also place 'realm' element inside 'kerberos' element to further restrict authentication to + only those requests whose initiator's realm matches it. Example: Example: EXAMPLE.COM How to generate decent password: - Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-' + Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | + sha256sum | tr -d '-' In first line will be password and in second - corresponding SHA256. How to generate double SHA1: - Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-' + Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | + sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-' In first line will be password and in second - corresponding double SHA1. --> @@ -89,7 +99,8 @@ To check access, DNS query is performed, and all received addresses compared to peer address. Regular expression for host names. Example, ^server\d\d-\d\d-\d\.yandex\.ru$ To check access, DNS PTR query is performed for peer address and then regexp is applied. - Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address. + Then, for result of PTR query, another DNS query is performed and all received addresses compared + to peer address. Strongly recommended that regexp is ends with $ All results of DNS requests are cached till server restart. --> @@ -126,4 +137,4 @@ - + \ No newline at end of file diff --git a/docker/clickhouse/users.xml b/docker/clickhouse/users.xml index 49ac9f73e0de5..ece3df0f09fbe 100644 --- a/docker/clickhouse/users.xml +++ b/docker/clickhouse/users.xml @@ -15,7 +15,8 @@ with minimum number of different symbols between replica's hostname and local hostname (Hamming distance). in_order - first live replica is chosen in specified order. - first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors. + first_or_random - if first replica one has higher number of errors, pick a random one from replicas + with minimum number of errors. --> random @@ -43,30 +44,39 @@ Password could be empty. If you want to specify SHA256, place it in 'password_sha256_hex' element. - Example: 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 - Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019). + Example: + 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 + Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July + 2019). If you want to specify double SHA1, place it in 'password_double_sha1_hex' element. - Example: e395796d6546b1b65db9d665cd43f0e858dd4303 + Example: + e395796d6546b1b65db9d665cd43f0e858dd4303 - If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication, + If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for + authentication, place its name in 'server' element inside 'ldap' element. Example: my_ldap_server - If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config), + If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in + the main config), place 'kerberos' element instead of 'password' (and similar) elements. - The name part of the canonical principal name of the initiator must match the user name for authentication to succeed. - You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests + The name part of the canonical principal name of the initiator must match the user name for + authentication to succeed. + You can also place 'realm' element inside 'kerberos' element to further restrict authentication to + only those requests whose initiator's realm matches it. Example: Example: EXAMPLE.COM How to generate decent password: - Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-' + Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | + sha256sum | tr -d '-' In first line will be password and in second - corresponding SHA256. How to generate double SHA1: - Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-' + Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | + sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-' In first line will be password and in second - corresponding double SHA1. --> @@ -87,7 +97,8 @@ To check access, DNS query is performed, and all received addresses compared to peer address. Regular expression for host names. Example, ^server\d\d-\d\d-\d\.yandex\.ru$ To check access, DNS PTR query is performed for peer address and then regexp is applied. - Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address. + Then, for result of PTR query, another DNS query is performed and all received addresses compared + to peer address. Strongly recommended that regexp is ends with $ All results of DNS requests are cached till server restart. --> @@ -124,4 +135,4 @@ - + \ No newline at end of file diff --git a/ee/api/feature_flag_role_access.py b/ee/api/feature_flag_role_access.py index d3ca7a68c1a32..3ce77dca89599 100644 --- a/ee/api/feature_flag_role_access.py +++ b/ee/api/feature_flag_role_access.py @@ -35,7 +35,11 @@ def has_permission(self, request, view): return True try: feature_flag: FeatureFlag = FeatureFlag.objects.get(id=view.parents_query_dict["feature_flag_id"]) - if feature_flag.created_by.uuid == request.user.uuid: + if ( + hasattr(feature_flag, "created_by") + and feature_flag.created_by + and feature_flag.created_by.uuid == request.user.uuid + ): return True except FeatureFlag.DoesNotExist: raise exceptions.NotFound("Feature flag not found.") diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index 87838d0b39dcc..c37c3ee9d6482 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -2,9 +2,9 @@ from typing import Any, Dict, List from unittest.mock import MagicMock, patch from uuid import uuid4 +from zoneinfo import ZoneInfo import jwt -from zoneinfo import ZoneInfo from dateutil.relativedelta import relativedelta from django.utils.timezone import now from freezegun import freeze_time @@ -43,6 +43,7 @@ def create_missing_billing_customer(**kwargs) -> CustomerInfo: usage_summary={ "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, free_trial_until=None, available_features=[], @@ -96,6 +97,7 @@ def create_billing_customer(**kwargs) -> CustomerInfo: usage_summary={ "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, free_trial_until=None, ) @@ -292,6 +294,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "usage_summary": { "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, "free_trial_until": None, } @@ -363,6 +366,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "usage_summary": { "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, "free_trial_until": None, "current_total_amount_usd": "0.00", @@ -521,6 +525,11 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "todays_usage": 0, "usage": 0, }, + "rows_synced": { + "limit": None, + "todays_usage": 0, + "usage": 0, + }, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } @@ -556,6 +565,11 @@ def mock_implementation_missing_customer(url: str, headers: Any = None, params: "todays_usage": 0, "usage": 0, }, + "rows_synced": { + "limit": None, + "todays_usage": 0, + "usage": 0, + }, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } assert self.organization.customer_id == "cus_123" @@ -613,5 +627,6 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma assert self.organization.usage == { "events": {"limit": None, "usage": 0, "todays_usage": 0}, "recordings": {"limit": None, "usage": 0, "todays_usage": 0}, + "rows_synced": {"limit": None, "usage": 0, "todays_usage": 0}, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } diff --git a/ee/api/test/test_feature_flag_role_access.py b/ee/api/test/test_feature_flag_role_access.py index f143f10505f0f..3cd4e947d90c9 100644 --- a/ee/api/test/test_feature_flag_role_access.py +++ b/ee/api/test/test_feature_flag_role_access.py @@ -37,6 +37,26 @@ def test_can_always_add_role_access_if_creator_of_feature_flag(self): self.assertEqual(flag_role.role.name, self.eng_role.name) self.assertEqual(flag_role.feature_flag.id, self.feature_flag.id) + def test_role_access_with_deleted_creator_of_feature_flag(self): + OrganizationResourceAccess.objects.create( + resource=OrganizationResourceAccess.Resources.FEATURE_FLAGS, + access_level=OrganizationResourceAccess.AccessLevel.CAN_ONLY_VIEW, + organization=self.organization, + ) + + flag = FeatureFlag.objects.create( + created_by=None, + team=self.team, + key="flag_role_access_none", + name="Flag role access", + ) + self.assertEqual(self.user.role_memberships.count(), 0) + flag_role_access_create_res = self.client.post( + f"/api/projects/@current/feature_flags/{flag.id}/role_access", + {"role_id": self.eng_role.id}, + ) + self.assertEqual(flag_role_access_create_res.status_code, status.HTTP_403_FORBIDDEN) + def test_cannot_add_role_access_if_feature_flags_access_level_too_low_and_not_creator(self): OrganizationResourceAccess.objects.create( resource=OrganizationResourceAccess.Resources.FEATURE_FLAGS, diff --git a/ee/api/test/test_organization.py b/ee/api/test/test_organization.py index 2f1b11bb95256..a77361dc579e8 100644 --- a/ee/api/test/test_organization.py +++ b/ee/api/test/test_organization.py @@ -1,6 +1,7 @@ import datetime as dt import random -from unittest.mock import ANY, patch +from unittest import mock +from unittest.mock import ANY, call, patch from freezegun.api import freeze_time from rest_framework import status @@ -104,11 +105,21 @@ def test_delete_last_organization(self, mock_capture): "Did not return a 404 on trying to delete a nonexistent org", ) - mock_capture.assert_called_once_with( - self.user.distinct_id, - "organization deleted", - organization_props, - groups={"instance": ANY, "organization": str(org_id)}, + mock_capture.assert_has_calls( + [ + call( + self.user.distinct_id, + "membership level changed", + properties={"new_level": 15, "previous_level": 1}, + groups=mock.ANY, + ), + call( + self.user.distinct_id, + "organization deleted", + organization_props, + groups={"instance": mock.ANY, "organization": str(org_id)}, + ), + ] ) def test_no_delete_organization_not_owning(self): diff --git a/ee/billing/billing_manager.py b/ee/billing/billing_manager.py index c626083460ef4..324b158fe071d 100644 --- a/ee/billing/billing_manager.py +++ b/ee/billing/billing_manager.py @@ -6,6 +6,7 @@ import structlog from django.utils import timezone from rest_framework.exceptions import NotAuthenticated +from sentry_sdk import capture_exception from ee.billing.billing_types import BillingStatus from ee.billing.quota_limiting import set_org_usage_summary, sync_org_quota_limits @@ -13,7 +14,7 @@ from ee.settings import BILLING_SERVICE_URL from posthog.cloud_utils import get_cached_instance_license from posthog.models import Organization -from posthog.models.organization import OrganizationUsageInfo +from posthog.models.organization import OrganizationMembership, OrganizationUsageInfo logger = structlog.get_logger(__name__) @@ -114,6 +115,14 @@ def update_billing_distinct_ids(self, organization: Organization) -> None: distinct_ids = list(organization.members.values_list("distinct_id", flat=True)) self.update_billing(organization, {"distinct_ids": distinct_ids}) + def update_billing_customer_email(self, organization: Organization) -> None: + try: + owner_membership = OrganizationMembership.objects.get(organization=organization, level=15) + user = owner_membership.user + self.update_billing(organization, {"org_customer_email": user.email}) + except Exception as e: + capture_exception(e) + def deactivate_products(self, organization: Organization, products: str) -> None: res = requests.get( f"{BILLING_SERVICE_URL}/api/billing/deactivate?products={products}", @@ -225,6 +234,7 @@ def update_org_details(self, organization: Organization, billing_status: Billing usage_info = OrganizationUsageInfo( events=usage_summary["events"], recordings=usage_summary["recordings"], + rows_synced=usage_summary.get("rows_synced", None), period=[ data["billing_period"]["current_period_start"], data["billing_period"]["current_period_end"], diff --git a/ee/billing/quota_limiting.py b/ee/billing/quota_limiting.py index ae6eefcc0b77a..ef3e12a421575 100644 --- a/ee/billing/quota_limiting.py +++ b/ee/billing/quota_limiting.py @@ -17,6 +17,7 @@ convert_team_usage_rows_to_dict, get_teams_with_billable_event_count_in_period, get_teams_with_recording_count_in_period, + get_teams_with_rows_synced_in_period, ) from posthog.utils import get_current_day @@ -26,11 +27,13 @@ class QuotaResource(Enum): EVENTS = "events" RECORDINGS = "recordings" + ROWS_SYNCED = "rows_synced" OVERAGE_BUFFER = { QuotaResource.EVENTS: 0, QuotaResource.RECORDINGS: 1000, + QuotaResource.ROWS_SYNCED: 0, } @@ -53,7 +56,7 @@ def remove_limited_team_tokens(resource: QuotaResource, tokens: List[str]) -> No @cache_for(timedelta(seconds=30), background_refresh=True) -def list_limited_team_tokens(resource: QuotaResource) -> List[str]: +def list_limited_team_attributes(resource: QuotaResource) -> List[str]: now = timezone.now() redis_client = get_client() results = redis_client.zrangebyscore(f"{QUOTA_LIMITER_CACHE_KEY}{resource.value}", min=now.timestamp(), max="+inf") @@ -63,6 +66,7 @@ def list_limited_team_tokens(resource: QuotaResource) -> List[str]: class UsageCounters(TypedDict): events: int recordings: int + rows_synced: int def org_quota_limited_until(organization: Organization, resource: QuotaResource) -> Optional[int]: @@ -70,6 +74,8 @@ def org_quota_limited_until(organization: Organization, resource: QuotaResource) return None summary = organization.usage.get(resource.value, {}) + if not summary: + return None usage = summary.get("usage", 0) todays_usage = summary.get("todays_usage", 0) limit = summary.get("limit") @@ -93,19 +99,34 @@ def sync_org_quota_limits(organization: Organization): if not organization.usage: return None - team_tokens: List[str] = [x for x in list(organization.teams.values_list("api_token", flat=True)) if x] - - if not team_tokens: - capture_exception(Exception(f"quota_limiting: No team tokens found for organization: {organization.id}")) - return - - for resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS]: + for resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS, QuotaResource.ROWS_SYNCED]: + team_attributes = get_team_attribute_by_quota_resource(organization, resource) quota_limited_until = org_quota_limited_until(organization, resource) if quota_limited_until: - add_limited_team_tokens(resource, {x: quota_limited_until for x in team_tokens}) + add_limited_team_tokens(resource, {x: quota_limited_until for x in team_attributes}) else: - remove_limited_team_tokens(resource, team_tokens) + remove_limited_team_tokens(resource, team_attributes) + + +def get_team_attribute_by_quota_resource(organization: Organization, resource: QuotaResource): + if resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS]: + team_tokens: List[str] = [x for x in list(organization.teams.values_list("api_token", flat=True)) if x] + + if not team_tokens: + capture_exception(Exception(f"quota_limiting: No team tokens found for organization: {organization.id}")) + return + + return team_tokens + + if resource == QuotaResource.ROWS_SYNCED: + team_ids: List[str] = [x for x in list(organization.teams.values_list("id", flat=True)) if x] + + if not team_ids: + capture_exception(Exception(f"quota_limiting: No team ids found for organization: {organization.id}")) + return + + return team_ids def set_org_usage_summary( @@ -125,8 +146,10 @@ def set_org_usage_summary( new_usage = copy.deepcopy(new_usage) - for field in ["events", "recordings"]: + for field in ["events", "recordings", "rows_synced"]: resource_usage = new_usage[field] # type: ignore + if not resource_usage: + continue if todays_usage: resource_usage["todays_usage"] = todays_usage[field] # type: ignore @@ -155,6 +178,9 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, teams_with_recording_count_in_period=convert_team_usage_rows_to_dict( get_teams_with_recording_count_in_period(period_start, period_end) ), + teams_with_rows_synced_in_period=convert_team_usage_rows_to_dict( + get_teams_with_rows_synced_in_period(period_start, period_end) + ), ) teams: Sequence[Team] = list( @@ -171,6 +197,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, team_report = UsageCounters( events=all_data["teams_with_event_count_in_period"].get(team.id, 0), recordings=all_data["teams_with_recording_count_in_period"].get(team.id, 0), + rows_synced=all_data["teams_with_rows_synced_in_period"].get(team.id, 0), ) org_id = str(team.organization.id) @@ -183,7 +210,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, for field in team_report: org_report[field] += team_report[field] # type: ignore - quota_limited_orgs: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}} + quota_limited_orgs: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}, "rows_synced": {}} # We find all orgs that should be rate limited for org_id, todays_report in todays_usage_report.items(): @@ -195,7 +222,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, if set_org_usage_summary(org, todays_usage=todays_report): org.save(update_fields=["usage"]) - for field in ["events", "recordings"]: + for field in ["events", "recordings", "rows_synced"]: quota_limited_until = org_quota_limited_until(org, QuotaResource(field)) if quota_limited_until: @@ -207,12 +234,13 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, previously_quota_limited_team_tokens: Dict[str, Dict[str, int]] = { "events": {}, "recordings": {}, + "rows_synced": {}, } for field in quota_limited_orgs: - previously_quota_limited_team_tokens[field] = list_limited_team_tokens(QuotaResource(field)) + previously_quota_limited_team_tokens[field] = list_limited_team_attributes(QuotaResource(field)) - quota_limited_teams: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}} + quota_limited_teams: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}, "rows_synced": {}} # Convert the org ids to team tokens for team in teams: @@ -233,6 +261,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, properties = { "quota_limited_events": quota_limited_orgs["events"].get(org_id, None), "quota_limited_recordings": quota_limited_orgs["events"].get(org_id, None), + "quota_limited_rows_synced": quota_limited_orgs["rows_synced"].get(org_id, None), } report_organization_action( diff --git a/ee/billing/test/test_billing_manager.py b/ee/billing/test/test_billing_manager.py index e0c09e0d071fb..1dbbcb464f068 100644 --- a/ee/billing/test/test_billing_manager.py +++ b/ee/billing/test/test_billing_manager.py @@ -33,3 +33,26 @@ def test_update_billing_distinct_ids(self, billing_patch_request_mock: MagicMock BillingManager(license).update_billing_distinct_ids(organization) assert billing_patch_request_mock.call_count == 1 assert len(billing_patch_request_mock.call_args[1]["json"]["distinct_ids"]) == 2 + + @patch( + "ee.billing.billing_manager.requests.patch", + return_value=MagicMock(status_code=200, json=MagicMock(return_value={"text": "ok"})), + ) + def test_update_billing_customer_email(self, billing_patch_request_mock: MagicMock): + organization = self.organization + license = super(LicenseManager, cast(LicenseManager, License.objects)).create( + key="key123::key123", + plan="enterprise", + valid_until=timezone.datetime(2038, 1, 19, 3, 14, 7), + ) + User.objects.create_and_join( + organization=organization, + email="y@x.com", + password=None, + level=OrganizationMembership.Level.OWNER, + ) + organization.refresh_from_db() + assert len(organization.members.values_list("distinct_id", flat=True)) == 2 # one exists in the test base + BillingManager(license).update_billing_customer_email(organization) + assert billing_patch_request_mock.call_count == 1 + assert billing_patch_request_mock.call_args[1]["json"]["org_customer_email"] == "y@x.com" diff --git a/ee/billing/test/test_quota_limiting.py b/ee/billing/test/test_quota_limiting.py index 3bdc70a06df9e..b8e68c235b2c5 100644 --- a/ee/billing/test/test_quota_limiting.py +++ b/ee/billing/test/test_quota_limiting.py @@ -9,7 +9,7 @@ from ee.billing.quota_limiting import ( QUOTA_LIMITER_CACHE_KEY, QuotaResource, - list_limited_team_tokens, + list_limited_team_attributes, org_quota_limited_until, replace_limited_team_tokens, set_org_usage_summary, @@ -47,15 +47,18 @@ def test_billing_rate_limit_not_set_if_missing_org_usage(self) -> None: result = update_all_org_billing_quotas() assert result["events"] == {} assert result["recordings"] == {} + assert result["rows_synced"] == {} assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}events", 0, -1) == [] assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}recordings", 0, -1) == [] + assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}rows_synced", 0, -1) == [] def test_billing_rate_limit(self) -> None: with self.settings(USE_TZ=False): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 5, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -77,16 +80,19 @@ def test_billing_rate_limit(self) -> None: org_id = str(self.organization.id) assert result["events"] == {org_id: 1612137599} assert result["recordings"] == {} + assert result["rows_synced"] == {} assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}events", 0, -1) == [ self.team.api_token.encode("UTF-8") ] assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}recordings", 0, -1) == [] + assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}rows_synced", 0, -1) == [] self.organization.refresh_from_db() assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 0}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 0}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -94,6 +100,7 @@ def test_set_org_usage_summary_updates_correctly(self): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 5, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -101,6 +108,7 @@ def test_set_org_usage_summary_updates_correctly(self): new_usage = dict( events={"usage": 100, "limit": 100}, recordings={"usage": 2, "limit": 100}, + rows_synced={"usage": 6, "limit": 100}, period=[ "2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z", @@ -112,6 +120,7 @@ def test_set_org_usage_summary_updates_correctly(self): assert self.organization.usage == { "events": {"usage": 100, "limit": 100, "todays_usage": 0}, "recordings": {"usage": 2, "limit": 100, "todays_usage": 0}, + "rows_synced": {"usage": 6, "limit": 100, "todays_usage": 0}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -119,6 +128,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): self.organization.usage = { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -126,6 +136,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): new_usage = dict( events={"usage": 99, "limit": 100}, recordings={"usage": 1, "limit": 100}, + rows_synced={"usage": 5, "limit": 100}, period=[ "2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z", @@ -137,6 +148,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -144,15 +156,19 @@ def test_set_org_usage_summary_updates_todays_usage(self): self.organization.usage = { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() - assert set_org_usage_summary(self.organization, todays_usage={"events": 20, "recordings": 21}) + assert set_org_usage_summary( + self.organization, todays_usage={"events": 20, "recordings": 21, "rows_synced": 21} + ) assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 20}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 21}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 21}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -163,6 +179,7 @@ def test_org_quota_limited_until(self): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 99, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -184,6 +201,11 @@ def test_org_quota_limited_until(self): self.organization.usage["recordings"]["usage"] = 1100 # Over limit + buffer assert org_quota_limited_until(self.organization, QuotaResource.RECORDINGS) == 1612137599 + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) is None + + self.organization.usage["rows_synced"]["usage"] = 101 + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) == 1612137599 + def test_over_quota_but_not_dropped_org(self): self.organization.usage = None assert org_quota_limited_until(self.organization, QuotaResource.EVENTS) is None @@ -191,12 +213,14 @@ def test_over_quota_but_not_dropped_org(self): self.organization.usage = { "events": {"usage": 100, "limit": 90}, "recordings": {"usage": 100, "limit": 90}, + "rows_synced": {"usage": 100, "limit": 90}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.never_drop_data = True assert org_quota_limited_until(self.organization, QuotaResource.EVENTS) is None assert org_quota_limited_until(self.organization, QuotaResource.RECORDINGS) is None + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) is None # reset for subsequent tests self.organization.never_drop_data = False @@ -208,21 +232,32 @@ def test_sync_org_quota_limits(self): now = timezone.now().timestamp() replace_limited_team_tokens(QuotaResource.EVENTS, {"1234": now + 10000}) + replace_limited_team_tokens(QuotaResource.ROWS_SYNCED, {"1337": now + 10000}) self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 35, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } sync_org_quota_limits(self.organization) - assert list_limited_team_tokens(QuotaResource.EVENTS) == ["1234"] + assert list_limited_team_attributes(QuotaResource.EVENTS) == ["1234"] + assert list_limited_team_attributes(QuotaResource.ROWS_SYNCED) == ["1337"] self.organization.usage["events"]["usage"] = 120 + self.organization.usage["rows_synced"]["usage"] = 120 sync_org_quota_limits(self.organization) - assert sorted(list_limited_team_tokens(QuotaResource.EVENTS)) == sorted( + assert sorted(list_limited_team_attributes(QuotaResource.EVENTS)) == sorted( ["1234", self.team.api_token, other_team.api_token] ) + # rows_synced uses teams, not tokens + assert sorted(list_limited_team_attributes(QuotaResource.ROWS_SYNCED)) == sorted( + ["1337", str(self.team.pk), str(other_team.pk)] + ) + self.organization.usage["events"]["usage"] = 80 + self.organization.usage["rows_synced"]["usage"] = 36 sync_org_quota_limits(self.organization) - assert sorted(list_limited_team_tokens(QuotaResource.EVENTS)) == sorted(["1234"]) + assert sorted(list_limited_team_attributes(QuotaResource.EVENTS)) == sorted(["1234"]) + assert sorted(list_limited_team_attributes(QuotaResource.ROWS_SYNCED)) == sorted(["1337"]) diff --git a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr index 29eb93b4ae929..5acb951590251 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr @@ -352,7 +352,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), '%example%'), 1)) + AND (and(ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), '%example%'), 0), 1)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -426,7 +426,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like(nullIf(nullIf(events.`mat_$current_url`, ''), 'null'), '%example%'), 1)) + AND (and(ifNull(like(nullIf(nullIf(events.`mat_$current_url`, ''), 'null'), '%example%'), 0), 1)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -501,7 +501,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'email'), ''), 'null'), '^"|"$', ''), '%test.com')) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'email'), ''), 'null'), '^"|"$', ''), '%test.com'), 0)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -576,7 +576,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like(nullIf(nullIf(mat_pp_email, ''), 'null'), '%test.com')) + AND (ifNull(like(nullIf(nullIf(mat_pp_email, ''), 'null'), '%test.com'), 0)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) diff --git a/ee/clickhouse/test/test_client.py b/ee/clickhouse/test/test_client.py deleted file mode 100644 index ab5ba1b4a53e0..0000000000000 --- a/ee/clickhouse/test/test_client.py +++ /dev/null @@ -1,129 +0,0 @@ -from unittest.mock import patch - -import fakeredis -from clickhouse_driver.errors import ServerException -from django.test import TestCase - -from posthog.clickhouse.client import execute_async as client -from posthog.client import sync_execute -from posthog.test.base import ClickhouseTestMixin - - -class ClickhouseClientTestCase(TestCase, ClickhouseTestMixin): - def setUp(self): - self.redis_client = fakeredis.FakeStrictRedis() - - def test_async_query_client(self): - query = "SELECT 1+1" - team_id = 2 - query_id = client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - result = client.get_status_or_results(team_id, query_id) - self.assertFalse(result.error) - self.assertTrue(result.complete) - self.assertEqual(result.results, [[2]]) - - def test_async_query_client_errors(self): - query = "SELECT WOW SUCH DATA FROM NOWHERE THIS WILL CERTAINLY WORK" - team_id = 2 - self.assertRaises( - ServerException, - client.enqueue_execute_with_progress, - **{"team_id": team_id, "query": query, "bypass_celery": True}, - ) - try: - query_id = client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - except Exception: - pass - - result = client.get_status_or_results(team_id, query_id) - self.assertTrue(result.error) - self.assertRegex(result.error_message, "Code: 62.\nDB::Exception: Syntax error:") - - def test_async_query_client_does_not_leak(self): - query = "SELECT 1+1" - team_id = 2 - wrong_team = 5 - query_id = client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - result = client.get_status_or_results(wrong_team, query_id) - self.assertTrue(result.error) - self.assertEqual(result.error_message, "Requesting team is not executing team") - - @patch("posthog.clickhouse.client.execute_async.enqueue_clickhouse_execute_with_progress") - def test_async_query_client_is_lazy(self, execute_sync_mock): - query = "SELECT 4 + 4" - team_id = 2 - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - - # Try the same query again - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - - # Try the same query again (for good measure!) - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - - # Assert that we only called clickhouse once - execute_sync_mock.assert_called_once() - - @patch("posthog.clickhouse.client.execute_async.enqueue_clickhouse_execute_with_progress") - def test_async_query_client_is_lazy_but_not_too_lazy(self, execute_sync_mock): - query = "SELECT 8 + 8" - team_id = 2 - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - - # Try the same query again, but with force - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True, force=True) - - # Try the same query again (for good measure!) - client.enqueue_execute_with_progress(team_id, query, bypass_celery=True) - - # Assert that we called clickhouse twice - self.assertEqual(execute_sync_mock.call_count, 2) - - @patch("posthog.clickhouse.client.execute_async.enqueue_clickhouse_execute_with_progress") - def test_async_query_client_manual_query_uuid(self, execute_sync_mock): - # This is a unique test because technically in the test pattern `SELECT 8 + 8` is already - # in redis. This tests to make sure it is treated as a unique run of that query - query = "SELECT 8 + 8" - team_id = 2 - query_id = "I'm so unique" - client.enqueue_execute_with_progress(team_id, query, query_id=query_id, bypass_celery=True) - - # Try the same query again, but with force - client.enqueue_execute_with_progress(team_id, query, query_id=query_id, bypass_celery=True, force=True) - - # Try the same query again (for good measure!) - client.enqueue_execute_with_progress(team_id, query, query_id=query_id, bypass_celery=True) - - # Assert that we called clickhouse twice - self.assertEqual(execute_sync_mock.call_count, 2) - - def test_client_strips_comments_from_request(self): - """ - To ensure we can easily copy queries from `system.query_log` in e.g. - Metabase, we strip comments from the query we send. Metabase doesn't - display multilined output. - - See https://github.com/metabase/metabase/issues/14253 - - Note I'm not really testing much complexity, I trust that those will - come out as failures in other tests. - """ - from posthog.clickhouse.query_tagging import tag_queries - - # First add in the request information that should be added to the sql. - # We check this to make sure it is not removed by the comment stripping - with self.capture_select_queries() as sqls: - tag_queries(kind="request", id="1") - sync_execute( - query=""" - -- this request returns 1 - SELECT 1 - """ - ) - self.assertEqual(len(sqls), 1) - first_query = sqls[0] - self.assertIn(f"SELECT 1", first_query) - self.assertNotIn("this request returns", first_query) - - # Make sure it still includes the "annotation" comment that includes - # request routing information for debugging purposes - self.assertIn("/* request:1 */", first_query) diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr index f039a2994204e..3474ae77b858f 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -1,6 +1,6 @@ # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results ' - /* user_id:126 celery:posthog.celery.sync_insight_caching_state */ + /* user_id:131 celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, date_diff('second', max(timestamp), now()) AS age FROM events diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr index d185a7a063790..09cddbe7f6756 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr @@ -1613,8 +1613,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') AND ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) - and isNull('true')))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), 0))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 @@ -1655,8 +1654,7 @@ WHERE e.team_id = 2 AND event = '$pageview' AND ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) - and isNull('true')))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), 0))) AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') AND replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) diff --git a/ee/clickhouse/views/test/test_clickhouse_trends.py b/ee/clickhouse/views/test/test_clickhouse_trends.py index 75ab015e39a15..8bf86c1524006 100644 --- a/ee/clickhouse/views/test/test_clickhouse_trends.py +++ b/ee/clickhouse/views/test/test_clickhouse_trends.py @@ -118,7 +118,7 @@ def test_includes_only_intervals_within_range(client: Client): { "action": ANY, "breakdown_value": cohort["id"], - "label": "$pageview - test cohort", + "label": "test cohort", "count": 3.0, "data": [1.0, 1.0, 1.0], # Prior to the fix this would also include '29-Aug-2021' @@ -827,14 +827,12 @@ def test_insight_trends_cumulative(self): ], ) data_response = get_trends_time_series_ok(self.client, request, self.team) - person_response = get_people_from_url_ok( - self.client, data_response["$pageview - val"]["2012-01-14"].person_url - ) + person_response = get_people_from_url_ok(self.client, data_response["val"]["2012-01-14"].person_url) - assert data_response["$pageview - val"]["2012-01-13"].value == 1 - assert data_response["$pageview - val"]["2012-01-13"].breakdown_value == "val" - assert data_response["$pageview - val"]["2012-01-14"].value == 3 - assert data_response["$pageview - val"]["2012-01-14"].label == "14-Jan-2012" + assert data_response["val"]["2012-01-13"].value == 1 + assert data_response["val"]["2012-01-13"].breakdown_value == "val" + assert data_response["val"]["2012-01-14"].value == 3 + assert data_response["val"]["2012-01-14"].label == "14-Jan-2012" assert sorted([p["id"] for p in person_response]) == sorted( [str(created_people["p1"].uuid), str(created_people["p3"].uuid)] @@ -862,12 +860,12 @@ def test_insight_trends_cumulative(self): properties=[{"type": "person", "key": "key", "value": "some_val"}], ) data_response = get_trends_time_series_ok(self.client, request, self.team) - people = get_people_from_url_ok(self.client, data_response["$pageview - val"]["2012-01-14"].person_url) + people = get_people_from_url_ok(self.client, data_response["val"]["2012-01-14"].person_url) - assert data_response["$pageview - val"]["2012-01-13"].value == 1 - assert data_response["$pageview - val"]["2012-01-13"].breakdown_value == "val" - assert data_response["$pageview - val"]["2012-01-14"].value == 3 - assert data_response["$pageview - val"]["2012-01-14"].label == "14-Jan-2012" + assert data_response["val"]["2012-01-13"].value == 1 + assert data_response["val"]["2012-01-13"].breakdown_value == "val" + assert data_response["val"]["2012-01-14"].value == 3 + assert data_response["val"]["2012-01-14"].label == "14-Jan-2012" assert sorted([p["id"] for p in people]) == sorted( [str(created_people["p1"].uuid), str(created_people["p3"].uuid)] @@ -894,12 +892,12 @@ def test_insight_trends_cumulative(self): ], ) data_response = get_trends_time_series_ok(self.client, request, self.team) - people = get_people_from_url_ok(self.client, data_response["$pageview - val"]["2012-01-14"].person_url) + people = get_people_from_url_ok(self.client, data_response["val"]["2012-01-14"].person_url) - assert data_response["$pageview - val"]["2012-01-13"].value == 1 - assert data_response["$pageview - val"]["2012-01-13"].breakdown_value == "val" - assert data_response["$pageview - val"]["2012-01-14"].value == 2 - assert data_response["$pageview - val"]["2012-01-14"].label == "14-Jan-2012" + assert data_response["val"]["2012-01-13"].value == 1 + assert data_response["val"]["2012-01-13"].breakdown_value == "val" + assert data_response["val"]["2012-01-14"].value == 2 + assert data_response["val"]["2012-01-14"].label == "14-Jan-2012" assert sorted([p["id"] for p in people]) == sorted( [str(created_people["p1"].uuid), str(created_people["p3"].uuid)] @@ -933,12 +931,10 @@ def test_breakdown_with_filter(self): properties=[{"key": "key", "value": "oh", "operator": "not_icontains"}], ) data_response = get_trends_time_series_ok(self.client, params, self.team) - person_response = get_people_from_url_ok( - self.client, data_response["sign up - val"]["2012-01-13"].person_url - ) + person_response = get_people_from_url_ok(self.client, data_response["val"]["2012-01-13"].person_url) - assert data_response["sign up - val"]["2012-01-13"].value == 1 - assert data_response["sign up - val"]["2012-01-13"].breakdown_value == "val" + assert data_response["val"]["2012-01-13"].value == 1 + assert data_response["val"]["2012-01-13"].breakdown_value == "val" assert sorted([p["id"] for p in person_response]) == sorted([str(created_people["person1"].uuid)]) @@ -950,11 +946,9 @@ def test_breakdown_with_filter(self): events=[{"id": "sign up", "name": "sign up", "type": "events", "order": 0}], ) aggregate_response = get_trends_aggregate_ok(self.client, params, self.team) - aggregate_person_response = get_people_from_url_ok( - self.client, aggregate_response["sign up - val"].person_url - ) + aggregate_person_response = get_people_from_url_ok(self.client, aggregate_response["val"].person_url) - assert aggregate_response["sign up - val"].value == 1 + assert aggregate_response["val"].value == 1 assert sorted([p["id"] for p in aggregate_person_response]) == sorted([str(created_people["person1"].uuid)]) def test_insight_trends_compare(self): diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card.png b/frontend/__snapshots__/components-cards-insight-card--insight-card.png index 3c8919af6eeb8..eaa0ed7df25ed 100644 Binary files a/frontend/__snapshots__/components-cards-insight-card--insight-card.png and b/frontend/__snapshots__/components-cards-insight-card--insight-card.png differ diff --git a/frontend/__snapshots__/components-cards-text-card--template.png b/frontend/__snapshots__/components-cards-text-card--template.png index 4eee8836ab698..3c045b8354ba6 100644 Binary files a/frontend/__snapshots__/components-cards-text-card--template.png and b/frontend/__snapshots__/components-cards-text-card--template.png differ diff --git a/frontend/__snapshots__/components-command-bar--actions.png b/frontend/__snapshots__/components-command-bar--actions.png new file mode 100644 index 0000000000000..a1c3b448c6a2e Binary files /dev/null and b/frontend/__snapshots__/components-command-bar--actions.png differ diff --git a/frontend/__snapshots__/components-command-bar--search.png b/frontend/__snapshots__/components-command-bar--search.png new file mode 100644 index 0000000000000..d155d92fac149 Binary files /dev/null and b/frontend/__snapshots__/components-command-bar--search.png differ diff --git a/frontend/__snapshots__/components-command-bar--shortcuts.png b/frontend/__snapshots__/components-command-bar--shortcuts.png new file mode 100644 index 0000000000000..32f4e7ff34955 Binary files /dev/null and b/frontend/__snapshots__/components-command-bar--shortcuts.png differ diff --git a/frontend/__snapshots__/components-compact-list--compact-list.png b/frontend/__snapshots__/components-compact-list--compact-list.png index 4a4b5e8704410..49cbfb2048571 100644 Binary files a/frontend/__snapshots__/components-compact-list--compact-list.png and b/frontend/__snapshots__/components-compact-list--compact-list.png differ diff --git a/frontend/__snapshots__/components-editable-field--default.png b/frontend/__snapshots__/components-editable-field--default.png index 2d16114431388..f68ba65618170 100644 Binary files a/frontend/__snapshots__/components-editable-field--default.png and b/frontend/__snapshots__/components-editable-field--default.png differ diff --git a/frontend/__snapshots__/components-networkrequesttiming--basic.png b/frontend/__snapshots__/components-networkrequesttiming--basic.png index ff34c2d27d479..effc91c21a0a6 100644 Binary files a/frontend/__snapshots__/components-networkrequesttiming--basic.png and b/frontend/__snapshots__/components-networkrequesttiming--basic.png differ diff --git a/frontend/__snapshots__/components-product-empty-state--empty-with-action.png b/frontend/__snapshots__/components-product-empty-state--empty-with-action.png index dd10594e21d1c..4c6bc2766b5e4 100644 Binary files a/frontend/__snapshots__/components-product-empty-state--empty-with-action.png and b/frontend/__snapshots__/components-product-empty-state--empty-with-action.png differ diff --git a/frontend/__snapshots__/components-product-empty-state--not-empty-with-action.png b/frontend/__snapshots__/components-product-empty-state--not-empty-with-action.png index d9ed865218733..a93edc4abb8e1 100644 Binary files a/frontend/__snapshots__/components-product-empty-state--not-empty-with-action.png and b/frontend/__snapshots__/components-product-empty-state--not-empty-with-action.png differ diff --git a/frontend/__snapshots__/components-product-empty-state--product-introduction.png b/frontend/__snapshots__/components-product-empty-state--product-introduction.png index dd10594e21d1c..4c6bc2766b5e4 100644 Binary files a/frontend/__snapshots__/components-product-empty-state--product-introduction.png and b/frontend/__snapshots__/components-product-empty-state--product-introduction.png differ diff --git a/frontend/__snapshots__/components-properties-table--properties-table.png b/frontend/__snapshots__/components-properties-table--properties-table.png new file mode 100644 index 0000000000000..0ebb3a71ccb83 Binary files /dev/null and b/frontend/__snapshots__/components-properties-table--properties-table.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight.png index cf79e173859f2..f0559ae34642d 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-historical-trends-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png index 38244e35ae9d8..aa5a9eab7354f 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png index 4a04621ca44de..dd92bf6b0ef79 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-left-to-right-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-time-to-convert-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-time-to-convert-insight.png index c403c659e15df..e97ac272bc3b5 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-time-to-convert-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-time-to-convert-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight.png index ec9d14b04e8ca..20d72c16238e4 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight.png b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight.png index 6f5b39c1f9d3e..c7f2676ae1bf2 100644 Binary files a/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight.png and b/frontend/__snapshots__/exporter-exporter--funnel-top-to-bottom-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png b/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png index fdcbbd24236f1..49e132b82db76 100644 Binary files a/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png and b/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--retention-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--retention-breakdown-insight.png index 082bdb9907417..7185efea76ab6 100644 Binary files a/frontend/__snapshots__/exporter-exporter--retention-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--retention-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--retention-insight.png b/frontend/__snapshots__/exporter-exporter--retention-insight.png index c62fc70ac088b..e91b842102b86 100644 Binary files a/frontend/__snapshots__/exporter-exporter--retention-insight.png and b/frontend/__snapshots__/exporter-exporter--retention-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--stickiness-insight.png b/frontend/__snapshots__/exporter-exporter--stickiness-insight.png index 1c1e95908112f..3fc3e03143c98 100644 Binary files a/frontend/__snapshots__/exporter-exporter--stickiness-insight.png and b/frontend/__snapshots__/exporter-exporter--stickiness-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png index 24ffc71713a28..7de2045284f44 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-area-insight.png b/frontend/__snapshots__/exporter-exporter--trends-area-insight.png index 56ac86a94a237..41d262d023141 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-area-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-area-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-bar-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-bar-breakdown-insight.png index 3c95a30cc395a..e197249c208ce 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-bar-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-bar-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png b/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png index 56ac86a94a237..41d262d023141 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-breakdown-insight.png index 3c95a30cc395a..e197249c208ce 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-line-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-line-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-insight.png index 56ac86a94a237..41d262d023141 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-line-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-line-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png index 75529341446f0..93c140d75f49a 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-number-insight.png b/frontend/__snapshots__/exporter-exporter--trends-number-insight.png index ec6d628426da3..67b2a70ddeb75 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-number-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-number-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-pie-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-pie-breakdown-insight.png index 7a1f9dfceee8c..61ac574a3c443 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-pie-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-pie-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-pie-insight.png b/frontend/__snapshots__/exporter-exporter--trends-pie-insight.png index 825a772994d7d..03e5f0a529a8a 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-pie-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-pie-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png index 036f84e322323..282daf6b30543 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-insight.png b/frontend/__snapshots__/exporter-exporter--trends-table-insight.png index 21d2a42f9eaf2..5223b75398630 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-table-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-value-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-value-breakdown-insight.png index 3fff287337545..3575438a7ad30 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-value-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-value-breakdown-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-value-insight.png b/frontend/__snapshots__/exporter-exporter--trends-value-insight.png index 56ac86a94a237..41d262d023141 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-value-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-value-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-world-map-insight.png b/frontend/__snapshots__/exporter-exporter--trends-world-map-insight.png index d4b629b6536ad..7654fa3f75324 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-world-map-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-world-map-insight.png differ diff --git a/frontend/__snapshots__/exporter-exporter--user-paths-insight.png b/frontend/__snapshots__/exporter-exporter--user-paths-insight.png index 79f1711336dbf..19977a7a1bd22 100644 Binary files a/frontend/__snapshots__/exporter-exporter--user-paths-insight.png and b/frontend/__snapshots__/exporter-exporter--user-paths-insight.png differ diff --git a/frontend/__snapshots__/insights-insightstable--embedded.png b/frontend/__snapshots__/insights-insightstable--embedded.png index 3f9234e5b9d3a..180bab30a3d06 100644 Binary files a/frontend/__snapshots__/insights-insightstable--embedded.png and b/frontend/__snapshots__/insights-insightstable--embedded.png differ diff --git a/frontend/__snapshots__/layout-navigation--app-page-with-side-bar-shown.png b/frontend/__snapshots__/layout-navigation--app-page-with-side-bar-shown.png index b49b6fc4bd341..b2ae49cd91f5b 100644 Binary files a/frontend/__snapshots__/layout-navigation--app-page-with-side-bar-shown.png and b/frontend/__snapshots__/layout-navigation--app-page-with-side-bar-shown.png differ diff --git a/frontend/__snapshots__/lemon-ui-colors--all-three-thousand-color-options.png b/frontend/__snapshots__/lemon-ui-colors--all-three-thousand-color-options.png index 2038fbe5c8bb2..02abe5eaa23a3 100644 Binary files a/frontend/__snapshots__/lemon-ui-colors--all-three-thousand-color-options.png and b/frontend/__snapshots__/lemon-ui-colors--all-three-thousand-color-options.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--closable.png b/frontend/__snapshots__/lemon-ui-lemon-banner--closable.png index a05dd78b3e3e7..a7a8ac55c5061 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--closable.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--closable.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--dismissable.png b/frontend/__snapshots__/lemon-ui-lemon-banner--dismissable.png index be2ef2e5a884b..540a8a3ef2c39 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--dismissable.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--dismissable.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--error.png b/frontend/__snapshots__/lemon-ui-lemon-banner--error.png index 7db8c557495b9..9389cfa4ea1b2 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--error.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--error.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--info.png b/frontend/__snapshots__/lemon-ui-lemon-banner--info.png index 7c6e78d57caf2..6848c05f89a32 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--info.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--info.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--success.png b/frontend/__snapshots__/lemon-ui-lemon-banner--success.png index 2053ce5ccc6de..f3b58cb98363a 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--success.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--success.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-banner--warning.png b/frontend/__snapshots__/lemon-ui-lemon-banner--warning.png index bf8c975d7385b..3c41933fb5078 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-banner--warning.png and b/frontend/__snapshots__/lemon-ui-lemon-banner--warning.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--as-links.png b/frontend/__snapshots__/lemon-ui-lemon-button--as-links.png index 24ae6fe59d181..292f9ce7d0a99 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--as-links.png and b/frontend/__snapshots__/lemon-ui-lemon-button--as-links.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-skeleton--presets.png b/frontend/__snapshots__/lemon-ui-lemon-skeleton--presets.png index d26a679f4fd68..18ea3052167f0 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-skeleton--presets.png and b/frontend/__snapshots__/lemon-ui-lemon-skeleton--presets.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-table--empty-loading.png b/frontend/__snapshots__/lemon-ui-lemon-table--empty-loading.png index b6109c6884322..90ae77d5ca04a 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-table--empty-loading.png and b/frontend/__snapshots__/lemon-ui-lemon-table--empty-loading.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-table--loading.png b/frontend/__snapshots__/lemon-ui-lemon-table--loading.png index e5852c23bda01..f3f2287fcdb4c 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-table--loading.png and b/frontend/__snapshots__/lemon-ui-lemon-table--loading.png differ diff --git a/frontend/__snapshots__/lemon-ui-textfit--basic.png b/frontend/__snapshots__/lemon-ui-textfit--basic.png new file mode 100644 index 0000000000000..269bfc9cfad86 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-textfit--basic.png differ diff --git a/frontend/__snapshots__/posthog-3000-keyboard-shortcut--default.png b/frontend/__snapshots__/posthog-3000-keyboard-shortcut--default.png new file mode 100644 index 0000000000000..995c1bc753ad1 Binary files /dev/null and b/frontend/__snapshots__/posthog-3000-keyboard-shortcut--default.png differ diff --git a/frontend/__snapshots__/posthog-3000-keyboard-shortcut--muted.png b/frontend/__snapshots__/posthog-3000-keyboard-shortcut--muted.png new file mode 100644 index 0000000000000..995c1bc753ad1 Binary files /dev/null and b/frontend/__snapshots__/posthog-3000-keyboard-shortcut--muted.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png index 3829c37b8d852..cb3513e97394d 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png index 827ca87dda7ee..d42c385e3c023 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-base.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--create-export.png b/frontend/__snapshots__/scenes-app-batchexports--create-export.png index 5812443d7cc01..51889a6cdcc34 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--create-export.png and b/frontend/__snapshots__/scenes-app-batchexports--create-export.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--edit.png b/frontend/__snapshots__/scenes-app-dashboards--edit.png index 6389702cb99c5..0bd4f10c2b233 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--edit.png and b/frontend/__snapshots__/scenes-app-dashboards--edit.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--show.png b/frontend/__snapshots__/scenes-app-dashboards--show.png index 4aaadd263a2a6..9f1dac8d8c809 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--show.png and b/frontend/__snapshots__/scenes-app-dashboards--show.png differ diff --git a/frontend/__snapshots__/scenes-app-events--event-explorer.png b/frontend/__snapshots__/scenes-app-events--event-explorer.png index 4ed82b3bdbbdd..7d3287e05481b 100644 Binary files a/frontend/__snapshots__/scenes-app-events--event-explorer.png and b/frontend/__snapshots__/scenes-app-events--event-explorer.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png index 8feeeb95edc07..0c9084824591a 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate.png index fd9d704276d4c..683e286506729 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list.png index f6760a46e6b69..4072657487cb8 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png index aab6a5a84f625..766de1662f8ae 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png index 0d7d6dd72c02b..9fe8b83975eb5 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--edit-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png index 45cc7c6c7b4d5..4638e3c252629 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--edit-multi-variate-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png index c640e778e8505..2d6a0dd22fbb2 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png index 015ec328151a8..a36d7365e7344 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png index dd9c31998e7e4..613e89cdbc0d8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png index 50f36a4871650..d2c639e108166 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png index edc1d0829245e..bcd656dcc26a3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png index 73ef96ff35115..93a2510edbd3e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png index 47ef343bc3320..88fce2b75ec39 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png index 83e9c3fa63d99..234167ed34ceb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png index e379604a2591b..8be9ffc926019 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png index 7cd7d0f64cc9a..8eaedc5c1d3fa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png index e06956a47c809..87320f1ad5b19 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png index 07f7cfe1b6935..92811307a20d4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png index db6f0a7f56163..ee0abcee40c6b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png index 226edac304a58..cd77f1bbd8447 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png index 656cf46515e6e..b0c052e1b37ff 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png index 486d89f6bd7ca..62ab313160740 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png index 036c2bc3b587d..0f65f00462135 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png index 69d5bf0584241..20f8540e7af66 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png index d26c9a76aad35..5ba4dd941fcbf 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png index 59d35f7d7b1da..64b2955db1cfe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png index db0e0dc5d29be..7fde8e692b299 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png index f4bdbb0d8c2cb..67d55e396a376 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png index ad94cc04a4881..a41c083a673a9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png index 896a7623a9071..10a781cafc5d0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png index fbdf18896ae06..8760764ed3a04 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png index ee1307cd8d1ba..c4212fc4ab28d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png index 373e3e0e19c98..4f11382fa8bb4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png index 15c56dfa56a59..a4c4a82e5d207 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle.png b/frontend/__snapshots__/scenes-app-insights--lifecycle.png index 8fda2317ec4d0..b5af8e303507d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention--webkit.png index 87c46c2fab5ab..3f6259a8a2428 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png index fd40f5ea3d016..8e6390a225b40 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png index f599ea59d602b..998fedb12e86e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png index 97fa118d79f56..6262a5918f1f1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png index 121d0b324adcd..62dd6f95bbd41 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png index c8ede7b9fd30c..0a8e1c20fdb87 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit.png index ae71eec088987..cd3d9f866c7f4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention.png b/frontend/__snapshots__/scenes-app-insights--retention.png index e90937f466eef..5df60b1e00204 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention.png and b/frontend/__snapshots__/scenes-app-insights--retention.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png index 4125f299a358e..d226c57fe43df 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png index 8eefb8a637100..c458ce60c87b2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png index 29db39c34e2c8..80e20f0a84a3e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness.png b/frontend/__snapshots__/scenes-app-insights--stickiness.png index de3afbf8cc7af..5f6daca8e6c78 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness.png and b/frontend/__snapshots__/scenes-app-insights--stickiness.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png index 21aa9bc7551f8..451decec4637d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png index b5c8307dea666..77b2e0087b84b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png index 9905ed61d7a89..39cb74dee61dd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png index 18e8183ddefb1..85107049f853f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png index f412b05f949a1..41ba6d59e1550 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png index 1c9bae1872eee..f70af9d2e782c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png index 2513bc614a783..f57645211428a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area.png b/frontend/__snapshots__/scenes-app-insights--trends-area.png index 693bb117d362c..fdd0315060948 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area.png and b/frontend/__snapshots__/scenes-app-insights--trends-area.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png index 029db225e47ab..5d60dee4f60b7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png index 7a047299abaef..8df383d36fc1b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png index aa1a85fbbf26f..3a7274819f516 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png index 42115a622a205..e32072ec0780d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png index b76b6e7a17d98..03c62e056870b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png index adb88102026c6..c9bd130f6cdab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png index 1b81ed4977ff1..ffaa1c4bf1988 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar.png b/frontend/__snapshots__/scenes-app-insights--trends-bar.png index cb863bb9c9bb6..74a5209277252 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png index 8aed583ad4064..81275e0a9df95 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png index a57438bd749a9..ab2904ba751e9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png index 2779891938055..966b934574728 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png index 370cd4a4c10a2..ca3343a84046c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--webkit.png new file mode 100644 index 0000000000000..8f97eb8c1a01c Binary files /dev/null and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels.png new file mode 100644 index 0000000000000..48a045de1d451 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png index 1ec537b3468ad..d4873d617014e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png index e14d719a073e2..a158e442de8ff 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png index fbeb271f50cf0..5e1fb2d08feb2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi--webkit.png index 6015dabcec713..225acef8aacb3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--webkit.png index 3f2231d07737c..c036f5a792f9e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit.png index 649c945bdf267..ca3654b2fd11d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi.png index 9fc201861aa47..d2c5a5a81dcc9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line.png b/frontend/__snapshots__/scenes-app-insights--trends-line.png index e1b4848578966..49bbad9f70249 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line.png and b/frontend/__snapshots__/scenes-app-insights--trends-line.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png index 17a8821fb39ed..2615d368706cd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png index 796e6548e08c8..635203fb3d413 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png index 25d7659b28b81..6924135553179 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number.png b/frontend/__snapshots__/scenes-app-insights--trends-number.png index 48d95207dd24d..693dd022b93e9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number.png and b/frontend/__snapshots__/scenes-app-insights--trends-number.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png index 846479f3753fe..660bb356237ca 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png index 983851141af8d..cadbd8a861f31 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png index 5163ac5fdb238..646050c69e70b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png index e020cb76a7760..bd1f3ed00e233 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--webkit.png new file mode 100644 index 0000000000000..082bca2e23f47 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels.png new file mode 100644 index 0000000000000..7a1c58bf02c1d Binary files /dev/null and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png index e396c60572415..bc2f9c129b6f9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png index af5c8b64103fe..3e784f8c855d5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png index 0a8ebe4751399..886c9d33a0add 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie.png b/frontend/__snapshots__/scenes-app-insights--trends-pie.png index 5f83bed8ef407..c6000ed1654c6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png index 2ba75da1d8204..cee544d679cc5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png index 063e78dd5feaf..4a32c1bf8df36 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png index 4196d96dfbd67..dc9333bb1780d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png index 8ae256c8199bf..f9e6a6f710268 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png index a3a2a9a805370..ed70303f61142 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png index fb0e1de7e7310..d71a4ba3b777d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png index 4ca89e34bfd22..43417b1c2f0ac 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table.png b/frontend/__snapshots__/scenes-app-insights--trends-table.png index e36f384700aee..2507ecadf9e8f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table.png and b/frontend/__snapshots__/scenes-app-insights--trends-table.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png index a350461bf3689..94e1c6b5604ab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png index 8af97f329f0d5..066eb2c1ac02a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png index c626cb8402ae2..63e0e28282346 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png index f45c73c45217e..f3599c707dd15 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png index 98d682c860521..a0f6e839e7e60 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png index 48b1f53b59bfb..140d24237944a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png index bdb269d4562ee..c908b3c0a5d9e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value.png b/frontend/__snapshots__/scenes-app-insights--trends-value.png index 24e25225e9902..ea8fd3d8fd413 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value.png and b/frontend/__snapshots__/scenes-app-insights--trends-value.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png index 1ae8bc9fccb15..24462204e7738 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png index 709a2698bae3b..e11671b67ea0a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png index 0462e9cfedd78..9fbed136e738d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map.png index e76857963a4de..cbb62ce7deb40 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png index 8c15340d735f3..bd9ba9dbf8a97 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png index 0db6e20f49afb..ea0ad2f69c6ae 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png index df710f4dd46da..af38b73f25636 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths.png b/frontend/__snapshots__/scenes-app-insights--user-paths.png index ca4c44785108f..deec9d37f5b61 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths.png and b/frontend/__snapshots__/scenes-app-insights--user-paths.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png index d196998a7e56f..2b56395785ef6 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png and b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--empty-notebook.png b/frontend/__snapshots__/scenes-app-notebooks--empty-notebook.png index 2086ed0aa90aa..c6df20187e100 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--empty-notebook.png and b/frontend/__snapshots__/scenes-app-notebooks--empty-notebook.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--headings.png b/frontend/__snapshots__/scenes-app-notebooks--headings.png index 3186fed28fd49..1d202bf688da1 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--headings.png and b/frontend/__snapshots__/scenes-app-notebooks--headings.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--notebook-not-found.png b/frontend/__snapshots__/scenes-app-notebooks--notebook-not-found.png index 0df1f64e9ec3c..6286e7ae27078 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--notebook-not-found.png and b/frontend/__snapshots__/scenes-app-notebooks--notebook-not-found.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--notebooks-list.png b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list.png index c9f29c566c2c6..04209a125f40b 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--notebooks-list.png and b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png index cbe5aefd199ed..de7c9e016a3d0 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png and b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png index e18ecc8d6a5a5..b69055ad3d2ec 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--text-formats.png b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png index aa872f0a5cfe9..6f7c0b4c36de0 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--text-formats.png and b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png b/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png index f0ea3f9550997..c475638688418 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png and b/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png new file mode 100644 index 0000000000000..4268b9820e627 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png new file mode 100644 index 0000000000000..fc3f054686e53 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png new file mode 100644 index 0000000000000..6e4f613841bdf Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-filtering-page.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-filtering-page.png index 2922f7f7736ff..a000f5709360c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-filtering-page.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-filtering-page.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page.png index 72113b0438d81..d5df660c362ee 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty.png index b9810d5bf2186..5655e24e12a0c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png index 80fdf0c1d8632..06bdfb7c4f880 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png differ diff --git a/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png b/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png index fe614866ab3d3..84eb120b096bf 100644 Binary files a/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png and b/frontend/__snapshots__/scenes-app-project-homepage--project-homepage.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-play-lists.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-lists.png index c4c27f4db817a..7bb41e1837cc3 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--recordings-play-lists.png and b/frontend/__snapshots__/scenes-app-recordings--recordings-play-lists.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--card-view.png b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png index df6c09ca18009..3349db3182ce2 100644 Binary files a/frontend/__snapshots__/scenes-app-saved-insights--card-view.png and b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--empty-state.png b/frontend/__snapshots__/scenes-app-saved-insights--empty-state.png index 7757d243d548c..7952158fdb025 100644 Binary files a/frontend/__snapshots__/scenes-app-saved-insights--empty-state.png and b/frontend/__snapshots__/scenes-app-saved-insights--empty-state.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--list-view.png b/frontend/__snapshots__/scenes-app-saved-insights--list-view.png index 520f2c041c0a8..8bfe21cb3985b 100644 Binary files a/frontend/__snapshots__/scenes-app-saved-insights--list-view.png and b/frontend/__snapshots__/scenes-app-saved-insights--list-view.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--survey-templates.png b/frontend/__snapshots__/scenes-app-surveys--survey-templates.png index 069a66dbfbb5b..d888557c99407 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--survey-templates.png and b/frontend/__snapshots__/scenes-app-surveys--survey-templates.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--surveys-list.png b/frontend/__snapshots__/scenes-app-surveys--surveys-list.png index 012692ee2758d..80ccaa5e006fd 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--surveys-list.png and b/frontend/__snapshots__/scenes-app-surveys--surveys-list.png differ diff --git a/frontend/__snapshots__/scenes-other-login--second-factor.png b/frontend/__snapshots__/scenes-other-login--second-factor.png index 0eda9f6a6d221..d770df97b4345 100644 Binary files a/frontend/__snapshots__/scenes-other-login--second-factor.png and b/frontend/__snapshots__/scenes-other-login--second-factor.png differ diff --git a/frontend/__snapshots__/scenes-other-login--sso-error.png b/frontend/__snapshots__/scenes-other-login--sso-error.png index 4bac52c291407..37309681cfb75 100644 Binary files a/frontend/__snapshots__/scenes-other-login--sso-error.png and b/frontend/__snapshots__/scenes-other-login--sso-error.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset-complete--default.png b/frontend/__snapshots__/scenes-other-password-reset-complete--default.png index b08362e5ab1ab..8671219107f0c 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset-complete--default.png and b/frontend/__snapshots__/scenes-other-password-reset-complete--default.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset-complete--invalid-link.png b/frontend/__snapshots__/scenes-other-password-reset-complete--invalid-link.png index d5f6503dbfa2b..6e928b3ee74ac 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset-complete--invalid-link.png and b/frontend/__snapshots__/scenes-other-password-reset-complete--invalid-link.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization.png b/frontend/__snapshots__/scenes-other-settings--settings-organization.png index 05fee3ba22511..0e5752e576d8b 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project.png b/frontend/__snapshots__/scenes-other-settings--settings-project.png index 71514ddb64f12..227e1bac41f89 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project.png and b/frontend/__snapshots__/scenes-other-settings--settings-project.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-user.png b/frontend/__snapshots__/scenes-other-settings--settings-user.png index 5574601d11794..7d18a6db46dd5 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-user.png and b/frontend/__snapshots__/scenes-other-settings--settings-user.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar-components--flags.png b/frontend/__snapshots__/scenes-other-toolbar-components--flags.png index 43dd944036a73..10086fc86f5c1 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar-components--flags.png and b/frontend/__snapshots__/scenes-other-toolbar-components--flags.png differ diff --git a/frontend/__snapshots__/scenes-other-unsubscribe--unsubscribe-scene.png b/frontend/__snapshots__/scenes-other-unsubscribe--unsubscribe-scene.png index 4c1e72c47cc86..e0e34f6fbc61b 100644 Binary files a/frontend/__snapshots__/scenes-other-unsubscribe--unsubscribe-scene.png and b/frontend/__snapshots__/scenes-other-unsubscribe--unsubscribe-scene.png differ diff --git a/frontend/__snapshots__/scenes-other-verify-email--verify-email-invalid.png b/frontend/__snapshots__/scenes-other-verify-email--verify-email-invalid.png index ea95c8af90bc0..0291a167335d6 100644 Binary files a/frontend/__snapshots__/scenes-other-verify-email--verify-email-invalid.png and b/frontend/__snapshots__/scenes-other-verify-email--verify-email-invalid.png differ diff --git a/frontend/__snapshots__/scenes-other-verify-email--verify-email-success.png b/frontend/__snapshots__/scenes-other-verify-email--verify-email-success.png index 45b7e05851b7a..71aad6c351f8b 100644 Binary files a/frontend/__snapshots__/scenes-other-verify-email--verify-email-success.png and b/frontend/__snapshots__/scenes-other-verify-email--verify-email-success.png differ diff --git a/frontend/__snapshots__/scenes-other-verify-email--verifying-email.png b/frontend/__snapshots__/scenes-other-verify-email--verifying-email.png index a6efa516c1cda..ecfc94858c28a 100644 Binary files a/frontend/__snapshots__/scenes-other-verify-email--verifying-email.png and b/frontend/__snapshots__/scenes-other-verify-email--verifying-email.png differ diff --git a/frontend/src/exporter/ExportedInsight/ExportedInsight.scss b/frontend/src/exporter/ExportedInsight/ExportedInsight.scss index c158e28a7bc55..bb643d8f0d3a9 100644 --- a/frontend/src/exporter/ExportedInsight/ExportedInsight.scss +++ b/frontend/src/exporter/ExportedInsight/ExportedInsight.scss @@ -8,6 +8,8 @@ background: var(--bg-light); display: flex; flex-direction: column; + height: 100%; + flex: 1; .ExportedInsight__header { padding: 1rem; @@ -25,22 +27,19 @@ flex: 1; position: relative; z-index: 1; - .FunnelBarChart { - min-height: 50vw; + display: flex; + flex-direction: column; + + .LemonTable { + border: none; + border-radius: 0; + background: none; } - .InsightViz { - position: relative; - display: flex; + .InsightCard__viz { + // We can't use the viewport height, as the screenshotter will resize the height of the window to capture the full content + // Instead we try to make it roughly rectangular min-height: 50vw; - - > * { - flex: 1; - } - - .ActionsPie { - position: absolute; - } } } @@ -49,36 +48,10 @@ right: 0; top: 0; z-index: 2; + svg { font-size: 0.75rem; margin: 0.25rem; } } - - &.ExportedInsight--fit-screen { - height: 100vh; - max-height: 100vh; - - .ExportedInsight__content { - display: flex; - flex-direction: column; - - .FunnelBarChart { - height: 100%; - max-height: 100%; - min-height: auto; - flex: 1; - } - - .InsightViz { - flex: 1; - min-height: 100px; - max-height: 100%; - } - - &.ExportedInsight__content--with-watermark { - padding-top: 1rem; - } - } - } } diff --git a/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx b/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx index 24bfe97cab377..d34818193fd4a 100644 --- a/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx +++ b/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx @@ -1,18 +1,20 @@ -import { ChartDisplayType, InsightLogicProps, InsightModel } from '~/types' +import './ExportedInsight.scss' + +import clsx from 'clsx' import { BindLogic } from 'kea' -import { insightLogic } from 'scenes/insights/insightLogic' import { FilterBasedCardContent } from 'lib/components/Cards/InsightCard/InsightCard' -import './ExportedInsight.scss' -import { Logo } from '~/toolbar/assets/Logo' +import { QueriesUnsupportedHere } from 'lib/components/Cards/InsightCard/QueriesUnsupportedHere' +import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' import { InsightLegend } from 'lib/components/InsightLegend/InsightLegend' -import { ExportOptions, ExportType } from '~/exporter/types' -import clsx from 'clsx' import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' +import { insightLogic } from 'scenes/insights/insightLogic' import { isTrendsFilter } from 'scenes/insights/sharedUtils' -import { isDataTableNode } from '~/queries/utils' -import { QueriesUnsupportedHere } from 'lib/components/Cards/InsightCard/QueriesUnsupportedHere' + +import { ExportOptions, ExportType } from '~/exporter/types' import { Query } from '~/queries/Query/Query' -import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' +import { isDataTableNode } from '~/queries/utils' +import { Logo } from '~/toolbar/assets/Logo' +import { ChartDisplayType, InsightLogicProps, InsightModel } from '~/types' export function ExportedInsight({ insight, @@ -91,11 +93,7 @@ export function ExportedInsight({ ) ) : ( - + )} {showLegend ? (
diff --git a/frontend/src/exporter/Exporter.scss b/frontend/src/exporter/Exporter.scss index db6d2acc6a495..f861f282a2926 100644 --- a/frontend/src/exporter/Exporter.scss +++ b/frontend/src/exporter/Exporter.scss @@ -1,35 +1,10 @@ @import '../styles/mixins'; -html.export-type-image { - // We don't want scrollbars to show in image captures - ::-webkit-scrollbar { - display: none; - } - - body { - // We put Inter first so that rendered images are the same no matter which platform it is rendered on. - font-family: 'Inter', 'Segoe UI', 'Roboto', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol'; - } -} - -html.export-type-embed { - overflow: hidden; - overflow-y: auto; - - .Exporter { - padding: 0; - - // Insights can resize to fit any height, whereas dashboards cannot - &--insight { - height: 100vh; - max-height: 100vh; - } - } -} - .Exporter { padding: 1rem; + min-height: 100vh; + display: flex; + flex-direction: column; &--recording { height: 100vh; @@ -58,3 +33,26 @@ html.export-type-embed { } } } + +html.export-type-image { + // We don't want scrollbars to show in image captures + ::-webkit-scrollbar { + display: none; + } + + body { + // We put Inter first so that rendered images are the same no matter which platform it is rendered on. + font-family: Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol'; + } +} + +html.export-type-embed { + overflow: hidden; + overflow-y: auto; + + .Exporter { + padding: 0; + min-height: 100vh; + } +} diff --git a/frontend/src/exporter/Exporter.stories.tsx b/frontend/src/exporter/Exporter.stories.tsx index 4c5acbd7257aa..95693b3465ad0 100644 --- a/frontend/src/exporter/Exporter.stories.tsx +++ b/frontend/src/exporter/Exporter.stories.tsx @@ -1,9 +1,11 @@ -import { useEffect } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { Exporter } from './Exporter' +import { useEffect } from 'react' + import { dashboard } from '~/exporter/__mocks__/Exporter.mocks' import { ExportType } from '~/exporter/types' +import { Exporter } from './Exporter' + type Story = StoryObj const meta: Meta = { title: 'Exporter/Exporter', diff --git a/frontend/src/exporter/Exporter.tsx b/frontend/src/exporter/Exporter.tsx index 1941637123e83..0ba873c570fe1 100644 --- a/frontend/src/exporter/Exporter.tsx +++ b/frontend/src/exporter/Exporter.tsx @@ -1,18 +1,21 @@ import '~/styles' import './Exporter.scss' -import { useEffect } from 'react' -import { ExportedData, ExportType } from '~/exporter/types' -import { DashboardPlacement } from '~/types' -import { ExportedInsight } from '~/exporter/ExportedInsight/ExportedInsight' -import { Logo } from '~/toolbar/assets/Logo' -import { Dashboard } from 'scenes/dashboard/Dashboard' -import { useResizeObserver } from 'lib/hooks/useResizeObserver' -import { Link } from 'lib/lemon-ui/Link' + import clsx from 'clsx' import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' +import { useResizeObserver } from 'lib/hooks/useResizeObserver' +import { Link } from 'lib/lemon-ui/Link' +import { useEffect } from 'react' +import { Dashboard } from 'scenes/dashboard/Dashboard' import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' import { SessionRecordingPlayerMode } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { teamLogic } from 'scenes/teamLogic' + +import { ExportedInsight } from '~/exporter/ExportedInsight/ExportedInsight' +import { ExportedData, ExportType } from '~/exporter/types' +import { Logo } from '~/toolbar/assets/Logo' +import { DashboardPlacement } from '~/types' + import { exporterViewLogic } from './exporterViewLogic' export function Exporter(props: ExportedData): JSX.Element { diff --git a/frontend/src/exporter/__mocks__/Exporter.mocks.tsx b/frontend/src/exporter/__mocks__/Exporter.mocks.tsx index fdf477f996322..6d2b7e87d1fad 100644 --- a/frontend/src/exporter/__mocks__/Exporter.mocks.tsx +++ b/frontend/src/exporter/__mocks__/Exporter.mocks.tsx @@ -1,3 +1,5 @@ +import { FunnelLayout, ShownAsValue } from 'lib/constants' + import { ChartDisplayType, DashboardTile, @@ -7,7 +9,6 @@ import { InsightShortId, InsightType, } from '~/types' -import { FunnelLayout, ShownAsValue } from 'lib/constants' export const dashboard: DashboardType = { id: 1, diff --git a/frontend/src/exporter/exporterViewLogic.ts b/frontend/src/exporter/exporterViewLogic.ts index e0c36900fc728..dcb6adf2e137c 100644 --- a/frontend/src/exporter/exporterViewLogic.ts +++ b/frontend/src/exporter/exporterViewLogic.ts @@ -1,7 +1,7 @@ import { kea, path, props, selectors } from 'kea' -import { ExportedData } from './types' import type { exporterViewLogicType } from './exporterViewLogicType' +import { ExportedData } from './types' // This is a simple logic that is mounted by the Exporter view and then can be found by any nested callers // This simplifies passing props everywhere. diff --git a/frontend/src/exporter/index.tsx b/frontend/src/exporter/index.tsx index df1660537cb44..995334d949021 100644 --- a/frontend/src/exporter/index.tsx +++ b/frontend/src/exporter/index.tsx @@ -1,10 +1,13 @@ import '~/styles' import './Exporter.scss' + import { createRoot } from 'react-dom/client' -import { loadPostHogJS } from '~/loadPostHogJS' -import { initKea } from '~/initKea' + import { Exporter } from '~/exporter/Exporter' import { ExportedData } from '~/exporter/types' +import { initKea } from '~/initKea' +import { loadPostHogJS } from '~/loadPostHogJS' + import { ErrorBoundary } from '../layout/ErrorBoundary' // Disable tracking for all exports and embeds. diff --git a/frontend/src/globals.d.ts b/frontend/src/globals.d.ts index 17bc65680725a..b03852c10a890 100644 --- a/frontend/src/globals.d.ts +++ b/frontend/src/globals.d.ts @@ -1,4 +1,5 @@ import posthog from 'posthog-js' + import { ExportedData } from '~/exporter/types' declare global { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 42746c188f0ea..272a61f785e3d 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,16 +1,14 @@ import '~/styles' -import { createRoot } from 'react-dom/client' import { getContext } from 'kea' - +import posthog from 'posthog-js' +import { PostHogProvider } from 'posthog-js/react' +import { createRoot } from 'react-dom/client' import { App } from 'scenes/App' -import { initKea } from './initKea' -import { loadPostHogJS } from './loadPostHogJS' +import { initKea } from './initKea' import { ErrorBoundary } from './layout/ErrorBoundary' - -import { PostHogProvider } from 'posthog-js/react' -import posthog from 'posthog-js' +import { loadPostHogJS } from './loadPostHogJS' loadPostHogJS() initKea() diff --git a/frontend/src/initKea.ts b/frontend/src/initKea.ts index 79d727740a4dc..bb5b0af3e5525 100644 --- a/frontend/src/initKea.ts +++ b/frontend/src/initKea.ts @@ -1,13 +1,13 @@ import { KeaPlugin, resetContext } from 'kea' +import { formsPlugin } from 'kea-forms' +import { loadersPlugin } from 'kea-loaders' import { localStoragePlugin } from 'kea-localstorage' import { routerPlugin } from 'kea-router' -import { loadersPlugin } from 'kea-loaders' -import { windowValuesPlugin } from 'kea-window-values' -import { identifierToHuman } from 'lib/utils' +import { subscriptionsPlugin } from 'kea-subscriptions' import { waitForPlugin } from 'kea-waitfor' +import { windowValuesPlugin } from 'kea-window-values' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { subscriptionsPlugin } from 'kea-subscriptions' -import { formsPlugin } from 'kea-forms' +import { identifierToHuman } from 'lib/utils' /* Actions for which we don't want to show error alerts, diff --git a/frontend/src/layout/ErrorBoundary/ErrorBoundary.scss b/frontend/src/layout/ErrorBoundary/ErrorBoundary.scss index c98425d731956..be9ac8887a160 100644 --- a/frontend/src/layout/ErrorBoundary/ErrorBoundary.scss +++ b/frontend/src/layout/ErrorBoundary/ErrorBoundary.scss @@ -5,20 +5,24 @@ padding: 0.75rem 1rem 1rem; min-width: 0; height: fit-content; + .main-app-content > & { margin: 1.5rem 0; } + h2 { margin-bottom: 0.75rem; color: var(--danger); font-weight: 600; } + pre { margin-bottom: 0.75rem; background: var(--border-light); border-radius: var(--radius); padding: 0.75rem 1rem; } + .help-button { margin-top: 0.75rem; } diff --git a/frontend/src/layout/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/layout/ErrorBoundary/ErrorBoundary.tsx index 456b89a8ab65b..78dde9a6d6b3a 100644 --- a/frontend/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -1,8 +1,9 @@ -import { getCurrentHub, ErrorBoundary as SentryErrorBoundary } from '@sentry/react' +import './ErrorBoundary.scss' + +import { ErrorBoundary as SentryErrorBoundary, getCurrentHub } from '@sentry/react' import { HelpButton } from 'lib/components/HelpButton/HelpButton' import { IconArrowDropDown } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './ErrorBoundary.scss' export function ErrorBoundary({ children }: { children: React.ReactElement }): JSX.Element { const isSentryInitialized = !!getCurrentHub().getClient() diff --git a/frontend/src/layout/ErrorNetwork.tsx b/frontend/src/layout/ErrorNetwork.tsx index 01473440a0801..a415d1fc8028d 100644 --- a/frontend/src/layout/ErrorNetwork.tsx +++ b/frontend/src/layout/ErrorNetwork.tsx @@ -1,5 +1,5 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconRefresh } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' export function ErrorNetwork(): JSX.Element { return ( diff --git a/frontend/src/layout/ErrorProjectUnavailable.tsx b/frontend/src/layout/ErrorProjectUnavailable.tsx index a3660e42c37cc..29a178c490b05 100644 --- a/frontend/src/layout/ErrorProjectUnavailable.tsx +++ b/frontend/src/layout/ErrorProjectUnavailable.tsx @@ -1,5 +1,6 @@ -import { PageHeader } from 'lib/components/PageHeader' import { useValues } from 'kea' +import { PageHeader } from 'lib/components/PageHeader' + import { organizationLogic } from '../scenes/organizationLogic' export function ErrorProjectUnavailable(): JSX.Element { diff --git a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx index 45025c5d8132c..8853e4812c90a 100644 --- a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx +++ b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.stories.tsx @@ -1,9 +1,11 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { FeaturePreviewsModal as FeaturePreviewsModalComponent } from './FeaturePreviewsModal' -import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' +import { FeatureFlagKey } from 'lib/constants' import { EarlyAccessFeature } from 'posthog-js' + +import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' + import { CONSTRAINED_PREVIEWS } from './featurePreviewsLogic' -import { FeatureFlagKey } from 'lib/constants' +import { FeaturePreviewsModal as FeaturePreviewsModalComponent } from './FeaturePreviewsModal' interface StoryProps { earlyAccessFeatures: EarlyAccessFeature[] diff --git a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx index cb120f597a5b1..5899f59b82d47 100644 --- a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx +++ b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx @@ -1,9 +1,10 @@ import { LemonButton, LemonDivider, LemonModal, LemonSwitch, LemonTextArea, Link } from '@posthog/lemon-ui' -import { useActions, useValues, useAsyncActions } from 'kea' +import clsx from 'clsx' +import { useActions, useAsyncActions, useValues } from 'kea' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner' import { useLayoutEffect, useState } from 'react' + import { EnrichedEarlyAccessFeature, featurePreviewsLogic } from './featurePreviewsLogic' -import clsx from 'clsx' export function FeaturePreviewsModal({ inline, @@ -120,9 +121,10 @@ function FeaturePreview({ feature }: { feature: EnrichedEarlyAccessFeature }): J /> { - await submitEarlyAccessFeatureFeedback(feedback) - setFeedback('') + onClick={() => { + void submitEarlyAccessFeatureFeedback(feedback).then(() => { + setFeedback('') + }) }} loading={activeFeedbackFlagKeyLoading} fullWidth diff --git a/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx b/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx index 40c2e201be346..1dc54a618c214 100644 --- a/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx +++ b/frontend/src/layout/FeaturePreviews/featurePreviewsLogic.tsx @@ -1,12 +1,13 @@ -import { actions, kea, reducers, path, selectors, connect, listeners } from 'kea' -import { EarlyAccessFeature, posthog } from 'posthog-js' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { actionToUrl, router, urlToAction } from 'kea-router' import { supportLogic } from 'lib/components/Support/supportLogic' -import { userLogic } from 'scenes/userLogic' import { FEATURE_FLAGS, FeatureFlagKey } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { EarlyAccessFeature, posthog } from 'posthog-js' +import { userLogic } from 'scenes/userLogic' + import type { featurePreviewsLogicType } from './featurePreviewsLogicType' -import { actionToUrl, router, urlToAction } from 'kea-router' /** Features that can only be toggled if you fall under the `${flagKey}-preview` flag */ export const CONSTRAINED_PREVIEWS: Set = new Set([FEATURE_FLAGS.POSTHOG_3000]) diff --git a/frontend/src/layout/FeaturePreviews/index.ts b/frontend/src/layout/FeaturePreviews/index.ts index 08a91428b164b..6101040e9e9f2 100644 --- a/frontend/src/layout/FeaturePreviews/index.ts +++ b/frontend/src/layout/FeaturePreviews/index.ts @@ -1,2 +1,2 @@ -export { FeaturePreviewsModal } from './FeaturePreviewsModal' export { featurePreviewsLogic } from './featurePreviewsLogic' +export { FeaturePreviewsModal } from './FeaturePreviewsModal' diff --git a/frontend/src/layout/GlobalModals.tsx b/frontend/src/layout/GlobalModals.tsx index 83e61a3476da0..e2041d0f72dc8 100644 --- a/frontend/src/layout/GlobalModals.tsx +++ b/frontend/src/layout/GlobalModals.tsx @@ -1,18 +1,19 @@ -import { kea, path, actions, reducers, useActions, useValues } from 'kea' -import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' -import { CreateProjectModal } from 'scenes/project/CreateProjectModal' - -import type { globalModalsLogicType } from './GlobalModalsType' -import { FeaturePreviewsModal } from './FeaturePreviews' -import { UpgradeModal } from 'scenes/UpgradeModal' import { LemonModal } from '@posthog/lemon-ui' -import { Setup2FA } from 'scenes/authentication/Setup2FA' -import { userLogic } from 'scenes/userLogic' -import { membersLogic } from 'scenes/organization/membersLogic' +import { actions, kea, path, reducers, useActions, useValues } from 'kea' import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { HedgehogBuddyWithLogic } from 'lib/components/HedgehogBuddy/HedgehogBuddy' import { Prompt } from 'lib/logic/newPrompt/Prompt' +import { Setup2FA } from 'scenes/authentication/Setup2FA' +import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' +import { membersLogic } from 'scenes/organization/membersLogic' +import { CreateProjectModal } from 'scenes/project/CreateProjectModal' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' 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([ path(['layout', 'navigation', 'globalModalsLogic']), @@ -76,6 +77,7 @@ export function GlobalModals(): JSX.Element { + ) } diff --git a/frontend/src/layout/navigation-3000/Navigation.scss b/frontend/src/layout/navigation-3000/Navigation.scss index a4c8e0bbffeb4..74236f4e9a7fc 100644 --- a/frontend/src/layout/navigation-3000/Navigation.scss +++ b/frontend/src/layout/navigation-3000/Navigation.scss @@ -15,6 +15,12 @@ min-width: 0; overflow: auto; } + + .BridgePage { + background: none; + height: 100%; + overflow: visible; + } } .Navigation3000__scene { @@ -22,9 +28,11 @@ position: relative; margin: var(--scene-padding-y) var(--scene-padding-x); min-height: calc(100vh - var(--breadcrumbs-height-full) - var(--scene-padding-y) * 2); + &.Navigation3000__scene--raw { --scene-padding-y: 0px; --scene-padding-x: 0px; + display: flex; flex-direction: column; } @@ -49,8 +57,10 @@ z-index: var(--z-main-nav); .LemonButton { - min-height: 2.25rem !important; // Reduce minimum height - padding: 0.375rem !important; // Use a custom padding for the navbar only + .LemonButton__chrome { + padding: 0.25rem !important; + font-size: 0.8125rem; + } } ul { @@ -62,16 +72,18 @@ } li + li { - margin-top: 1px; + margin-top: -1px; } } } .NavbarButton { position: relative; + .LemonButton__icon { transition: color 100ms ease, transform 100ms ease; } + &.NavbarButton--here { &::after { content: '•'; @@ -84,6 +96,7 @@ line-height: 0.5625rem; color: var(--default); } + .LemonButton__icon { color: var(--default); transform: translateY(-0.25rem); @@ -98,7 +111,9 @@ --sidebar-horizontal-padding: 0.5rem; --sidebar-row-height: 2rem; --sidebar-background: var(--bg-3000); + position: relative; + // This border is just for sizing, the visible border is on the content and slider // Hidden when the sidebar is closed border-right: min(1px, var(--sidebar-width)) solid transparent; @@ -122,23 +137,27 @@ margin: 0; line-height: inherit; } + h3 { font-weight: 600; line-height: 2rem; font-size: 0.75rem; } + h4 { font-weight: 600; line-height: 1.75rem; font-size: 0.6875rem; flex-grow: 1; } + h5 { font-weight: 400; font-size: 0.75rem; text-transform: none; letter-spacing: normal; } + b { font-weight: 700; } @@ -164,6 +183,7 @@ white-space: nowrap; overflow: hidden; background: var(--sidebar-background); + // Extend the border into viewport overscroll border-right: min(1px, var(--sidebar-width)) solid var(--border); @@ -182,8 +202,7 @@ flex-direction: column; align-items: stretch; flex-grow: 1; - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; } .Sidebar3000__hint { @@ -206,6 +225,7 @@ right: calc( -1 * var(--sidebar-slider-padding) - min(1px, var(--sidebar-width)) ); // Center around the original sidebar border + width: calc(2 * var(--sidebar-slider-padding) + 1px); // The interactive area is enlarged for easier interaction cursor: col-resize; user-select: none; // Fixes inadvertent selection of scene text when resizing @@ -222,23 +242,28 @@ width: 1px; pointer-events: none; } + &::before { transition: 100ms ease transform; background: var(--border); } + &::after { transition: 100ms ease transform; background: var(--text-3000); opacity: 0; } + &:hover::after, .Sidebar3000--resizing &::after { opacity: 0.25; } + .Sidebar3000--resizing &::before, .Sidebar3000--resizing &::after { transform: scaleX(3); } + .Sidebar3000[aria-hidden='true'] & { cursor: e-resize; } @@ -260,20 +285,25 @@ --accordion-inset-expandable: 1.25rem; --accordion-header-background: var(--accent-3000); --accordion-inset: 0rem; + min-height: var(--accordion-row-height); flex-shrink: 0; flex-basis: 0; display: flex; flex-direction: column; + [theme='dark'] & { --accordion-header-background: var(--bg-3000); } + &[aria-expanded] { // This means: if accordion is expandable --accordion-inset: var(--accordion-inset-expandable); } + &:not([aria-expanded='false']) { flex-grow: 1; + &:not(:last-child) { border-bottom-width: 1px; } @@ -283,10 +313,12 @@ .Accordion[aria-disabled='true'] { .Accordion__header { cursor: default; + &:hover { background: var(--accordion-header-background); } } + &:not([aria-busy='true']) .Accordion__header .LemonIcon { visibility: hidden; } @@ -302,14 +334,17 @@ border-bottom-width: 1px; cursor: pointer; user-select: none; + &:hover { background: var(--border-3000); } + > .LemonIcon { transition: 50ms ease transform; font-size: var(--accordion-arrow-size); flex-shrink: 0; margin-right: calc(var(--accordion-inset-expandable) - var(--accordion-arrow-size)); + .Accordion[aria-expanded='true'] & { transform: rotate(90deg); } @@ -321,6 +356,7 @@ --sidebar-list-item-fold-size: 0.5rem; --sidebar-list-item-ribbon-width: 0.1875rem; --sidebar-list-item-background: var(--sidebar-background); + position: relative; color: var(--muted); line-height: 1.125rem; @@ -333,8 +369,10 @@ &[aria-current='page'], &.SidebarListItem--is-renaming { opacity: 1; + --sidebar-list-item-background: var(--border-3000); } + &:hover, &:focus-within, &[aria-current='page'], @@ -343,14 +381,17 @@ .SidebarListItem__actions { display: flex; } + // Accommodate menu button by moving stuff out of the way &.SidebarListItem--has-menu:not(.SidebarListItem--extended) .SidebarListItem__link { padding-right: calc(var(--sidebar-horizontal-padding) + 1.25rem); } + &.SidebarListItem--has-menu.SidebarListItem--extended { &::after { content: ''; position: absolute; + // Position 1px away so that the :focus-visible border isn't overlaid top: 1px; right: 1px; @@ -377,6 +418,7 @@ z-index: 1; } } + &.SidebarListItem--marker-fold { &::before { width: 0; @@ -385,23 +427,29 @@ border-bottom: var(--sidebar-list-item-fold-size) solid transparent; } } + &.SidebarListItem--marker-ribbon { --sidebar-list-item-marker-offset: var(--sidebar-list-item-ribbon-width); + &::before { width: var(--sidebar-list-item-ribbon-width); height: 100%; background: var(--sidebar-list-item-status-color); } } + &.SidebarListItem--marker-status-success { --sidebar-list-item-status-color: var(--success); } + &.SidebarListItem--marker-status-warning { --sidebar-list-item-status-color: var(--warning); } + &.SidebarListItem--marker-status-danger { --sidebar-list-item-status-color: var(--danger); } + &.SidebarListItem--marker-status-completion { --sidebar-list-item-status-color: var(--purple); } @@ -412,6 +460,7 @@ --sidebar-list-item-inset: calc( var(--accordion-inset, 0px) + var(--sidebar-horizontal-padding) + var(--sidebar-list-item-marker-offset, 0px) ); + position: relative; display: flex; flex-direction: column; @@ -419,13 +468,11 @@ width: 100%; height: 100%; color: inherit; + &:focus-visible::after { content: ''; position: absolute; - top: 0; - left: 0; - bottom: -1px; - right: 0; + inset: 0 0 -1px; border: 1px solid var(--default); pointer-events: none; } @@ -443,6 +490,7 @@ .SidebarListItem__rename { // Pseudo-elements don't work on inputs, so we use a wrapper div background: var(--bg-light); + input { outline: none; height: 100%; @@ -452,15 +500,14 @@ padding: 0 calc(var(--sidebar-horizontal-padding) + 2.5rem) 0 var(--sidebar-list-item-inset); background: none; } + &::after { content: ''; position: absolute; - top: 0; - left: 0; - bottom: -1px; - right: 0; + inset: 0 0 -1px; border: 1px solid var(--default); pointer-events: none; + .SidebarListItem[aria-invalid='true'] & { border-color: var(--danger); } @@ -477,6 +524,7 @@ background: var(--danger); color: #fff; white-space: normal; + &::before { display: block; content: ''; diff --git a/frontend/src/layout/navigation-3000/Navigation.stories.tsx b/frontend/src/layout/navigation-3000/Navigation.stories.tsx index 73d7298878007..bc31a2d8079d3 100644 --- a/frontend/src/layout/navigation-3000/Navigation.stories.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.stories.tsx @@ -1,11 +1,12 @@ import { Meta } from '@storybook/react' -import { mswDecorator, setFeatureFlags } from '~/mocks/browser' -import { useEffect } from 'react' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { FEATURE_FLAGS } from 'lib/constants' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + +import { mswDecorator, setFeatureFlags } from '~/mocks/browser' import { EMPTY_PAGINATED_RESPONSE } from '~/mocks/handlers' -import { FEATURE_FLAGS } from 'lib/constants' const meta: Meta = { title: 'PostHog 3000/Navigation', diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index 851a3fe2b86ab..dff2a25ec7ca4 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -1,37 +1,45 @@ -import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette' +import './Navigation.scss' + +import clsx from 'clsx' import { useMountedLogic, useValues } from 'kea' +import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FEATURE_FLAGS } from 'lib/constants' import { ReactNode, useEffect } from 'react' +import { SceneConfig } from 'scenes/sceneTypes' + import { Breadcrumbs } from './components/Breadcrumbs' +import { MinimalNavigation } from './components/MinimalNavigation' import { Navbar } from './components/Navbar' import { Sidebar } from './components/Sidebar' -import './Navigation.scss' -import { themeLogic } from './themeLogic' import { navigation3000Logic } from './navigationLogic' -import clsx from 'clsx' -import { Scene, SceneConfig } from 'scenes/sceneTypes' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' import { SidePanel } from './sidepanel/SidePanel' +import { themeLogic } from './themeLogic' export function Navigation({ children, sceneConfig, }: { children: ReactNode - scene: Scene | null sceneConfig: SceneConfig | null }): JSX.Element { useMountedLogic(themeLogic) - const { activeNavbarItem } = useValues(navigation3000Logic) + const { activeNavbarItem, mode } = useValues(navigation3000Logic) useEffect(() => { // FIXME: Include debug notice in a non-obstructing way document.getElementById('bottom-notice')?.remove() }, []) - if (sceneConfig?.layout === 'plain') { - return <>{children} + if (mode !== 'full') { + return ( +
+ {mode === 'minimal' ? : null} +
{children}
+
+ ) } + return (
@@ -43,6 +51,7 @@ export function Navigation({
diff --git a/frontend/src/layout/navigation-3000/components/Breadcrumbs.scss b/frontend/src/layout/navigation-3000/components/Breadcrumbs.scss index 10c4b8fc13240..51124686f5429 100644 --- a/frontend/src/layout/navigation-3000/components/Breadcrumbs.scss +++ b/frontend/src/layout/navigation-3000/components/Breadcrumbs.scss @@ -1,5 +1,6 @@ .Breadcrumbs3000 { --breadcrumbs-compaction-rate: 0; + z-index: var(--z-main-nav); position: sticky; top: 0; @@ -25,32 +26,40 @@ font-size: calc(0.75rem + 0.0625rem * var(--breadcrumbs-compaction-rate)); line-height: 1rem; font-weight: 600; - user-select: none; pointer-events: auto; } -.Breadcrumbs3000__crumbs { - display: flex; - align-items: center; -} - .Breadcrumbs3000__trail { flex-grow: 1; flex-shrink: 1; - overflow-x: auto; + min-width: 0; +} + +.Breadcrumbs3000__crumbs { + height: 1rem; + margin-top: calc(0.25rem * (1 - var(--breadcrumbs-compaction-rate))); + display: flex; + align-items: center; + overflow: visible; } .Breadcrumbs3000__here { + visibility: var(--breadcrumbs-title-large-visibility); position: relative; line-height: 1.2; - margin: calc(0.25rem * (1 - var(--breadcrumbs-compaction-rate))) 0 0; + margin: 0; + padding: calc(0.5rem * (1 - var(--breadcrumbs-compaction-rate))) 0 0; font-size: 1rem; font-weight: 700; overflow: hidden; - height: calc(1em * 1.2 * (1 - var(--breadcrumbs-compaction-rate))); - > span { + height: calc(1.2em * (1 - var(--breadcrumbs-compaction-rate))); + box-sizing: content-box; + font-family: var(--font-sans) !important; + + > * { position: absolute; - bottom: 0; + bottom: 0.25rem; + height: 1.2em; } } @@ -68,8 +77,10 @@ } &.Breadcrumbs3000__breadcrumb--here { + visibility: var(--breadcrumbs-title-small-visibility); cursor: default; - > span { + + > * { opacity: 1; transform: translateY(calc(100% * (1 - var(--breadcrumbs-compaction-rate)))); } @@ -77,6 +88,7 @@ &.Breadcrumbs3000__breadcrumb--actionable { cursor: pointer; + &:hover > span, &.Breadcrumbs3000__breadcrumb--open > span { opacity: 1; @@ -103,6 +115,7 @@ flex-shrink: 0; margin: 0 0.5rem; opacity: 0.5; + &::after { content: '/'; } diff --git a/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx b/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx index b818d42923c95..881c7139c6b9a 100644 --- a/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx +++ b/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx @@ -1,13 +1,16 @@ -import React, { useEffect, useState } from 'react' +import './Breadcrumbs.scss' + +import { LemonSkeleton } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' +import { EditableField } from 'lib/components/EditableField/EditableField' import { IconArrowDropDown } from 'lib/lemon-ui/icons' import { Link } from 'lib/lemon-ui/Link' -import './Breadcrumbs.scss' -import { Breadcrumb as IBreadcrumb } from '~/types' -import clsx from 'clsx' import { Popover } from 'lib/lemon-ui/Popover/Popover' +import React, { useLayoutEffect, useState } from 'react' + import { breadcrumbsLogic } from '~/layout/navigation/Breadcrumbs/breadcrumbsLogic' -import { LemonSkeleton } from '@posthog/lemon-ui' +import { FinalizedBreadcrumb } from '~/types' const COMPACTION_DISTANCE = 44 @@ -17,27 +20,47 @@ const COMPACTION_DISTANCE = 44 * - The "Quick scene actions" buttons (zero or more buttons on the right) */ export function Breadcrumbs(): JSX.Element | null { - const { breadcrumbs } = useValues(breadcrumbsLogic) + const { breadcrumbs, renameState } = useValues(breadcrumbsLogic) const { setActionsContainer } = useActions(breadcrumbsLogic) const [compactionRate, setCompactionRate] = useState(0) - useEffect(() => { + useLayoutEffect(() => { function handleScroll(): void { const scrollTop = document.getElementsByTagName('main')[0].scrollTop - setCompactionRate(Math.min(scrollTop / COMPACTION_DISTANCE, 1)) + const newCompactionRate = Math.min(scrollTop / COMPACTION_DISTANCE, 1) + setCompactionRate(newCompactionRate) + if ( + renameState && + ((newCompactionRate > 0.5 && compactionRate <= 0.5) || + (newCompactionRate <= 0.5 && compactionRate > 0.5)) + ) { + // Transfer selection from the outgoing input to the incoming one + const [source, target] = newCompactionRate > 0.5 ? ['large', 'small'] : ['small', 'large'] + const sourceEl = document.querySelector(`input[name="item-name-${source}"]`) + const targetEl = document.querySelector(`input[name="item-name-${target}"]`) + if (sourceEl && targetEl) { + targetEl.focus() + targetEl.setSelectionRange(sourceEl.selectionStart || 0, sourceEl.selectionEnd || 0) + } + } } const main = document.getElementsByTagName('main')[0] main.addEventListener('scroll', handleScroll) return () => main.removeEventListener('scroll', handleScroll) - }, []) + }, [compactionRate]) return breadcrumbs.length ? (
@@ -65,14 +88,43 @@ export function Breadcrumbs(): JSX.Element | null { } interface BreadcrumbProps { - breadcrumb: IBreadcrumb + breadcrumb: FinalizedBreadcrumb index: number here?: boolean } function Breadcrumb({ breadcrumb, index, here }: BreadcrumbProps): JSX.Element { + const { renameState } = useValues(breadcrumbsLogic) + const { tentativelyRename, finishRenaming } = useActions(breadcrumbsLogic) const [popoverShown, setPopoverShown] = useState(false) + let nameElement: JSX.Element + if (breadcrumb.name != null && breadcrumb.onRename) { + nameElement = ( + tentativelyRename(breadcrumb.globalKey, newName)} + onSave={(newName) => { + void breadcrumb.onRename?.(newName) + }} + mode={renameState && renameState[0] === breadcrumb.globalKey ? 'edit' : 'view'} + onModeToggle={(newMode) => { + if (newMode === 'edit') { + tentativelyRename(breadcrumb.globalKey, breadcrumb.name as string) + } else { + finishRenaming() + } + setPopoverShown(false) + }} + compactButtons="xsmall" + editingIndication="underlined" + /> + ) + } else { + nameElement = {breadcrumb.name} + } + const Component = breadcrumb.path ? Link : 'div' const breadcrumbContent = ( - {breadcrumb.name} + {nameElement} {breadcrumb.popover && } ) @@ -118,13 +170,39 @@ function Breadcrumb({ breadcrumb, index, here }: BreadcrumbProps): JSX.Element { } interface HereProps { - breadcrumb: IBreadcrumb + breadcrumb: FinalizedBreadcrumb } function Here({ breadcrumb }: HereProps): JSX.Element { + const { renameState } = useValues(breadcrumbsLogic) + const { tentativelyRename, finishRenaming } = useActions(breadcrumbsLogic) + return (

- {breadcrumb.name || } + {breadcrumb.name == null ? ( + + ) : breadcrumb.onRename ? ( + tentativelyRename(breadcrumb.globalKey, newName)} + onSave={(newName) => { + void breadcrumb.onRename?.(newName) + }} + mode={renameState && renameState[0] === breadcrumb.globalKey ? 'edit' : 'view'} + onModeToggle={(newMode) => { + if (newMode === 'edit') { + tentativelyRename(breadcrumb.globalKey, breadcrumb.name as string) + } else { + finishRenaming() + } + }} + compactButtons="xsmall" + editingIndication="underlined" + /> + ) : ( + {breadcrumb.name} + )}

) } diff --git a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss index f868dd473b6b4..1620ab43cb0ab 100644 --- a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss +++ b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.scss @@ -3,26 +3,38 @@ } .KeyboardShortcut__key { - display: inline-flex; align-items: center; - justify-content: center; - height: 1.25rem; - min-width: 1.25rem; - padding: 0 0.1875rem; + background: var(--accent-3000); border-radius: 0.125rem; border-width: 1px; - background: var(--accent-3000); color: var(--default); + display: inline-flex; + height: 1.25rem; + justify-content: center; + min-width: 1.25rem; + padding: 0 0.1875rem; text-transform: capitalize; + + .posthog-3000 & { + border-bottom-width: 2px; + border-color: var(--secondary-3000-button-border-hover); + border-radius: 0.25rem; + font-size: 0.625rem; + padding: 0.125rem 0.25rem; + text-transform: uppercase; + } + .KeyboardShortcut--muted > & { background: none; color: var(--muted); } + .ant-tooltip & { color: #fff; background: rgba(#000, 0.333); border-color: rgba(#fff, 0.333); } + & + .KeyboardShortcut__key { margin-left: 0.25rem; } diff --git a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.stories.tsx b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.stories.tsx new file mode 100644 index 0000000000000..435e87ebfbdce --- /dev/null +++ b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.stories.tsx @@ -0,0 +1,27 @@ +import { Meta } from '@storybook/react' + +import { KeyboardShortcut } from './KeyboardShortcut' + +const meta: Meta = { + title: 'PostHog 3000/Keyboard Shortcut', + component: KeyboardShortcut, + tags: ['autodocs'], +} +export default meta + +export const Default = { + args: { + cmd: true, + shift: true, + k: true, + }, +} + +export const Muted = { + args: { + muted: true, + cmd: true, + shift: true, + k: true, + }, +} diff --git a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.tsx b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.tsx index a636defcd78dd..d6d5b42d7a91d 100644 --- a/frontend/src/layout/navigation-3000/components/KeyboardShortcut.tsx +++ b/frontend/src/layout/navigation-3000/components/KeyboardShortcut.tsx @@ -1,7 +1,9 @@ -import { isMac } from 'lib/utils' -import { HotKeyOrModifier } from '~/types' import './KeyboardShortcut.scss' + import clsx from 'clsx' +import { isMac } from 'lib/utils' + +import { HotKeyOrModifier } from '~/types' const IS_MAC = isMac() const KEY_TO_SYMBOL: Partial> = { @@ -34,11 +36,11 @@ export function KeyboardShortcut({ muted, ...keys }: KeyboardShortcutProps): JSX ) as HotKeyOrModifier[] return ( - + {sortedKeys.map((key) => ( - - {KEY_TO_SYMBOL[key] || key} - + {KEY_TO_SYMBOL[key] || key} ))} ) diff --git a/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx b/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx new file mode 100644 index 0000000000000..1161a4dadc1c8 --- /dev/null +++ b/frontend/src/layout/navigation-3000/components/MinimalNavigation.tsx @@ -0,0 +1,62 @@ +import { IconLogomark } from '@posthog/icons' +import { LemonButton, Lettermark, Popover, ProfilePicture } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { organizationLogic } from 'scenes/organizationLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { navigationLogic } from '~/layout/navigation/navigationLogic' +import { ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' +import { SitePopoverOverlay } from '~/layout/navigation/TopBar/SitePopover' + +export function MinimalNavigation(): JSX.Element { + const { user } = useValues(userLogic) + + const { currentTeam } = useValues(teamLogic) + const { currentOrganization } = useValues(organizationLogic) + + const { isSitePopoverOpen, isProjectSwitcherShown } = useValues(navigationLogic) + const { closeSitePopover, toggleSitePopover, toggleProjectSwitcher, hideProjectSwitcher } = + useActions(navigationLogic) + + return ( + + ) +} diff --git a/frontend/src/layout/navigation-3000/components/Navbar.tsx b/frontend/src/layout/navigation-3000/components/Navbar.tsx index 26619b7fd6cf2..c620d0c9b927f 100644 --- a/frontend/src/layout/navigation-3000/components/Navbar.tsx +++ b/frontend/src/layout/navigation-3000/components/Navbar.tsx @@ -1,20 +1,37 @@ +import { IconAsterisk, IconDay, IconGear, IconNight, IconSearch } from '@posthog/icons' import { LemonBadge } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { HelpButton } from 'lib/components/HelpButton/HelpButton' -import { IconQuestion, IconGear, IconDay, IconNight, IconAsterisk } from '@posthog/icons' +import { commandBarLogic } from 'lib/components/CommandBar/commandBarLogic' +import { Resizer } from 'lib/components/Resizer/Resizer' import { Popover } from 'lib/lemon-ui/Popover' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useRef } from 'react' import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + import { navigationLogic } from '~/layout/navigation/navigationLogic' import { SitePopoverOverlay } from '~/layout/navigation/TopBar/SitePopover' + import { navigation3000Logic } from '../navigationLogic' import { themeLogic } from '../themeLogic' import { NavbarButton } from './NavbarButton' -import { urls } from 'scenes/urls' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { Resizer } from 'lib/components/Resizer/Resizer' -import { useRef } from 'react' + +export function ThemeIcon(): JSX.Element { + const { isDarkModeOn, isThemeSyncedWithSystem } = useValues(themeLogic) + + const activeThemeIcon = isDarkModeOn ? : + + return isThemeSyncedWithSystem ? ( +
+ {activeThemeIcon} + } /> +
+ ) : ( + activeThemeIcon + ) +} export function Navbar(): JSX.Element { const { user } = useValues(userLogic) @@ -22,12 +39,10 @@ export function Navbar(): JSX.Element { const { closeSitePopover, toggleSitePopover } = useActions(navigationLogic) const { isSidebarShown, activeNavbarItemId, navbarItems } = useValues(navigation3000Logic) const { showSidebar, hideSidebar, toggleNavCollapsed } = useActions(navigation3000Logic) - const { isDarkModeOn, darkModeSavedPreference, darkModeSystemPreference, isThemeSyncedWithSystem } = - useValues(themeLogic) + const { darkModeSavedPreference, darkModeSystemPreference } = useValues(themeLogic) const { toggleTheme } = useActions(themeLogic) const { featureFlags } = useValues(featureFlagLogic) - - const activeThemeIcon = isDarkModeOn ? : + const { toggleSearchBar } = useActions(commandBarLogic) const containerRef = useRef(null) @@ -67,16 +82,14 @@ export function Navbar(): JSX.Element {
    - {activeThemeIcon} - } /> -
- ) : ( - activeThemeIcon - ) - } + identifier="search-button" + icon={} + title="Search" + onClick={toggleSearchBar} + keyboardShortcut={{ command: true, k: true }} + /> + } identifier="theme-button" title={ darkModeSavedPreference === false @@ -91,23 +104,13 @@ export function Navbar(): JSX.Element { onClick={() => toggleTheme()} persistentTooltip /> - } - identifier="help-button" - title="Need any help?" - shortTitle="Help" - /> - } - placement="right-end" - /> } identifier={Scene.Settings} title="Project settings" to={urls.settings('project')} /> + } visible={isSitePopoverOpen} diff --git a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx index 72e07508a9ebf..51996b0419c3f 100644 --- a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx +++ b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx @@ -1,13 +1,16 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import React, { FunctionComponent, ReactElement, useState } from 'react' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { LemonTag } from '@posthog/lemon-ui' import clsx from 'clsx' import { useValues } from 'kea' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import React, { FunctionComponent, ReactElement, useState } from 'react' import { sceneLogic } from 'scenes/sceneLogic' + import { SidebarChangeNoticeContent, useSidebarChangeNotices } from '~/layout/navigation/SideBar/SidebarChangeNotice' + import { navigation3000Logic } from '../navigationLogic' -import { LemonTag } from '@posthog/lemon-ui' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { KeyboardShortcut, KeyboardShortcutProps } from './KeyboardShortcut' export interface NavbarButtonProps { identifier: string @@ -19,92 +22,107 @@ export interface NavbarButtonProps { to?: string persistentTooltip?: boolean active?: boolean + keyboardShortcut?: KeyboardShortcutProps } export const NavbarButton: FunctionComponent = React.forwardRef< HTMLButtonElement, NavbarButtonProps ->(({ identifier, shortTitle, title, tag, onClick, persistentTooltip, ...buttonProps }, ref): JSX.Element => { - const { aliasedActiveScene } = useValues(sceneLogic) - const { isNavCollapsed } = useValues(navigation3000Logic) - const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV') +>( + ( + { identifier, shortTitle, title, tag, onClick, persistentTooltip, keyboardShortcut, ...buttonProps }, + ref + ): JSX.Element => { + const { aliasedActiveScene } = useValues(sceneLogic) + const { isNavCollapsed } = useValues(navigation3000Logic) + const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV') - const [hasBeenClicked, setHasBeenClicked] = useState(false) + const [hasBeenClicked, setHasBeenClicked] = useState(false) - const here = aliasedActiveScene === identifier - const isNavCollapsedActually = isNavCollapsed || isUsingNewNav + const here = aliasedActiveScene === identifier + const isNavCollapsedActually = isNavCollapsed || isUsingNewNav - if (!isUsingNewNav) { - buttonProps.active = here - } + if (!isUsingNewNav) { + buttonProps.active = here + } - let content: JSX.Element | string | undefined - if (!isNavCollapsedActually) { - content = shortTitle || title - if (tag) { - if (tag === 'alpha') { - content = ( - <> - {content} - - ALPHA - - - ) - } else if (tag === 'beta') { - content = ( - <> - {content} - - BETA - - - ) + let content: JSX.Element | string | undefined + if (!isNavCollapsedActually) { + content = shortTitle || title + if (tag) { + if (tag === 'alpha') { + content = ( + <> + {content} + + ALPHA + + + ) + } else if (tag === 'beta') { + content = ( + <> + {content} + + BETA + + + ) + } } } - } - const buttonContent = ( - setHasBeenClicked(false)} - onClick={() => { - setHasBeenClicked(true) - onClick?.() - }} - className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')} - fullWidth - {...buttonProps} - > - {content} - - ) + const buttonContent = ( + setHasBeenClicked(false)} + onClick={() => { + setHasBeenClicked(true) + onClick?.() + }} + className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')} + fullWidth + type="secondary" + stealth={true} + sideIcon={ + !isNavCollapsedActually && keyboardShortcut ? ( + + + + ) : null + } + {...buttonProps} + > + {content} + + ) - const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier }) + const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier }) - return ( -
  • - {notices.length ? ( - } - placement={notices[0].placement ?? 'right'} - delayMs={0} - visible={true} - > - {buttonContent} - - ) : ( - - {buttonContent} - - )} -
  • - ) -}) + return ( +
  • + {notices.length ? ( + } + placement={notices[0].placement ?? 'right'} + delayMs={0} + visible={true} + > + {buttonContent} + + ) : ( + + {buttonContent} + + )} +
  • + ) + } +) NavbarButton.displayName = 'NavbarButton' diff --git a/frontend/src/layout/navigation-3000/components/NewItemButton.tsx b/frontend/src/layout/navigation-3000/components/NewItemButton.tsx index bad5abf273ce8..57bffcc72b15f 100644 --- a/frontend/src/layout/navigation-3000/components/NewItemButton.tsx +++ b/frontend/src/layout/navigation-3000/components/NewItemButton.tsx @@ -1,8 +1,9 @@ -import { useActions, useValues } from 'kea' -import { SidebarCategory } from '../types' -import { navigation3000Logic } from '../navigationLogic' import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { IconPlus } from 'lib/lemon-ui/icons' + +import { navigation3000Logic } from '../navigationLogic' +import { SidebarCategory } from '../types' import { singularizeCategory } from './SidebarAccordion' export function NewItemButton({ category }: { category: SidebarCategory }): JSX.Element | null { diff --git a/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx b/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx index be58ae746db17..3b58b716c8e73 100644 --- a/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx +++ b/frontend/src/layout/navigation-3000/components/Sidebar.stories.tsx @@ -1,14 +1,16 @@ import { Meta } from '@storybook/react' import { useActions, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' import { useEffect } from 'react' import { Scene } from 'scenes/sceneTypes' + import { useStorybookMocks } from '~/mocks/browser' -import { navigation3000Logic } from '../navigationLogic' -import { Sidebar } from './Sidebar' -import featureFlagsJson from '../../../scenes/feature-flags/__mocks__/feature_flags.json' + import dashboardsJson from '../../../scenes/dashboard/__mocks__/dashboards.json' +import featureFlagsJson from '../../../scenes/feature-flags/__mocks__/feature_flags.json' +import { navigation3000Logic } from '../navigationLogic' import { SidebarNavbarItem } from '../types' -import { FEATURE_FLAGS } from 'lib/constants' +import { Sidebar } from './Sidebar' const meta: Meta = { title: 'PostHog 3000/Sidebar', diff --git a/frontend/src/layout/navigation-3000/components/Sidebar.tsx b/frontend/src/layout/navigation-3000/components/Sidebar.tsx index e2b0ef1adbc7f..c8bff277ca08f 100644 --- a/frontend/src/layout/navigation-3000/components/Sidebar.tsx +++ b/frontend/src/layout/navigation-3000/components/Sidebar.tsx @@ -2,16 +2,17 @@ import { LemonButton, LemonInput } from '@posthog/lemon-ui' import clsx from 'clsx' import { LogicWrapper, useActions, useValues } from 'kea' import { IconClose, IconMagnifier } from 'lib/lemon-ui/icons' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { capitalizeFirstLetter } from 'lib/utils' import React, { useRef, useState } from 'react' +import { useDebouncedCallback } from 'use-debounce' + import { navigation3000Logic } from '../navigationLogic' +import { SidebarLogic, SidebarNavbarItem } from '../types' import { KeyboardShortcut } from './KeyboardShortcut' -import { SidebarAccordion, pluralizeCategory } from './SidebarAccordion' -import { SidebarCategory, SidebarLogic, SidebarNavbarItem } from '../types' -import { Spinner } from 'lib/lemon-ui/Spinner' -import { useDebouncedCallback } from 'use-debounce' -import { SidebarList } from './SidebarList' import { NewItemButton } from './NewItemButton' -import { capitalizeFirstLetter } from 'lib/utils' +import { pluralizeCategory, SidebarAccordion } from './SidebarAccordion' +import { SidebarList } from './SidebarList' /** A small delay that prevents us from making a search request on each key press. */ const SEARCH_DEBOUNCE_MS = 300 @@ -177,7 +178,7 @@ function SidebarContent({ return contents.length !== 1 ? ( <> - {(contents as SidebarCategory[]).map((accordion) => ( + {contents.map((accordion) => ( ))} diff --git a/frontend/src/layout/navigation-3000/components/SidebarAccordion.tsx b/frontend/src/layout/navigation-3000/components/SidebarAccordion.tsx index db9d57ce5f305..03dde39b1921b 100644 --- a/frontend/src/layout/navigation-3000/components/SidebarAccordion.tsx +++ b/frontend/src/layout/navigation-3000/components/SidebarAccordion.tsx @@ -1,11 +1,12 @@ +import { useActions, useValues } from 'kea' import { IconChevronRight } from 'lib/lemon-ui/icons' -import { SidebarCategory } from '../types' import { Spinner } from 'lib/lemon-ui/Spinner' -import { SidebarList } from './SidebarList' +import { capitalizeFirstLetter } from 'lib/utils' + import { navigation3000Logic } from '../navigationLogic' -import { useActions, useValues } from 'kea' +import { SidebarCategory } from '../types' import { NewItemButton } from './NewItemButton' -import { capitalizeFirstLetter } from 'lib/utils' +import { SidebarList } from './SidebarList' interface SidebarAccordionProps { category: SidebarCategory diff --git a/frontend/src/layout/navigation-3000/components/SidebarList.tsx b/frontend/src/layout/navigation-3000/components/SidebarList.tsx index f6d006c32dc3b..7380b85422915 100644 --- a/frontend/src/layout/navigation-3000/components/SidebarList.tsx +++ b/frontend/src/layout/navigation-3000/components/SidebarList.tsx @@ -1,19 +1,20 @@ import { Link, TZLabel } from '@posthog/apps-common' +import { LemonButton, LemonTag, lemonToast } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' import clsx from 'clsx' +import { useActions, useAsyncActions, useValues } from 'kea' import { isDayjs } from 'lib/dayjs' import { IconCheckmark, IconClose, IconEllipsis } from 'lib/lemon-ui/icons' -import { BasicListItem, ExtendedListItem, ExtraListItemContext, SidebarCategory, TentativeListItem } from '../types' -import React, { useEffect, useMemo, useRef, useState } from 'react' import { LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { LemonButton, LemonTag, lemonToast } from '@posthog/lemon-ui' -import { ITEM_KEY_PART_SEPARATOR, navigation3000Logic } from '../navigationLogic' -import { captureException } from '@sentry/react' -import { KeyboardShortcut } from './KeyboardShortcut' -import { List, ListProps } from 'react-virtualized/dist/es/List' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' import { InfiniteLoader } from 'react-virtualized/dist/es/InfiniteLoader' -import { useActions, useAsyncActions, useValues } from 'kea' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { List, ListProps } from 'react-virtualized/dist/es/List' + +import { ITEM_KEY_PART_SEPARATOR, navigation3000Logic } from '../navigationLogic' +import { BasicListItem, ExtendedListItem, ExtraListItemContext, SidebarCategory, TentativeListItem } from '../types' +import { KeyboardShortcut } from './KeyboardShortcut' export function SidebarList({ category }: { category: SidebarCategory }): JSX.Element { const { normalizedActiveListItemKey, sidebarWidth, newItemInlineCategory, savingNewItem } = @@ -307,7 +308,7 @@ function SidebarListItem({ item, validateName, active, style }: SidebarListItemP navigation3000Logic.actions.focusPreviousItem() e.preventDefault() } else if (e.key === 'Enter') { - save(newName || '').then(() => { + void save(newName || '').then(() => { // In the keyboard nav experience, we need to refocus the item once it's a link again setTimeout(() => ref.current?.focus(), 0) }) @@ -327,7 +328,7 @@ function SidebarListItem({ item, validateName, active, style }: SidebarListItemP }} onBlur={(e) => { if (e.relatedTarget?.ariaLabel === 'Save name') { - save(newName || '') + void save(newName || '') } else { cancel() } diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 5e5c1237c1b8f..31b669ee9a8c6 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -1,35 +1,35 @@ -import { actions, events, kea, listeners, path, props, reducers, selectors } from 'kea' -import { subscriptions } from 'kea-subscriptions' -import { BasicListItem, ExtendedListItem, NavbarItem, SidebarNavbarItem } from './types' - -import type { navigation3000LogicType } from './navigationLogicType' -import { Scene } from 'scenes/sceneTypes' -import React from 'react' -import { captureException } from '@sentry/react' -import { lemonToast } from '@posthog/lemon-ui' -import { router } from 'kea-router' -import { sceneLogic } from 'scenes/sceneLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' import { IconApps, + IconChat, IconDashboard, IconDatabase, IconGraph, IconHome, IconLive, + IconNotebook, IconPeople, IconPieChart, IconRewindPlay, + IconRocket, + IconServer, IconTestTube, IconToggle, IconToolbar, - IconNotebook, - IconRocket, - IconServer, - IconChat, } from '@posthog/icons' +import { lemonToast } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' +import { actions, connect, events, kea, listeners, path, props, reducers, selectors } from 'kea' +import { router } from 'kea-router' +import { subscriptions } from 'kea-subscriptions' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { isNotNil } from 'lib/utils' +import React from 'react' +import { sceneLogic } from 'scenes/sceneLogic' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + +import type { navigation3000LogicType } from './navigationLogicType' import { dashboardsSidebarLogic } from './sidebars/dashboards' import { dataManagementSidebarLogic } from './sidebars/dataManagement' import { experimentsSidebarLogic } from './sidebars/experiments' @@ -37,11 +37,13 @@ import { featureFlagsSidebarLogic } from './sidebars/featureFlags' import { insightsSidebarLogic } from './sidebars/insights' import { personsAndGroupsSidebarLogic } from './sidebars/personsAndGroups' import { toolbarSidebarLogic } from './sidebars/toolbar' -import { isNotNil } from 'lib/utils' +import { BasicListItem, ExtendedListItem, NavbarItem, SidebarNavbarItem } from './types' /** Multi-segment item keys are joined using this separator for easy comparisons. */ export const ITEM_KEY_PART_SEPARATOR = '::' +export type Navigation3000Mode = 'none' | 'minimal' | 'full' + const MINIMUM_SIDEBAR_WIDTH_PX: number = 192 const DEFAULT_SIDEBAR_WIDTH_PX: number = 288 const MAXIMUM_SIDEBAR_WIDTH_PX: number = 1024 @@ -50,6 +52,9 @@ const MAXIMUM_SIDEBAR_WIDTH_PERCENTAGE: number = 50 export const navigation3000Logic = kea([ path(['layout', 'navigation-3000', 'navigationLogic']), props({} as { inputElement?: HTMLInputElement | null }), + connect(() => ({ + values: [sceneLogic, ['sceneConfig']], + })), actions({ hideSidebar: true, showSidebar: (newNavbarItemId?: string) => ({ newNavbarItemId }), @@ -278,6 +283,16 @@ export const navigation3000Logic = kea([ }, })), selectors({ + mode: [ + (s) => [s.sceneConfig], + (sceneConfig): Navigation3000Mode => { + return sceneConfig?.layout === 'plain' && !sceneConfig.allowUnauthenticated + ? 'minimal' + : sceneConfig?.layout !== 'plain' + ? 'full' + : 'none' + }, + ], navbarItems: [ () => [featureFlagLogic.selectors.featureFlags], (featureFlags): NavbarItem[][] => { diff --git a/frontend/src/layout/navigation-3000/sidebars/annotations.tsx b/frontend/src/layout/navigation-3000/sidebars/annotations.tsx index 1db0c56ecc48d..f563a924f4f05 100644 --- a/frontend/src/layout/navigation-3000/sidebars/annotations.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/annotations.tsx @@ -1,16 +1,18 @@ +import { urls } from '@posthog/apps-common' +import Fuse from 'fuse.js' import { connect, kea, path, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { AnnotationModal } from 'scenes/annotations/AnnotationModal' +import { annotationModalLogic } from 'scenes/annotations/annotationModalLogic' import { sceneLogic } from 'scenes/sceneLogic' + +import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' import { annotationsModel } from '~/models/annotationsModel' -import { SidebarCategory, ExtendedListItem } from '../types' +import { AnnotationType } from '~/types' + +import { ExtendedListItem, SidebarCategory } from '../types' import type { annotationsSidebarLogicType } from './annotationsType' -import Fuse from 'fuse.js' -import { subscriptions } from 'kea-subscriptions' -import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' import { FuseSearchMatch } from './utils' -import { AnnotationType } from '~/types' -import { urls } from '@posthog/apps-common' -import { AnnotationModal } from 'scenes/annotations/AnnotationModal' -import { annotationModalLogic } from 'scenes/annotations/annotationModalLogic' const fuse = new Fuse([], { keys: [{ name: 'content', weight: 2 }, 'date_marker'], diff --git a/frontend/src/layout/navigation-3000/sidebars/cohorts.ts b/frontend/src/layout/navigation-3000/sidebars/cohorts.ts index 5206087f6e37f..cb5c16bf4f323 100644 --- a/frontend/src/layout/navigation-3000/sidebars/cohorts.ts +++ b/frontend/src/layout/navigation-3000/sidebars/cohorts.ts @@ -1,17 +1,19 @@ +import { api } from '@posthog/apps-common' +import Fuse from 'fuse.js' import { connect, kea, path, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { dayjs } from 'lib/dayjs' +import { pluralize } from 'lib/utils' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { ExtendedListItem, SidebarCategory } from '../types' -import type { cohortsSidebarLogicType } from './cohortsType' -import Fuse from 'fuse.js' -import { CohortType } from '~/types' -import { subscriptions } from 'kea-subscriptions' + import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' import { cohortsModel } from '~/models/cohortsModel' -import { pluralize } from 'lib/utils' -import { dayjs } from 'lib/dayjs' -import { api } from '@posthog/apps-common' +import { CohortType } from '~/types' + +import { ExtendedListItem, SidebarCategory } from '../types' +import type { cohortsSidebarLogicType } from './cohortsType' import { FuseSearchMatch } from './utils' const fuse = new Fuse([], { diff --git a/frontend/src/layout/navigation-3000/sidebars/dashboards.tsx b/frontend/src/layout/navigation-3000/sidebars/dashboards.tsx index 13ae4ee92a0e9..aa313b8713f6a 100644 --- a/frontend/src/layout/navigation-3000/sidebars/dashboards.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/dashboards.tsx @@ -1,22 +1,24 @@ -import { connect, kea, path, selectors } from 'kea' -import { sceneLogic } from 'scenes/sceneLogic' -import { Scene } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' -import { dashboardsModel } from '~/models/dashboardsModel' -import { SidebarCategory, BasicListItem } from '../types' -import type { dashboardsSidebarLogicType } from './dashboardsType' import Fuse from 'fuse.js' -import { DashboardBasicType, DashboardType } from '~/types' +import { connect, kea, path, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' -import { DashboardMode } from '~/types' import { DashboardEventSource } from 'lib/utils/eventUsageLogic' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' +import { sceneLogic } from 'scenes/sceneLogic' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' +import { dashboardsModel } from '~/models/dashboardsModel' +import { DashboardBasicType, DashboardType } from '~/types' +import { DashboardMode } from '~/types' + +import { BasicListItem, SidebarCategory } from '../types' +import type { dashboardsSidebarLogicType } from './dashboardsType' import { FuseSearchMatch } from './utils' -import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' const fuse = new Fuse([], { keys: [{ name: 'name', weight: 2 }, 'description', 'tags'], diff --git a/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts b/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts index 3c97bfb557d9a..faaa2ee4c7e2e 100644 --- a/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts +++ b/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts @@ -1,17 +1,19 @@ import { actions, afterMount, connect, kea, path, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { subscriptions } from 'kea-subscriptions' +import api from 'lib/api' +import { getPropertyLabel } from 'lib/taxonomy' +import { actionsFuse, actionsLogic } from 'scenes/actions/actionsLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SidebarCategory, BasicListItem } from '../types' + import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' +import { ActionType, EventDefinition, PropertyDefinition, ReplayTabs } from '~/types' + +import { BasicListItem, SidebarCategory } from '../types' import type { dataManagementSidebarLogicType } from './dataManagementType' import { findSearchTermInItemName } from './utils' -import { loaders } from 'kea-loaders' -import { ActionType, EventDefinition, PropertyDefinition, ReplayTabs } from '~/types' -import api from 'lib/api' -import { subscriptions } from 'kea-subscriptions' -import { getPropertyLabel } from 'lib/taxonomy' -import { actionsFuse, actionsLogic } from 'scenes/actions/actionsLogic' import { FuseSearchMatch } from './utils' export const dataManagementSidebarLogic = kea([ diff --git a/frontend/src/layout/navigation-3000/sidebars/experiments.ts b/frontend/src/layout/navigation-3000/sidebars/experiments.ts index 9d2dc2ae03515..de4ca1e8f01ae 100644 --- a/frontend/src/layout/navigation-3000/sidebars/experiments.ts +++ b/frontend/src/layout/navigation-3000/sidebars/experiments.ts @@ -1,16 +1,18 @@ +import Fuse from 'fuse.js' import { connect, kea, path, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { dayjs } from 'lib/dayjs' +import { experimentsLogic, getExperimentStatus } from 'scenes/experiments/experimentsLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SidebarCategory, ExtendedListItem } from '../types' -import Fuse from 'fuse.js' -import { subscriptions } from 'kea-subscriptions' -import { navigation3000Logic } from '../navigationLogic' -import { FuseSearchMatch } from './utils' + import { Experiment, ProgressStatus } from '~/types' + +import { navigation3000Logic } from '../navigationLogic' +import { ExtendedListItem, SidebarCategory } from '../types' import type { experimentsSidebarLogicType } from './experimentsType' -import { experimentsLogic, getExperimentStatus } from 'scenes/experiments/experimentsLogic' -import { dayjs } from 'lib/dayjs' +import { FuseSearchMatch } from './utils' const fuse = new Fuse([], { keys: [{ name: 'name', weight: 2 }, 'description'], diff --git a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx index 13435fc8b1d6d..16acfa8f807e5 100644 --- a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx @@ -1,21 +1,24 @@ -import { dayjs } from 'lib/dayjs' +import Fuse from 'fuse.js' import { connect, kea, path, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { dayjs } from 'lib/dayjs' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { groupFilters } from 'scenes/feature-flags/FeatureFlags' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' -import { SidebarCategory, ExtendedListItem } from '../types' -import type { featureFlagsSidebarLogicType } from './featureFlagsType' -import Fuse from 'fuse.js' + +import { groupsModel } from '~/models/groupsModel' import { FeatureFlagType } from '~/types' -import { subscriptions } from 'kea-subscriptions' -import { copyToClipboard, deleteWithUndo } from 'lib/utils' -import { teamLogic } from 'scenes/teamLogic' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' + import { navigation3000Logic } from '../navigationLogic' +import { ExtendedListItem, SidebarCategory } from '../types' +import type { featureFlagsSidebarLogicType } from './featureFlagsType' import { FuseSearchMatch } from './utils' -import { groupsModel } from '~/models/groupsModel' const fuse = new Fuse([], { // Note: For feature flags `name` is the description field @@ -78,7 +81,7 @@ export const featureFlagsSidebarLogic = kea([ items: [ { label: 'Edit', - to: urls.featureFlag(featureFlag.id as number), + to: urls.featureFlag(featureFlag.id), onClick: () => { featureFlagLogic({ id: featureFlag.id as number }).mount() featureFlagLogic({ @@ -106,8 +109,8 @@ export const featureFlagsSidebarLogic = kea([ }, { label: 'Copy flag key', - onClick: async () => { - await copyToClipboard(featureFlag.key, 'feature flag key') + onClick: () => { + void copyToClipboard(featureFlag.key, 'feature flag key') }, }, { @@ -128,7 +131,7 @@ export const featureFlagsSidebarLogic = kea([ { label: 'Delete feature flag', onClick: () => { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: actions.loadFeatureFlags, diff --git a/frontend/src/layout/navigation-3000/sidebars/insights.ts b/frontend/src/layout/navigation-3000/sidebars/insights.ts index d89d7e310a0a1..577a2179c2739 100644 --- a/frontend/src/layout/navigation-3000/sidebars/insights.ts +++ b/frontend/src/layout/navigation-3000/sidebars/insights.ts @@ -1,18 +1,20 @@ +import { api } from '@posthog/apps-common' import { afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { INSIGHTS_PER_PAGE, savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' -import { SidebarCategory, BasicListItem } from '../types' -import { InsightModel } from '~/types' -import { subscriptions } from 'kea-subscriptions' + import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' -import { INSIGHTS_PER_PAGE, savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { insightsModel } from '~/models/insightsModel' +import { InsightModel } from '~/types' + +import { BasicListItem, SidebarCategory } from '../types' import type { insightsSidebarLogicType } from './insightsType' import { findSearchTermInItemName } from './utils' -import { deleteWithUndo } from 'lib/utils' -import { teamLogic } from 'scenes/teamLogic' -import { api } from '@posthog/apps-common' -import { insightsModel } from '~/models/insightsModel' export const insightsSidebarLogic = kea([ path(['layout', 'navigation-3000', 'sidebars', 'insightsSidebarLogic']), @@ -86,7 +88,7 @@ export const insightsSidebarLogic = kea([ }, { onClick: () => { - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: actions.loadInsights, @@ -116,7 +118,7 @@ export const insightsSidebarLogic = kea([ for (let i = startIndex; i < startIndex + INSIGHTS_PER_PAGE; i++) { cache.requestedInsights[i] = true } - await savedInsightsLogic.actions.setSavedInsightsFilters( + await savedInsightsLogic.asyncActions.setSavedInsightsFilters( { page: Math.floor(startIndex / INSIGHTS_PER_PAGE) + 1 }, true, false diff --git a/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts b/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts index 2cefac31cf5ba..724d9c704684d 100644 --- a/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts +++ b/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts @@ -1,19 +1,21 @@ +import { urls } from '@posthog/apps-common' import { afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { combineUrl } from 'kea-router' +import { subscriptions } from 'kea-subscriptions' +import { groupsListLogic, GroupsPaginatedResponse } from 'scenes/groups/groupsListLogic' +import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { asDisplay, asLink } from 'scenes/persons/person-utils' +import { personsLogic } from 'scenes/persons/personsLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' -import type { personsAndGroupsSidebarLogicType } from './personsAndGroupsType' -import { personsLogic } from 'scenes/persons/personsLogic' -import { subscriptions } from 'kea-subscriptions' -import { navigation3000Logic } from '../navigationLogic' -import { SidebarCategory, BasicListItem } from '../types' -import { urls } from '@posthog/apps-common' -import { findSearchTermInItemName } from './utils' + import { groupsModel } from '~/models/groupsModel' -import { GroupsPaginatedResponse, groupsListLogic } from 'scenes/groups/groupsListLogic' -import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { combineUrl } from 'kea-router' import { PersonType } from '~/types' -import { asDisplay, asLink } from 'scenes/persons/person-utils' + +import { navigation3000Logic } from '../navigationLogic' +import { BasicListItem, SidebarCategory } from '../types' +import type { personsAndGroupsSidebarLogicType } from './personsAndGroupsType' +import { findSearchTermInItemName } from './utils' export const personsAndGroupsSidebarLogic = kea([ path(['layout', 'navigation-3000', 'sidebars', 'personsAndGroupsSidebarLogic']), @@ -98,16 +100,17 @@ export const personsAndGroupsSidebarLogic = kea { - const { searchTerm } = values - const displayId = groupDisplayId(group.group_key, group.group_properties) - return { - key: group.group_key, - name: displayId, - url: urls.group(groupType.group_type_index, group.group_key), - searchMatch: findSearchTermInItemName(displayId, searchTerm), - } as BasicListItem - }), + items: + groups[groupType.group_type_index]?.results.map((group) => { + const { searchTerm } = values + const displayId = groupDisplayId(group.group_key, group.group_properties) + return { + key: group.group_key, + name: displayId, + url: urls.group(groupType.group_type_index, group.group_key), + searchMatch: findSearchTermInItemName(displayId, searchTerm), + } as BasicListItem + }) || [], loading: groupsLoading[groupType.group_type_index], // FIXME: Add remote } as SidebarCategory) diff --git a/frontend/src/layout/navigation-3000/sidebars/toolbar.ts b/frontend/src/layout/navigation-3000/sidebars/toolbar.ts index 61875332d6981..dfb417aaf6b3b 100644 --- a/frontend/src/layout/navigation-3000/sidebars/toolbar.ts +++ b/frontend/src/layout/navigation-3000/sidebars/toolbar.ts @@ -1,21 +1,22 @@ -import { connect, kea, path, selectors } from 'kea' -import { sceneLogic } from 'scenes/sceneLogic' -import { Scene } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' -import { SidebarCategory, BasicListItem } from '../types' import Fuse from 'fuse.js' +import { connect, kea, path, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' -import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' -import { FuseSearchMatch } from './utils' import { + authorizedUrlListLogic, AuthorizedUrlListType, KeyedAppUrl, - authorizedUrlListLogic, validateProposedUrl, } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { sceneLogic } from 'scenes/sceneLogic' +import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' + +import { BasicListItem, SidebarCategory } from '../types' import type { toolbarSidebarLogicType } from './toolbarType' -import { teamLogic } from 'scenes/teamLogic' +import { FuseSearchMatch } from './utils' const fuse = new Fuse([], { keys: ['url'], diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss index 4f3d08f139753..30c14c722c8d4 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss @@ -17,7 +17,7 @@ top: 0; right: 0; max-width: calc(100vw - 3rem); - box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2); + box-shadow: 0 0 30px rgb(0 0 0 / 20%); [theme='dark'] & { box-shadow: none; diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index d011a0a6561de..d3a9ecb2fd9f3 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -1,18 +1,21 @@ -import { LemonButton } from '@posthog/lemon-ui' import './SidePanel.scss' -import { useActions, useValues } from 'kea' -import { sidePanelLogic } from './sidePanelLogic' + +import { IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons' +import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { Resizer } from 'lib/components/Resizer/Resizer' +import { resizerLogic, ResizerLogicProps } from 'lib/components/Resizer/resizerLogic' import { useRef } from 'react' -import { ResizerLogicProps, resizerLogic } from 'lib/components/Resizer/resizerLogic' -import { IconNotebook, IconInfo, IconSupport, IconGear } from '@posthog/icons' -import { SidePanelDocs } from './panels/SidePanelDocs' -import { SidePanelSupport } from './panels/SidePanelSupport' import { NotebookPanel } from 'scenes/notebooks/NotebookPanel/NotebookPanel' + +import { SidePanelTab } from '~/types' + import { SidePanelActivation, SidePanelActivationIcon } from './panels/SidePanelActivation' +import { SidePanelDocs } from './panels/SidePanelDocs' import { SidePanelSettings } from './panels/SidePanelSettings' -import { SidePanelTab } from '~/types' +import { SidePanelSupport } from './panels/SidePanelSupport' +import { sidePanelLogic } from './sidePanelLogic' import { sidePanelStateLogic } from './sidePanelStateLogic' export const SidePanelTabs: Record = { @@ -98,6 +101,8 @@ export function SidePanel(): JSX.Element | null { } data-attr={`sidepanel-tab-${tab}`} active={activeTab === tab} + type="secondary" + stealth={true} > {label} diff --git a/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPane.tsx b/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPane.tsx index db6cab4ca16b8..7bc9a9e19e3ca 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPane.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPane.tsx @@ -1,6 +1,7 @@ import { LemonButton, Tooltip } from '@posthog/lemon-ui' import { useActions } from 'kea' import { IconClose } from 'lib/lemon-ui/icons' + import { sidePanelStateLogic } from '../sidePanelStateLogic' export function SidePanelPaneHeader({ children }: { children: React.ReactNode }): JSX.Element { diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx index f6a137cc850ee..5cae81b23ae69 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx @@ -1,9 +1,9 @@ import { useValues } from 'kea' +import { activationLogic, ActivationTaskType } from 'lib/components/ActivationSidebar/activationLogic' import { ActivationTask } from 'lib/components/ActivationSidebar/ActivationSidebar' -import { ActivationTaskType, activationLogic } from 'lib/components/ActivationSidebar/activationLogic' import { ProfessorHog } from 'lib/components/hedgehogs' -import { LemonProgressCircle } from 'lib/lemon-ui/LemonProgressCircle' import { LemonIconProps } from 'lib/lemon-ui/icons' +import { LemonProgressCircle } from 'lib/lemon-ui/LemonProgressCircle' export const SidePanelActivation = (): JSX.Element => { const { activeTasks, completionPercent, completedTasks } = useValues(activationLogic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx index 413219b69f33c..e080f49c67f26 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelDocs.tsx @@ -1,11 +1,12 @@ +import { IconExternal } from '@posthog/icons' +import { LemonButton, LemonSkeleton } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { POSTHOG_WEBSITE_ORIGIN, sidePanelDocsLogic } from './sidePanelDocsLogic' import { useEffect, useRef, useState } from 'react' -import clsx from 'clsx' -import { SidePanelPaneHeader } from '../components/SidePanelPane' -import { LemonButton, LemonSkeleton } from '@posthog/lemon-ui' -import { IconExternal } from '@posthog/icons' + import { themeLogic } from '../../themeLogic' +import { SidePanelPaneHeader } from '../components/SidePanelPane' +import { POSTHOG_WEBSITE_ORIGIN, sidePanelDocsLogic } from './sidePanelDocsLogic' function SidePanelDocsSkeleton(): JSX.Element { return ( diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx index 4ed5684861719..78dc0dc520dcf 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx @@ -1,12 +1,14 @@ +import { IconExternal } from '@posthog/icons' +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { sidePanelSettingsLogic } from './sidePanelSettingsLogic' +import { useEffect } from 'react' import { Settings } from 'scenes/settings/Settings' -import { LemonButton } from '@posthog/lemon-ui' +import { settingsLogic } from 'scenes/settings/settingsLogic' +import { SettingsLogicProps } from 'scenes/settings/types' import { urls } from 'scenes/urls' -import { SettingsLogicProps, settingsLogic } from 'scenes/settings/settingsLogic' -import { useEffect } from 'react' + import { SidePanelPaneHeader } from '../components/SidePanelPane' -import { IconExternal } from '@posthog/icons' +import { sidePanelSettingsLogic } from './sidePanelSettingsLogic' export const SidePanelSettings = (): JSX.Element => { const { settings } = useValues(sidePanelSettingsLogic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx index 7c028e8bd497e..e36e7661311de 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx @@ -1,11 +1,13 @@ +import { LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { SupportForm, SupportFormButtons } from 'lib/components/Support/SupportForm' import { supportLogic } from 'lib/components/Support/supportLogic' import { useEffect } from 'react' -import { LemonDivider } from '@posthog/lemon-ui' -import { sidePanelStateLogic } from '../sidePanelStateLogic' + import { SidePanelTab } from '~/types' +import { sidePanelStateLogic } from '../sidePanelStateLogic' + export const SidePanelSupport = (): JSX.Element => { const { closeSidePanel } = useActions(sidePanelStateLogic) const { selectedTab } = useValues(sidePanelStateLogic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts index 7ec638c7b05a2..fa81eaedc2f32 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts @@ -1,9 +1,10 @@ -import { actions, kea, reducers, path, listeners, connect, selectors } from 'kea' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { router } from 'kea-router' -import type { sidePanelDocsLogicType } from './sidePanelDocsLogicType' -import { sidePanelStateLogic } from '../sidePanelStateLogic' import { SidePanelTab } from '~/types' -import { router } from 'kea-router' + +import { sidePanelStateLogic } from '../sidePanelStateLogic' +import type { sidePanelDocsLogicType } from './sidePanelDocsLogicType' export const POSTHOG_WEBSITE_ORIGIN = 'https://posthog.com' diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx index da07a199f139d..53682a2556b53 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx @@ -1,13 +1,14 @@ -import { actions, kea, reducers, path, listeners, connect } from 'kea' -import { Settings } from 'scenes/settings/Settings' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' import { LemonDialog } from '@posthog/lemon-ui' +import { actions, connect, kea, listeners, path, reducers } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { Settings } from 'scenes/settings/Settings' +import { SettingsLogicProps } from 'scenes/settings/types' -import type { sidePanelSettingsLogicType } from './sidePanelSettingsLogicType' -import { sidePanelStateLogic } from '../sidePanelStateLogic' import { SidePanelTab } from '~/types' -import { SettingsLogicProps } from 'scenes/settings/settingsLogic' + +import { sidePanelStateLogic } from '../sidePanelStateLogic' +import type { sidePanelSettingsLogicType } from './sidePanelSettingsLogicType' export const sidePanelSettingsLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelSettingsLogic']), diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx index b0c46bb1056c5..a5c97b8b897d6 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx @@ -1,11 +1,12 @@ -import { kea, path, selectors, connect } from 'kea' - -import type { sidePanelLogicType } from './sidePanelLogicType' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { connect, kea, path, selectors } from 'kea' +import { activationLogic } from 'lib/components/ActivationSidebar/activationLogic' import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { activationLogic } from 'lib/components/ActivationSidebar/activationLogic' + import { SidePanelTab } from '~/types' + +import type { sidePanelLogicType } from './sidePanelLogicType' import { sidePanelStateLogic } from './sidePanelStateLogic' export const sidePanelLogic = kea([ @@ -35,10 +36,7 @@ export const sidePanelLogic = kea([ tabs.push(SidePanelTab.Support) } - if (featureFlags[FEATURE_FLAGS.SIDE_PANEL_DOCS]) { - tabs.push(SidePanelTab.Docs) - } - + tabs.push(SidePanelTab.Docs) tabs.push(SidePanelTab.Settings) tabs.push(SidePanelTab.Activation) diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx index 2c29443facbc6..773cfaf8d6b81 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx @@ -1,4 +1,5 @@ import { actions, kea, listeners, path, reducers } from 'kea' + import { SidePanelTab } from '~/types' import type { sidePanelStateLogicType } from './sidePanelStateLogicType' diff --git a/frontend/src/layout/navigation-3000/themeLogic.ts b/frontend/src/layout/navigation-3000/themeLogic.ts index 811960d5bf15a..2b90cd27abdee 100644 --- a/frontend/src/layout/navigation-3000/themeLogic.ts +++ b/frontend/src/layout/navigation-3000/themeLogic.ts @@ -2,15 +2,15 @@ import { actions, events, kea, path, reducers, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { sceneLogic } from 'scenes/sceneLogic' import type { themeLogicType } from './themeLogicType' -import { sceneLogic } from 'scenes/sceneLogic' export const themeLogic = kea([ path(['layout', 'navigation-3000', 'themeLogic']), actions({ toggleTheme: true, - overrideTheme: (darkModePreference: boolean) => ({ darkModePreference }), + overrideTheme: (darkModePreference: boolean | null) => ({ darkModePreference }), syncDarkModePreference: (darkModePreference: boolean) => ({ darkModePreference }), }), reducers({ diff --git a/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.scss b/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.scss index 625535c49575e..6141582fea852 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.scss +++ b/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.scss @@ -22,7 +22,7 @@ &--actionable { cursor: pointer; - color: var(--primary); + color: var(--primary-3000); } } diff --git a/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.tsx b/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.tsx index 902f45cc06d0d..b33d8ad0d3324 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.tsx +++ b/frontend/src/layout/navigation/Breadcrumbs/Breadcrumbs.tsx @@ -1,12 +1,15 @@ -import React, { useState } from 'react' +import './Breadcrumbs.scss' + +import clsx from 'clsx' import { useValues } from 'kea' import { IconArrowDropDown, IconChevronRight } from 'lib/lemon-ui/icons' import { Link } from 'lib/lemon-ui/Link' -import './Breadcrumbs.scss' -import { breadcrumbsLogic } from './breadcrumbsLogic' -import { Breadcrumb as IBreadcrumb } from '~/types' -import clsx from 'clsx' import { Popover } from 'lib/lemon-ui/Popover/Popover' +import React, { useState } from 'react' + +import { Breadcrumb as IBreadcrumb } from '~/types' + +import { breadcrumbsLogic } from './breadcrumbsLogic' function Breadcrumb({ breadcrumb, index }: { breadcrumb: IBreadcrumb; index: number }): JSX.Element { const [popoverShown, setPopoverShown] = useState(false) diff --git a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts index dd49842bc5fa7..b19bef66a11fe 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts +++ b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts @@ -1,10 +1,12 @@ -import { breadcrumbsLogic } from './breadcrumbsLogic' -import { initKeaTests } from '~/test/init' -import { expectLogic } from 'kea-test-utils' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { expectLogic } from 'kea-test-utils' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { initKeaTests } from '~/test/init' + +import { breadcrumbsLogic } from './breadcrumbsLogic' const blankScene = (): any => ({ scene: { component: () => null, logic: null } }) const scenes: any = { [Scene.SavedInsights]: blankScene, [Scene.Dashboards]: blankScene } @@ -25,8 +27,8 @@ describe('breadcrumbsLogic', () => { // test with .delay because subscriptions happen async router.actions.push(urls.savedInsights()) - await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Insights • PostHog' }) - expect(global.document.title).toEqual('Insights • PostHog') + await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Product analytics • PostHog' }) + expect(global.document.title).toEqual('Product analytics • PostHog') router.actions.push(urls.dashboards()) await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Dashboards • PostHog' }) diff --git a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx index 1d1739651cb09..85a4e54243d45 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx +++ b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx @@ -1,18 +1,21 @@ -import { actions, connect, kea, path, props, reducers, selectors } from 'kea' -import { organizationLogic } from 'scenes/organizationLogic' -import { teamLogic } from 'scenes/teamLogic' import './Breadcrumbs.scss' -import type { breadcrumbsLogicType } from './breadcrumbsLogicType' -import { sceneLogic } from 'scenes/sceneLogic' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { identifierToHuman, objectsEqual, stripHTTP } from 'lib/utils' -import { userLogic } from 'scenes/userLogic' + +import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' import { Lettermark } from 'lib/lemon-ui/Lettermark' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' +import { identifierToHuman, objectsEqual, stripHTTP } from 'lib/utils' +import { organizationLogic } from 'scenes/organizationLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { sceneLogic } from 'scenes/sceneLogic' +import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + import { OrganizationSwitcherOverlay } from '~/layout/navigation/OrganizationSwitcher' -import { Breadcrumb } from '~/types' -import { subscriptions } from 'kea-subscriptions' +import { ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' +import { Breadcrumb, FinalizedBreadcrumb } from '~/types' + +import type { breadcrumbsLogicType } from './breadcrumbsLogicType' export const breadcrumbsLogic = kea([ path(['layout', 'navigation', 'Breadcrumbs', 'breadcrumbsLogic']), @@ -37,6 +40,11 @@ export const breadcrumbsLogic = kea([ })), actions({ setActionsContainer: (element: HTMLElement | null) => ({ element }), + tentativelyRename: (breadcrumbGlobalKey: string, tentativeName: string) => ({ + breadcrumbGlobalKey, + tentativeName, + }), + finishRenaming: true, }), reducers({ actionsContainer: [ @@ -45,7 +53,17 @@ export const breadcrumbsLogic = kea([ setActionsContainer: (_, { element }) => element, }, ], + renameState: [ + null as [breadcrumbGlobalKey: string, tentativeName: string] | null, + { + tentativelyRename: (_, { breadcrumbGlobalKey, tentativeName }) => [breadcrumbGlobalKey, tentativeName], + finishRenaming: () => null, + }, + ], }), + listeners(({ actions }) => ({ + [sceneLogic.actionTypes.loadScene]: () => actions.finishRenaming(), // Cancel renaming on navigation away + })), selectors(() => ({ sceneBreadcrumbs: [ (s) => [ @@ -94,6 +112,7 @@ export const breadcrumbsLogic = kea([ return breadcrumbs } breadcrumbs.push({ + key: 'me', name: user.first_name, symbol: , }) @@ -104,6 +123,7 @@ export const breadcrumbsLogic = kea([ return breadcrumbs } breadcrumbs.push({ + key: 'instance', name: stripHTTP(preflight.site_url), symbol: , }) @@ -114,6 +134,7 @@ export const breadcrumbsLogic = kea([ return breadcrumbs } breadcrumbs.push({ + key: 'organization', name: currentOrganization.name, symbol: , popover: @@ -131,6 +152,7 @@ export const breadcrumbsLogic = kea([ return breadcrumbs } breadcrumbs.push({ + key: 'project', name: currentTeam.name, popover: { overlay: , @@ -144,8 +166,24 @@ export const breadcrumbsLogic = kea([ ], breadcrumbs: [ (s) => [s.appBreadcrumbs, s.sceneBreadcrumbs], - (appBreadcrumbs, sceneBreadcrumbs) => { - return [...appBreadcrumbs, ...sceneBreadcrumbs] + (appBreadcrumbs, sceneBreadcrumbs): FinalizedBreadcrumb[] => { + const breadcrumbs = Array(appBreadcrumbs.length + sceneBreadcrumbs.length) + const globalPathSoFar: string[] = [] + for (let i = 0; i < appBreadcrumbs.length; i++) { + globalPathSoFar.push(String(appBreadcrumbs[i].key)) + breadcrumbs[i] = { + ...appBreadcrumbs[i], + globalKey: globalPathSoFar.join('.'), + } + } + for (let i = 0; i < sceneBreadcrumbs.length; i++) { + globalPathSoFar.push(String(sceneBreadcrumbs[i].key)) + breadcrumbs[i + appBreadcrumbs.length] = { + ...sceneBreadcrumbs[i], + globalKey: globalPathSoFar.join('.'), + } + } + return breadcrumbs }, ], firstBreadcrumb: [(s) => [s.breadcrumbs], (breadcrumbs) => breadcrumbs[0]], diff --git a/frontend/src/layout/navigation/Navigation.stories.tsx b/frontend/src/layout/navigation/Navigation.stories.tsx index dbb4e54b94bbc..71d2903d628a3 100644 --- a/frontend/src/layout/navigation/Navigation.stories.tsx +++ b/frontend/src/layout/navigation/Navigation.stories.tsx @@ -1,12 +1,13 @@ -import { Meta } from '@storybook/react' -import { TopBar } from './TopBar/TopBar' -import { SideBar } from './SideBar/SideBar' -import { PageHeader } from 'lib/components/PageHeader' import { LemonButton, LemonTable } from '@posthog/lemon-ui' +import { Meta } from '@storybook/react' import { useActions } from 'kea' -import { navigationLogic } from './navigationLogic' +import { PageHeader } from 'lib/components/PageHeader' import { useEffect } from 'react' +import { navigationLogic } from './navigationLogic' +import { SideBar } from './SideBar/SideBar' +import { TopBar } from './TopBar/TopBar' + const meta: Meta = { title: 'Layout/Navigation', parameters: { diff --git a/frontend/src/layout/navigation/Navigation.tsx b/frontend/src/layout/navigation/Navigation.tsx index 57b29956bdde1..5dbdb204a2c5f 100644 --- a/frontend/src/layout/navigation/Navigation.tsx +++ b/frontend/src/layout/navigation/Navigation.tsx @@ -1,24 +1,23 @@ import clsx from 'clsx' import { BillingAlertsV2 } from 'lib/components/BillingAlertsV2' -import { Scene, SceneConfig } from 'scenes/sceneTypes' +import { ReactNode } from 'react' +import { SceneConfig } from 'scenes/sceneTypes' + import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs' import { ProjectNotice } from './ProjectNotice' import { SideBar } from './SideBar/SideBar' import { TopBar } from './TopBar/TopBar' -import { ReactNode } from 'react' export function Navigation({ children, - scene, sceneConfig, }: { children: ReactNode - scene: Scene | null sceneConfig: SceneConfig | null }): JSX.Element { return (
    - {scene !== Scene.Ingestion && } +
    { - updateCurrentTeam(altTeamForIngestion?.id, urls.ingestion()) + updateCurrentTeam(altTeamForIngestion?.id, urls.products()) }} data-attr="demo-project-alt-team-ingestion_link" > - ingestion wizard + onboarding wizard {' '} to get started with your own data. @@ -60,8 +63,11 @@ export function ProjectNotice(): JSX.Element | null { message: ( <> This project has no events yet. Go to the{' '} - - ingestion wizard + + onboarding wizard {' '} or grab your project API key/HTML snippet from{' '} @@ -71,7 +77,7 @@ export function ProjectNotice(): JSX.Element | null { ), action: { - to: '/ingestion', + to: urls.onboarding(ProductKey.PRODUCT_ANALYTICS), 'data-attr': 'demo-warning-cta', icon: , children: 'Go to wizard', diff --git a/frontend/src/layout/navigation/ProjectSwitcher.tsx b/frontend/src/layout/navigation/ProjectSwitcher.tsx index 25f426f7a0e38..a552fe00b5b13 100644 --- a/frontend/src/layout/navigation/ProjectSwitcher.tsx +++ b/frontend/src/layout/navigation/ProjectSwitcher.tsx @@ -9,7 +9,9 @@ import { sceneLogic } from 'scenes/sceneLogic' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + import { AvailableFeature, TeamBasicType } from '~/types' + import { globalModalsLogic } from '../GlobalModals' export function ProjectName({ team }: { team: TeamBasicType }): JSX.Element { diff --git a/frontend/src/layout/navigation/SideBar/PageButton.tsx b/frontend/src/layout/navigation/SideBar/PageButton.tsx index 1e8fafeca07de..0086001ada2e2 100644 --- a/frontend/src/layout/navigation/SideBar/PageButton.tsx +++ b/frontend/src/layout/navigation/SideBar/PageButton.tsx @@ -1,12 +1,13 @@ import { useActions, useValues } from 'kea' -import { sceneLogic } from 'scenes/sceneLogic' -import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { dashboardsModel } from '~/models/dashboardsModel' -import { Scene } from 'scenes/sceneTypes' import { LemonButton, LemonButtonProps, LemonButtonWithSideAction, SideAction } from 'lib/lemon-ui/LemonButton' -import { sceneConfigurations } from 'scenes/scenes' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { sceneLogic } from 'scenes/sceneLogic' +import { sceneConfigurations } from 'scenes/scenes' +import { Scene } from 'scenes/sceneTypes' + +import { navigationLogic } from '~/layout/navigation/navigationLogic' import { SidebarChangeNoticeTooltip } from '~/layout/navigation/SideBar/SidebarChangeNotice' +import { dashboardsModel } from '~/models/dashboardsModel' export interface PageButtonProps extends Pick { /** Used for highlighting the active scene. `identifier` of type number means dashboard ID instead of scene. */ diff --git a/frontend/src/layout/navigation/SideBar/SideBar.scss b/frontend/src/layout/navigation/SideBar/SideBar.scss index eb2db9103bb60..32576a7fb18db 100644 --- a/frontend/src/layout/navigation/SideBar/SideBar.scss +++ b/frontend/src/layout/navigation/SideBar/SideBar.scss @@ -31,9 +31,11 @@ // because the sidebar does not affect the rest of the layout on mobile transform: translateX(-15.5rem); } + @include screen($lg) { height: initial; position: relative; + .SideBar--hidden & { margin-left: -15.5rem; } @@ -49,6 +51,7 @@ > ul { overflow: auto; padding: 1rem 0.5rem; + li { margin-top: 1px; } @@ -64,15 +67,15 @@ position: absolute; height: 100%; width: 100%; - background-color: var(--modal-backdrop-color); backdrop-filter: blur(var(--modal-backdrop-blur)); .SideBar--hidden & { background-color: transparent; - backdrop-filter: blur(0px); + backdrop-filter: blur(0); pointer-events: none; } + @include screen($lg) { display: none; } @@ -87,6 +90,7 @@ text-transform: uppercase; letter-spacing: 0.5px; margin-top: 1rem; + &:first-of-type { margin-top: 0; } diff --git a/frontend/src/layout/navigation/SideBar/SideBar.tsx b/frontend/src/layout/navigation/SideBar/SideBar.tsx index b946d96f6de83..c8811625e90a0 100644 --- a/frontend/src/layout/navigation/SideBar/SideBar.tsx +++ b/frontend/src/layout/navigation/SideBar/SideBar.tsx @@ -1,8 +1,12 @@ +import './SideBar.scss' + import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { Link } from 'lib/lemon-ui/Link' -import { useState } from 'react' -import { ProjectName, ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' +import { ActivationSidebar } from 'lib/components/ActivationSidebar/ActivationSidebar' +import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { DebugNotice } from 'lib/components/DebugNotice' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FEATURE_FLAGS } from 'lib/constants' import { IconApps, IconBarChart, @@ -25,8 +29,22 @@ import { IconUnverifiedEvent, IconWeb, } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { Lettermark } from 'lib/lemon-ui/Lettermark' +import { Link } from 'lib/lemon-ui/Link' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useState } from 'react' +import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' +import { IconNotebook } from 'scenes/notebooks/IconNotebook' +import { NotebookPopover } from 'scenes/notebooks/NotebookPanel/NotebookPopover' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { ProjectName, ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' +import { PageButton } from '~/layout/navigation/SideBar/PageButton' +import { SideBarApps } from '~/layout/navigation/SideBar/SideBarApps' import { dashboardsModel } from '~/models/dashboardsModel' import { organizationLogic } from '~/scenes/organizationLogic' import { canViewPlugins } from '~/scenes/plugins/access' @@ -34,23 +52,8 @@ import { Scene } from '~/scenes/sceneTypes' import { isAuthenticatedTeam, teamLogic } from '~/scenes/teamLogic' import { urls } from '~/scenes/urls' import { AvailableFeature } from '~/types' -import './SideBar.scss' + import { navigationLogic } from '../navigationLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { userLogic } from 'scenes/userLogic' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { SideBarApps } from '~/layout/navigation/SideBar/SideBarApps' -import { PageButton } from '~/layout/navigation/SideBar/PageButton' -import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' -import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { DebugNotice } from 'lib/components/DebugNotice' -import { NotebookPopover } from 'scenes/notebooks/NotebookPanel/NotebookPopover' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { IconNotebook } from 'scenes/notebooks/IconNotebook' -import { ActivationSidebar } from 'lib/components/ActivationSidebar/ActivationSidebar' function Pages(): JSX.Element { const { currentOrganization } = useValues(organizationLogic) @@ -201,7 +204,7 @@ function Pages(): JSX.Element { } identifier={Scene.EarlyAccessFeatures} - title={'Early Access Management'} + title={'Early access features'} to={urls.earlyAccessFeatures()} />
    Data
    @@ -210,7 +213,7 @@ function Pages(): JSX.Element { icon={} identifier={Scene.Events} to={urls.events()} - title={'Event Explorer'} + title={'Event explorer'} /> } @@ -230,7 +233,7 @@ function Pages(): JSX.Element { } identifier={Scene.DataWarehouse} - title={'Data Warehouse'} + title={'Data warehouse'} to={urls.dataWarehouse()} highlight="beta" /> @@ -240,7 +243,7 @@ function Pages(): JSX.Element {
    Apps
    {canViewPlugins(currentOrganization) && ( } identifier={Scene.Apps} to={urls.projectApps()} diff --git a/frontend/src/layout/navigation/SideBar/SideBarApps.tsx b/frontend/src/layout/navigation/SideBar/SideBarApps.tsx index 950fff098e82c..412ab375a10af 100644 --- a/frontend/src/layout/navigation/SideBar/SideBarApps.tsx +++ b/frontend/src/layout/navigation/SideBar/SideBarApps.tsx @@ -1,16 +1,17 @@ +import { useActions, useValues } from 'kea' +import { router } from 'kea-router' import { IconExtension } from 'lib/lemon-ui/icons' -import { urls } from 'scenes/urls' -import { Scene } from 'scenes/sceneTypes' -import { canInstallPlugins } from 'scenes/plugins/access' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { PluginSource } from 'scenes/plugins/source/PluginSource' -import { useActions, useValues } from 'kea' +import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' import { organizationLogic } from 'scenes/organizationLogic' +import { canInstallPlugins } from 'scenes/plugins/access' +import { PluginSource } from 'scenes/plugins/source/PluginSource' +import { PluginInstallationType } from 'scenes/plugins/types' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' -import { router } from 'kea-router' import { PageButton } from '~/layout/navigation/SideBar/PageButton' -import { PluginInstallationType } from 'scenes/plugins/types' export function SideBarApps(): JSX.Element { const { currentOrganization } = useValues(organizationLogic) diff --git a/frontend/src/layout/navigation/TopBar/Announcement.tsx b/frontend/src/layout/navigation/TopBar/Announcement.tsx index 24b0d3e73ec6b..04fc851df88e5 100644 --- a/frontend/src/layout/navigation/TopBar/Announcement.tsx +++ b/frontend/src/layout/navigation/TopBar/Announcement.tsx @@ -1,12 +1,13 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' import clsx from 'clsx' -import { MOCK_NODE_PROCESS } from 'lib/constants' -import { announcementLogic, AnnouncementType } from '~/layout/navigation/TopBar/announcementLogic' import { useActions, useValues } from 'kea' +import { MOCK_NODE_PROCESS } from 'lib/constants' import { NewFeatureBanner } from 'lib/introductions/NewFeatureBanner' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { LemonButton, Link } from '@posthog/lemon-ui' import { IconClose } from 'lib/lemon-ui/icons' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { announcementLogic, AnnouncementType } from '~/layout/navigation/TopBar/announcementLogic' window.process = MOCK_NODE_PROCESS diff --git a/frontend/src/layout/navigation/TopBar/NotebookButton.tsx b/frontend/src/layout/navigation/TopBar/NotebookButton.tsx index de32610df9620..87e08761ffd06 100644 --- a/frontend/src/layout/navigation/TopBar/NotebookButton.tsx +++ b/frontend/src/layout/navigation/TopBar/NotebookButton.tsx @@ -1,5 +1,5 @@ -import { useActions, useValues } from 'kea' import { LemonButton, LemonButtonWithSideActionProps } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconNotebook } from 'scenes/notebooks/IconNotebook' import { notebookPanelLogic } from 'scenes/notebooks/NotebookPanel/notebookPanelLogic' diff --git a/frontend/src/layout/navigation/TopBar/NotificationBell.tsx b/frontend/src/layout/navigation/TopBar/NotificationBell.tsx index 997c7a2ce3803..206086bfafc47 100644 --- a/frontend/src/layout/navigation/TopBar/NotificationBell.tsx +++ b/frontend/src/layout/navigation/TopBar/NotificationBell.tsx @@ -1,16 +1,18 @@ -import { IconArrowDropDown, IconInfo, IconNotification, IconWithCount } from 'lib/lemon-ui/icons' -import { notificationsLogic } from '~/layout/navigation/TopBar/notificationsLogic' -import { useActions, useValues } from 'kea' +import './NotificationsBell.scss' + import clsx from 'clsx' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { usePageVisibility } from 'lib/hooks/usePageVisibility' +import { useActions, useValues } from 'kea' import { ActivityLogRow } from 'lib/components/ActivityLog/ActivityLog' -import './NotificationsBell.scss' +import { usePageVisibility } from 'lib/hooks/usePageVisibility' +import { IconArrowDropDown, IconInfo, IconNotification, IconWithCount } from 'lib/lemon-ui/icons' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Link } from 'lib/lemon-ui/Link' +import { Popover } from 'lib/lemon-ui/Popover/Popover' import { urls } from 'scenes/urls' +import { notificationsLogic } from '~/layout/navigation/TopBar/notificationsLogic' + export function NotificationBell(): JSX.Element { const { unreadCount, hasNotifications, notifications, isNotificationPopoverOpen, hasUnread } = useValues(notificationsLogic) diff --git a/frontend/src/layout/navigation/TopBar/SitePopover.tsx b/frontend/src/layout/navigation/TopBar/SitePopover.tsx index 9d8269df080b1..4a5e4ee0e9f58 100644 --- a/frontend/src/layout/navigation/TopBar/SitePopover.tsx +++ b/frontend/src/layout/navigation/TopBar/SitePopover.tsx @@ -1,43 +1,48 @@ +import { IconLive } from '@posthog/icons' +import { LemonButtonPropsBase } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { userLogic } from '../../../scenes/userLogic' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { hedgehogbuddyLogic } from 'lib/components/HedgehogBuddy/hedgehogbuddyLogic' +import { FEATURE_FLAGS } from 'lib/constants' import { - IconCheckmark, - IconOffline, - IconLogout, - IconUpdate, - IconExclamation, - IconBill, IconArrowDropDown, - IconSettings, + IconBill, + IconCheckmark, IconCorporate, + IconExclamation, + IconFlare, + IconLogout, + IconOffline, IconPlus, IconRedeem, + IconSettings, + IconUpdate, } from 'lib/lemon-ui/icons' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from '../../../scenes/urls' -import { navigationLogic } from '../navigationLogic' -import { OrganizationBasicType } from '../../../types' -import { organizationLogic } from '../../../scenes/organizationLogic' -import { preflightLogic } from '../../../scenes/PreflightCheck/preflightLogic' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonRow } from 'lib/lemon-ui/LemonRow' import { Lettermark } from 'lib/lemon-ui/Lettermark' +import { Link } from 'lib/lemon-ui/Link' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { billingLogic } from 'scenes/billing/billingLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' + +import { featurePreviewsLogic } from '~/layout/FeaturePreviews/featurePreviewsLogic' import { AccessLevelIndicator, NewOrganizationButton, OtherOrganizationButton, } from '~/layout/navigation/OrganizationSwitcher' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { LemonButtonPropsBase } from '@posthog/lemon-ui' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { billingLogic } from 'scenes/billing/billingLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { featurePreviewsLogic } from '~/layout/FeaturePreviews/featurePreviewsLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + +import { organizationLogic } from '../../../scenes/organizationLogic' +import { preflightLogic } from '../../../scenes/PreflightCheck/preflightLogic' +import { urls } from '../../../scenes/urls' +import { userLogic } from '../../../scenes/userLogic' +import { OrganizationBasicType } from '../../../types' +import { navigationLogic } from '../navigationLogic' function SitePopoverSection({ title, children }: { title?: string | JSX.Element; children: any }): JSX.Element { return ( @@ -237,6 +242,8 @@ export function SitePopoverOverlay(): JSX.Element { const { preflight } = useValues(preflightLogic) const { closeSitePopover } = useActions(navigationLogic) const { billing } = useValues(billingLogic) + const { hedgehogModeEnabled } = useValues(hedgehogbuddyLogic) + const { setHedgehogModeEnabled } = useActions(hedgehogbuddyLogic) return ( <> @@ -277,11 +284,30 @@ export function SitePopoverOverlay(): JSX.Element { )} - - + + } + fullWidth + data-attr="whats-new-button" + targetBlank + > + What's new? + + - - + + + setHedgehogModeEnabled(!hedgehogModeEnabled)} + icon={} + fullWidth + data-attr="hedgehog-mode-button" + > + {hedgehogModeEnabled ? 'Disable' : 'Enable'} hedgehog mode + + diff --git a/frontend/src/layout/navigation/TopBar/TopBar.scss b/frontend/src/layout/navigation/TopBar/TopBar.scss index 950c5cfec2565..79815df7875f5 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.scss +++ b/frontend/src/layout/navigation/TopBar/TopBar.scss @@ -12,6 +12,7 @@ background: var(--bg-bridge); border-bottom: 1px solid var(--border); gap: 1rem; + @include screen($sm) { padding: 0.5rem 1rem; } @@ -63,6 +64,7 @@ .TopBar__lightning-mode-box { background: var(--bridge) !important; + .LemonSwitch__slider { background-color: var(--border) !important; } @@ -112,6 +114,7 @@ cursor: pointer; font-size: 1.25rem; padding: 0.125rem; + @include screen($sm) { right: 1rem; } @@ -127,7 +130,7 @@ } .SitePopover__side-link { - color: var(--primary); + color: var(--primary-3000); margin-left: 0.5rem; font-weight: 600; font-size: 0.8125rem; diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 585cb5554b1c0..73252e825e95e 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -1,27 +1,37 @@ +import './TopBar.scss' + +import { LemonButtonWithDropdown, Lettermark } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { Logo } from '~/toolbar/assets/Logo' -import { SitePopover } from './SitePopover' -import { Announcement } from './Announcement' -import { navigationLogic } from '../navigationLogic' -import { HelpButton } from 'lib/components/HelpButton/HelpButton' +import { ActivationSidebarToggle } from 'lib/components/ActivationSidebar/ActivationSidebarToggle' import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette' -import { Link } from 'lib/lemon-ui/Link' -import { IconMenu, IconMenuOpen } from 'lib/lemon-ui/icons' -import './TopBar.scss' -import { UniversalSearchPopover } from 'lib/components/UniversalSearch/UniversalSearchPopover' +import { HelpButton } from 'lib/components/HelpButton/HelpButton' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { groupsModel } from '~/models/groupsModel' -import { NotificationBell } from '~/layout/navigation/TopBar/NotificationBell' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { UniversalSearchPopover } from 'lib/components/UniversalSearch/UniversalSearchPopover' import { FEATURE_FLAGS } from 'lib/constants' +import { IconMenu, IconMenuOpen } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { organizationLogic } from 'scenes/organizationLogic' + import { NotebookButton } from '~/layout/navigation/TopBar/NotebookButton' -import { ActivationSidebarToggle } from 'lib/components/ActivationSidebar/ActivationSidebarToggle' +import { NotificationBell } from '~/layout/navigation/TopBar/NotificationBell' +import { groupsModel } from '~/models/groupsModel' +import { Logo } from '~/toolbar/assets/Logo' + +import { navigationLogic } from '../navigationLogic' +import { ProjectSwitcherOverlay } from '../ProjectSwitcher' +import { Announcement } from './Announcement' +import { SitePopover } from './SitePopover' +import { topBarLogic } from './topBarLogic' export function TopBar(): JSX.Element { const { isSideBarShown, noSidebar, minimalTopBar, mobileLayout } = useValues(navigationLogic) const { toggleSideBarBase, toggleSideBarMobile } = useActions(navigationLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) const { featureFlags } = useValues(featureFlagLogic) + const { currentOrganization } = useValues(organizationLogic) + const { isProjectSwitcherShown } = useValues(topBarLogic) + const { toggleProjectSwitcher, hideProjectSwitcher } = useActions(topBarLogic) const hasNotebooks = !!featureFlags[FEATURE_FLAGS.NOTEBOOKS] @@ -71,11 +81,32 @@ export function TopBar(): JSX.Element { )}
    - {!minimalTopBar && ( + {!minimalTopBar ? ( <> {hasNotebooks && } + ) : ( + currentOrganization?.teams && + currentOrganization.teams.length > 1 && ( +
    + } + onClick={() => toggleProjectSwitcher()} + dropdown={{ + visible: isProjectSwitcherShown, + onClickOutside: hideProjectSwitcher, + overlay: , + actionable: true, + placement: 'top-end', + }} + type="secondary" + fullWidth + > + Switch project + +
    + ) )} diff --git a/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts b/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts index b5f3e96fd31c8..562a5ec022e59 100644 --- a/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts +++ b/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts @@ -1,13 +1,15 @@ +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import { announcementLogic, AnnouncementType, DEFAULT_CLOUD_ANNOUNCEMENT } from './announcementLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + +import { initKeaTests } from '~/test/init' + import { navigationLogic } from '../navigationLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import { announcementLogic, AnnouncementType, DEFAULT_CLOUD_ANNOUNCEMENT } from './announcementLogic' describe('announcementLogic', () => { let logic: ReturnType @@ -31,7 +33,7 @@ describe('announcementLogic', () => { }) it('hides announcements during the ingestion phase', async () => { - router.actions.push(urls.ingestion()) + router.actions.push(urls.products()) await expectLogic(logic).toMatchValues({ cloudAnnouncement: DEFAULT_CLOUD_ANNOUNCEMENT, shownAnnouncementType: null, diff --git a/frontend/src/layout/navigation/TopBar/announcementLogic.ts b/frontend/src/layout/navigation/TopBar/announcementLogic.ts index 4a947e95107c7..593ef92032830 100644 --- a/frontend/src/layout/navigation/TopBar/announcementLogic.ts +++ b/frontend/src/layout/navigation/TopBar/announcementLogic.ts @@ -1,13 +1,12 @@ -import { kea, connect, path, actions, reducers, selectors } from 'kea' +import { actions, connect, kea, path, reducers, selectors } from 'kea' import { router } from 'kea-router' import { FEATURE_FLAGS, OrganizationMembershipLevel } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import posthog from 'posthog-js' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { navigationLogic } from '../navigationLogic' -import posthog from 'posthog-js' +import { navigationLogic } from '../navigationLogic' import type { announcementLogicType } from './announcementLogicType' export enum AnnouncementType { @@ -87,7 +86,8 @@ export const announcementLogic = kea([ (closable && (closed || (relevantAnnouncementType && persistedClosedAnnouncements[relevantAnnouncementType]))) || // hide if already closed - pathname == urls.ingestion() // hide during the ingestion phase + pathname.includes('/onboarding') || + pathname.includes('/products') // hide during the onboarding phase ) { return null } diff --git a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx index cc11a0c436539..1dcf216b92566 100644 --- a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx +++ b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx @@ -1,14 +1,14 @@ import { actions, events, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' -import { teamLogic } from 'scenes/teamLogic' -import { ActivityLogItem, humanize, HumanizedActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' - -import type { notificationsLogicType } from './notificationsLogicType' import { describerFor } from 'lib/components/ActivityLog/activityLogLogic' +import { ActivityLogItem, humanize, HumanizedActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' import { dayjs } from 'lib/dayjs' -import posthog from 'posthog-js' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import posthog from 'posthog-js' +import { teamLogic } from 'scenes/teamLogic' + +import type { notificationsLogicType } from './notificationsLogicType' const POLL_TIMEOUT = 5 * 60 * 1000 const MARK_READ_TIMEOUT = 2500 @@ -55,9 +55,9 @@ export const notificationsLogic = kea([ clearTimeout(values.pollTimeout) try { - const response = (await api.get( + const response = await api.get( `api/projects/${teamLogic.values.currentTeamId}/activity_log/important_changes` - )) as ChangesResponse + ) // we can't rely on automatic success action here because we swallow errors so always succeed actions.clearErrorCount() return response @@ -115,14 +115,17 @@ export const notificationsLogic = kea([ a.created_at.isAfter(b.created_at) ? a : b ).created_at actions.setMarkReadTimeout( - window.setTimeout(async () => { - await api.create( - `api/projects/${teamLogic.values.currentTeamId}/activity_log/bookmark_activity_notification`, - { - bookmark: bookmarkDate.toISOString(), - } - ) - actions.markAllAsRead(bookmarkDate.toISOString()) + window.setTimeout(() => { + void api + .create( + `api/projects/${teamLogic.values.currentTeamId}/activity_log/bookmark_activity_notification`, + { + bookmark: bookmarkDate.toISOString(), + } + ) + .then(() => { + actions.markAllAsRead(bookmarkDate.toISOString()) + }) }, MARK_READ_TIMEOUT) ) } diff --git a/frontend/src/layout/navigation/TopBar/topBarLogic.ts b/frontend/src/layout/navigation/TopBar/topBarLogic.ts new file mode 100644 index 0000000000000..a1c2f8ce00a70 --- /dev/null +++ b/frontend/src/layout/navigation/TopBar/topBarLogic.ts @@ -0,0 +1,20 @@ +import { actions, kea, path, reducers } from 'kea' + +import type { topBarLogicType } from './topBarLogicType' + +export const topBarLogic = kea([ + path(['layout', 'navigation', 'TopBar', 'topBarLogic']), + actions({ + toggleProjectSwitcher: true, + hideProjectSwitcher: true, + }), + reducers({ + isProjectSwitcherShown: [ + false, + { + toggleProjectSwitcher: (state) => !state, + hideProjectSwitcher: () => false, + }, + ], + }), +]) diff --git a/frontend/src/layout/navigation/navigationLogic.ts b/frontend/src/layout/navigation/navigationLogic.ts index f819384e973e3..2b11ef83f4a38 100644 --- a/frontend/src/layout/navigation/navigationLogic.ts +++ b/frontend/src/layout/navigation/navigationLogic.ts @@ -1,16 +1,16 @@ -import { windowValues } from 'kea-window-values' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners } from 'kea' +import { windowValues } from 'kea-window-values' import api from 'lib/api' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { sceneLogic } from 'scenes/sceneLogic' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' + import type { navigationLogicType } from './navigationLogicType' -import { membersLogic } from 'scenes/organization/membersLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { Scene } from 'scenes/sceneTypes' export type ProjectNoticeVariant = | 'demo_project' @@ -22,7 +22,7 @@ export type ProjectNoticeVariant = export const navigationLogic = kea([ path(['layout', 'navigation', 'navigationLogic']), connect(() => ({ - values: [sceneLogic, ['sceneConfig', 'activeScene'], membersLogic, ['members', 'membersLoading']], + values: [sceneLogic, ['sceneConfig'], membersLogic, ['members', 'membersLoading']], actions: [eventUsageLogic, ['reportProjectNoticeDismissed']], })), actions({ @@ -121,10 +121,9 @@ export const navigationLogic = kea([ (fullscreen, sceneConfig) => fullscreen || sceneConfig?.layout === 'plain', ], minimalTopBar: [ - (s) => [s.activeScene], - (activeScene) => { - const minimalTopBarScenes = [Scene.Products, Scene.Onboarding] - return activeScene && minimalTopBarScenes.includes(activeScene) + (s) => [s.sceneConfig], + (sceneConfig) => { + return sceneConfig?.layout === 'plain' && !sceneConfig.allowUnauthenticated }, ], isSideBarShown: [ diff --git a/frontend/src/lib/Chart.ts b/frontend/src/lib/Chart.ts index e1575707bd4a1..69e8ee04f44d3 100644 --- a/frontend/src/lib/Chart.ts +++ b/frontend/src/lib/Chart.ts @@ -7,16 +7,16 @@ import { ChartOptions, ChartType, Color, + DefaultDataPoint, + GridLineOptions, InteractionItem, + Plugin, + registerables, + ScriptableLineSegmentContext, TickOptions, - GridLineOptions, + Tooltip, TooltipModel, TooltipOptions, - ScriptableLineSegmentContext, - DefaultDataPoint, - Tooltip, - Plugin, - registerables, } from 'chart.js' import CrosshairPlugin from 'chartjs-plugin-crosshair' import { inStorybookTestRunner } from 'lib/utils' @@ -56,11 +56,11 @@ export { ChartOptions, ChartType, Color, + GridLineOptions, InteractionItem, + Plugin, + ScriptableLineSegmentContext, TickOptions, - GridLineOptions, TooltipModel, TooltipOptions, - Plugin, - ScriptableLineSegmentContext, } diff --git a/frontend/src/lib/actionUtils.test.ts b/frontend/src/lib/actionUtils.test.ts index e08c8e6df35fb..69246099b53ed 100644 --- a/frontend/src/lib/actionUtils.test.ts +++ b/frontend/src/lib/actionUtils.test.ts @@ -1,4 +1,5 @@ import { elementToSelector } from 'lib/actionUtils' + import { ElementType } from '~/types' describe('elementToSelector', () => { diff --git a/frontend/src/lib/actionUtils.ts b/frontend/src/lib/actionUtils.ts index 26f3def245c02..fb6ac933ac4d3 100644 --- a/frontend/src/lib/actionUtils.ts +++ b/frontend/src/lib/actionUtils.ts @@ -1,6 +1,7 @@ -import { ElementType } from '~/types' import { cssEscape } from 'lib/utils/cssEscape' +import { ElementType } from '~/types' + // these plus any element with cursor:pointer will be click targets export const CLICK_TARGETS = ['a', 'button', 'input', 'select', 'textarea', 'label'] export const CLICK_TARGET_SELECTOR = CLICK_TARGETS.join(', ') diff --git a/frontend/src/lib/animations/animations.ts b/frontend/src/lib/animations/animations.ts index 7d30b6932498c..40551f4979cb1 100644 --- a/frontend/src/lib/animations/animations.ts +++ b/frontend/src/lib/animations/animations.ts @@ -33,7 +33,7 @@ async function fetchJson(url: string): Promise> { export async function getAnimationSource(animation: AnimationType): Promise> { if (!animationCache[animation]) { - if (!fetchCache[animation]) { + if (!(animation in fetchCache)) { fetchCache[animation] = fetchJson(animations[animation].url) } animationCache[animation] = await fetchCache[animation] diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index beebac7314bc7..84602fe23d41c 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -1,3 +1,6 @@ +import apiReal from 'lib/api' +import { PluginInstallationType } from 'scenes/plugins/types' + import { CohortType, FilterLogicalOperator, @@ -14,9 +17,8 @@ import { UserBasicType, UserType, } from '~/types' + import { OrganizationMembershipLevel, PluginsAccessLevel } from './constants' -import apiReal from 'lib/api' -import { PluginInstallationType } from 'scenes/plugins/types' export const MOCK_USER_UUID: UserType['uuid'] = 'USER_UUID' export const MOCK_TEAM_ID: TeamType['id'] = 997 diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index 8657a253f95e2..9fdcb3ec5410e 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -1,7 +1,8 @@ import api from 'lib/api' -import { PropertyFilterType, PropertyOperator } from '~/types' import posthog from 'posthog-js' +import { PropertyFilterType, PropertyOperator } from '~/types' + describe('API helper', () => { let fakeFetch: jest.Mock diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d1de3a313acb2..6e3ce2cb92e6c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,15 +1,27 @@ +import { decompressSync, strFromU8 } from 'fflate' +import { encodeParams } from 'kea-router' +import { ActivityLogProps } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { toParams } from 'lib/utils' import posthog from 'posthog-js' +import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' + +import { getCurrentExporterData } from '~/exporter/exporterViewLogic' +import { QuerySchema, QueryStatus } from '~/queries/schema' import { ActionType, + BatchExportConfiguration, BatchExportLogEntry, + BatchExportRun, CohortType, DashboardCollaboratorType, DashboardTemplateEditorType, DashboardTemplateListParams, DashboardTemplateType, DashboardType, - DataWarehouseTable, DataWarehouseSavedQuery, + DataWarehouseTable, + DataWarehouseViewLink, EarlyAccessFeatureType, EventDefinition, EventDefinitionType, @@ -17,15 +29,18 @@ import { EventType, Experiment, ExportedAssetType, + ExternalDataStripeSource, + ExternalDataStripeSourceCreatePayload, FeatureFlagAssociatedRoleType, FeatureFlagType, - OrganizationFeatureFlags, - OrganizationFeatureFlagsCopyBody, InsightModel, IntegrationType, MediaUploadResponse, NewEarlyAccessFeatureType, + NotebookNodeResource, NotebookType, + OrganizationFeatureFlags, + OrganizationFeatureFlagsCopyBody, OrganizationResourcePermissionType, OrganizationType, PersonListParams, @@ -46,31 +61,26 @@ import { SubscriptionType, Survey, TeamType, - UserType, - DataWarehouseViewLink, - BatchExportConfiguration, - BatchExportRun, UserBasicType, - NotebookNodeResource, - ExternalDataStripeSourceCreatePayload, - ExternalDataStripeSource, + UserType, } from '~/types' -import { getCurrentOrganizationId, getCurrentTeamId } from './utils/logics' -import { CheckboxValueType } from 'antd/lib/checkbox/Group' -import { LOGS_PORTION_LIMIT } from 'scenes/plugins/plugin/pluginLogsLogic' -import { toParams } from 'lib/utils' -import { DashboardPrivilegeLevel } from './constants' -import { EVENT_DEFINITIONS_PER_PAGE } from 'scenes/data-management/events/eventDefinitionsTableLogic' -import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' -import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { ActivityLogProps } from 'lib/components/ActivityLog/ActivityLog' -import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' -import { QuerySchema } from '~/queries/schema' -import { decompressSync, strFromU8 } from 'fflate' -import { getCurrentExporterData } from '~/exporter/exporterViewLogic' -import { encodeParams } from 'kea-router' -export const ACTIVITY_PAGE_SIZE = 20 +import { + ACTIVITY_PAGE_SIZE, + DashboardPrivilegeLevel, + EVENT_DEFINITIONS_PER_PAGE, + EVENT_PROPERTY_DEFINITIONS_PER_PAGE, + LOGS_PORTION_LIMIT, +} from './constants' + +/** + * WARNING: Be very careful importing things here. This file is heavily used and can trigger a lot of cyclic imports + * Preferably create a dedicated file in utils/.. + */ + +type CheckboxValueType = string | number | boolean + +const PAGINATION_DEFAULT_MAX_PAGES = 10 export interface PaginatedResponse { results: T[] @@ -115,6 +125,33 @@ export async function getJSONOrThrow(response: Response): Promise { } } +export class ApiConfig { + private static _currentOrganizationId: OrganizationType['id'] | null = null + private static _currentTeamId: TeamType['id'] | null = null + + static getCurrentOrganizationId(): OrganizationType['id'] { + if (!this._currentOrganizationId) { + throw new Error('Organization ID is not known.') + } + return this._currentOrganizationId + } + + static setCurrentOrganizationId(id: OrganizationType['id']): void { + this._currentOrganizationId = id + } + + static getCurrentTeamId(): TeamType['id'] { + if (!this._currentTeamId) { + throw new Error('Team ID is not known.') + } + return this._currentTeamId + } + + static setCurrentTeamId(id: TeamType['id']): void { + this._currentTeamId = id + } +} + class ApiRequest { private pathComponents: string[] private queryString: string | undefined @@ -168,7 +205,7 @@ class ApiRequest { return this.addPathComponent('organizations') } - public organizationsDetail(id: OrganizationType['id'] = getCurrentOrganizationId()): ApiRequest { + public organizationsDetail(id: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId()): ApiRequest { return this.organizations().addPathComponent(id) } @@ -199,7 +236,7 @@ class ApiRequest { return this.addPathComponent('projects') } - public projectsDetail(id: TeamType['id'] = getCurrentTeamId()): ApiRequest { + public projectsDetail(id: TeamType['id'] = ApiConfig.getCurrentTeamId()): ApiRequest { return this.projects().addPathComponent(id) } @@ -445,6 +482,13 @@ class ApiRequest { return this.featureFlags(teamId).addPathComponent(id) } + public featureFlagCreateStaticCohort(id: FeatureFlagType['id'], teamId?: TeamType['id']): ApiRequest { + if (!id) { + throw new Error('Must provide an ID for the feature flag to construct the URL') + } + return this.featureFlag(id, teamId).addPathComponent('create_static_cohort_for_flag') + } + public featureFlagsActivity(id: FeatureFlagType['id'], teamId: TeamType['id']): ApiRequest { if (id) { return this.featureFlag(id, teamId).addPathComponent('activity') @@ -527,7 +571,7 @@ class ApiRequest { // Resource Access Permissions public featureFlagAccessPermissions(flagId: FeatureFlagType['id']): ApiRequest { - return this.featureFlag(flagId, getCurrentTeamId()).addPathComponent('role_access') + return this.featureFlag(flagId, ApiConfig.getCurrentTeamId()).addPathComponent('role_access') } public featureFlagAccessPermissionsDetail( @@ -542,6 +586,10 @@ class ApiRequest { return this.projectsDetail(teamId).addPathComponent('query') } + public queryStatus(queryId: string, teamId?: TeamType['id']): ApiRequest { + return this.query(teamId).addPathComponent(queryId) + } + // Notebooks public notebooks(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('notebooks') @@ -688,17 +736,20 @@ const api = { async get(id: FeatureFlagType['id']): Promise { return await new ApiRequest().featureFlag(id).get() }, + async createStaticCohort(id: FeatureFlagType['id']): Promise<{ cohort: CohortType }> { + return await new ApiRequest().featureFlagCreateStaticCohort(id).create() + }, }, organizationFeatureFlags: { async get( - orgId: OrganizationType['id'] = getCurrentOrganizationId(), + orgId: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId(), featureFlagKey: FeatureFlagType['key'] ): Promise { return await new ApiRequest().organizationFeatureFlags(orgId, featureFlagKey).get() }, async copy( - orgId: OrganizationType['id'] = getCurrentOrganizationId(), + orgId: OrganizationType['id'] = ApiConfig.getCurrentOrganizationId(), data: OrganizationFeatureFlagsCopyBody ): Promise<{ success: FeatureFlagType[]; failed: any }> { return await new ApiRequest().copyOrganizationFeatureFlags(orgId).create({ data }) @@ -740,7 +791,7 @@ const api = { list( activityLogProps: ActivityLogProps, page: number = 1, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise> { const requestForScope: Record ApiRequest | null> = { [ActivityScope.FEATURE_FLAG]: (props) => { @@ -785,7 +836,7 @@ const api = { }, exports: { - determineExportUrl(exportId: number, teamId: TeamType['id'] = getCurrentTeamId()): string { + determineExportUrl(exportId: number, teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): string { return new ApiRequest() .export(exportId, teamId) .withAction('content') @@ -796,12 +847,12 @@ const api = { async create( data: Partial, params: Record = {}, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise { return new ApiRequest().exports(teamId).withQueryString(toParams(params)).create({ data }) }, - async get(id: number, teamId: TeamType['id'] = getCurrentTeamId()): Promise { + async get(id: number, teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): Promise { return new ApiRequest().export(id, teamId).get() }, }, @@ -810,7 +861,7 @@ const api = { async get( id: EventType['id'], includePerson: boolean = false, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise { let apiRequest = new ApiRequest().event(id, teamId) if (includePerson) { @@ -821,7 +872,7 @@ const api = { async list( filters: EventsListQueryParams, limit: number = 100, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): Promise> { const params: EventsListQueryParams = { ...filters, limit, orderBy: filters.orderBy ?? ['-timestamp'] } return new ApiRequest().events(teamId).withQueryString(toParams(params)).get() @@ -829,7 +880,7 @@ const api = { determineListEndpoint( filters: EventsListQueryParams, limit: number = 100, - teamId: TeamType['id'] = getCurrentTeamId() + teamId: TeamType['id'] = ApiConfig.getCurrentTeamId() ): string { const params: EventsListQueryParams = { ...filters, limit } return new ApiRequest().events(teamId).withQueryString(toParams(params)).assembleFullUrl() @@ -837,7 +888,7 @@ const api = { }, tags: { - async list(teamId: TeamType['id'] = getCurrentTeamId()): Promise { + async list(teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): Promise { return new ApiRequest().tags(teamId).get() }, }, @@ -860,7 +911,7 @@ const api = { }, async list({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { limit?: number @@ -876,7 +927,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { limit?: number @@ -925,7 +976,7 @@ const api = { }, async list({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { event_names?: string[] @@ -951,7 +1002,7 @@ const api = { }, determineListEndpoint({ limit = EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - teamId = getCurrentTeamId(), + teamId = ApiConfig.getCurrentTeamId(), ...params }: { event_names?: string[] @@ -1447,7 +1498,7 @@ const api = { }, async update( notebookId: NotebookType['short_id'], - data: Pick + data: Partial> ): Promise { return await new ApiRequest().notebook(notebookId).update({ data }) }, @@ -1722,6 +1773,12 @@ const api = { }, }, + queryStatus: { + async get(queryId: string): Promise { + return await new ApiRequest().queryStatus(queryId).get() + }, + }, + queryURL: (): string => { return new ApiRequest().query().assembleFullUrl(true) }, @@ -1730,7 +1787,8 @@ const api = { query: T, options?: ApiMethodOptions, queryId?: string, - refresh?: boolean + refresh?: boolean, + async?: boolean ): Promise< T extends { [response: string]: any } ? T['response'] extends infer P | undefined @@ -1740,7 +1798,7 @@ const api = { > { return await new ApiRequest() .query() - .create({ ...options, data: { query, client_query_id: queryId, refresh: refresh } }) + .create({ ...options, data: { query, client_query_id: queryId, refresh: refresh, async } }) }, /** Fetch data from specified URL. The result already is JSON-parsed. */ @@ -1852,6 +1910,23 @@ const api = { } return response }, + + async loadPaginatedResults( + url: string | null, + maxIterations: number = PAGINATION_DEFAULT_MAX_PAGES + ): Promise { + let results: any[] = [] + for (let i = 0; i <= maxIterations; ++i) { + if (!url) { + break + } + + const { results: partialResults, next } = await api.get(url) + results = results.concat(partialResults) + url = next + } + return results + }, } function reportError(method: string, url: string, response: Response, startTime: number): void { diff --git a/frontend/src/lib/colors.ts b/frontend/src/lib/colors.ts index c378ce0e0032c..6c95c2443a214 100644 --- a/frontend/src/lib/colors.ts +++ b/frontend/src/lib/colors.ts @@ -5,20 +5,21 @@ export const BRAND_BLUE_HSL: [number, number, number] = [228, 100, 56] /* Insight series colors. */ const dataColorVars = [ - 'brand-blue', - 'purple', - 'viridian', - 'magenta', - 'vermilion', - 'brown', - 'green', - 'blue', - 'pink', - 'navy', - 'turquoise', - 'brick', - 'yellow', - 'lilac', + 'color-1', + 'color-2', + 'color-3', + 'color-4', + 'color-5', + 'color-6', + 'color-7', + 'color-8', + 'color-9', + 'color-10', + 'color-11', + 'color-12', + 'color-13', + 'color-14', + 'color-15', ] export const tagColors = [ @@ -80,8 +81,8 @@ export function getBarColorFromStatus(status: LifecycleToggle, hover?: boolean): export function getGraphColors(isDarkModeOn: boolean): Record { return { axisLabel: isDarkModeOn ? '#fff' : '#2d2d2d', // --text-3000 - axisLine: isDarkModeOn ? '#888' : '#ddd', // --funnel-grid - axis: isDarkModeOn ? '#aaa' : '#999', + axisLine: isDarkModeOn ? '#4b4d58' : '#ddd', // --funnel-grid + axis: isDarkModeOn ? '#4b4d58' : '#999', crosshair: isDarkModeOn ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)', tooltipBackground: '#1dc9b7', tooltipTitle: '#fff', diff --git a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.scss b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.scss index d91b3aee43f33..267ed27a7a08f 100644 --- a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.scss +++ b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.scss @@ -28,13 +28,13 @@ right: 0; width: 100%; height: calc(100vh - 3.5rem); - display: flex; flex-direction: column; overflow: scroll; > div > ul { overflow: auto; + li { margin-top: 0.5rem; } diff --git a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx index 2e66f6e854a5a..cb4bcdc9cd6d1 100644 --- a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx +++ b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx @@ -1,14 +1,17 @@ +import './ActivationSidebar.scss' + import { LemonButton, LemonButtonProps, LemonButtonWithSideAction } from '@posthog/lemon-ui' +import { Progress } from 'antd' import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { activationLogic, ActivationTaskType } from './activationLogic' -import './ActivationSidebar.scss' -import { Progress } from 'antd' import { IconCheckmark, IconClose } from 'lib/lemon-ui/icons' -import { ProfessorHog } from '../hedgehogs' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { navigationLogic } from '~/layout/navigation/navigationLogic' + +import { ProfessorHog } from '../hedgehogs' +import { activationLogic, ActivationTaskType } from './activationLogic' + export const ActivationTask = ({ id, name, diff --git a/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx b/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx index f123a00247135..6e2675bfc6741 100644 --- a/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx +++ b/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx @@ -1,7 +1,9 @@ import { LemonButton } from '@posthog/lemon-ui' +import { Progress } from 'antd' import { useActions, useValues } from 'kea' + import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { Progress } from 'antd' + import { activationLogic } from './activationLogic' export const ActivationSidebarToggle = (): JSX.Element | null => { diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts index 064e9d442b607..c9c61a55e8be6 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts @@ -1,10 +1,12 @@ import { expectLogic } from 'kea-test-utils' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { membersLogic } from 'scenes/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { teamLogic } from 'scenes/teamLogic' + import { navigationLogic } from '~/layout/navigation/navigationLogic' import { initKeaTests } from '~/test/init' + import { activationLogic } from './activationLogic' describe('activationLogic', () => { diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts index b2579b889df9e..9d11bceabbcf3 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts @@ -1,19 +1,21 @@ -import { kea, path, actions, selectors, connect, reducers, listeners, events } from 'kea' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { permanentlyMount } from 'lib/utils/kea-logic-builders' import { membersLogic } from 'scenes/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { teamLogic } from 'scenes/teamLogic' -import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { EventDefinitionType, TeamBasicType } from '~/types' -import type { activationLogicType } from './activationLogicType' import { urls } from 'scenes/urls' -import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' + +import { navigationLogic } from '~/layout/navigation/navigationLogic' import { dashboardsModel } from '~/models/dashboardsModel' -import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { EventDefinitionType, ProductKey, TeamBasicType } from '~/types' + +import type { activationLogicType } from './activationLogicType' export enum ActivationTasks { IngestFirstEvent = 'ingest_first_event', @@ -327,7 +329,7 @@ export const activationLogic = kea([ runTask: async ({ id }) => { switch (id) { case ActivationTasks.IngestFirstEvent: - router.actions.push(urls.ingestion()) + router.actions.push(urls.onboarding(ProductKey.PRODUCT_ANALYTICS)) break case ActivationTasks.InviteTeamMember: actions.showInviteModal() diff --git a/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx b/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx index 16739c8fa9824..c15951310c0db 100644 --- a/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx +++ b/frontend/src/lib/components/ActivityLog/ActivityLog.stories.tsx @@ -1,17 +1,18 @@ +import { Meta } from '@storybook/react' import { featureFlagsActivityResponseJson, insightsActivityResponseJson, personActivityResponseJson, } from 'lib/components/ActivityLog/__mocks__/activityLogMocks' -import { mswDecorator } from '~/mocks/browser' -import { Meta } from '@storybook/react' import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { mswDecorator } from '~/mocks/browser' + const meta: Meta = { title: 'Components/ActivityLog', component: ActivityLog, - parameters: { testOptions: { skip: true } }, // FIXME: Currently disabled as the Timeout story is flaky + tags: ['test-skip'], // FIXME: Currently disabled as the Timeout story is flaky decorators: [ mswDecorator({ get: { diff --git a/frontend/src/lib/components/ActivityLog/ActivityLog.tsx b/frontend/src/lib/components/ActivityLog/ActivityLog.tsx index 9cd152bc34778..336f831dd0ea0 100644 --- a/frontend/src/lib/components/ActivityLog/ActivityLog.tsx +++ b/frontend/src/lib/components/ActivityLog/ActivityLog.tsx @@ -1,15 +1,18 @@ -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { TZLabel } from 'lib/components/TZLabel' -import { useValues } from 'kea' import './ActivityLog.scss' -import { ActivityLogLogicProps, activityLogLogic } from 'lib/components/ActivityLog/activityLogLogic' + +import { LemonDivider } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useValues } from 'kea' +import { activityLogLogic, ActivityLogLogicProps } from 'lib/components/ActivityLog/activityLogLogic' import { HumanizedActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' -import { PaginationControl, usePagination } from 'lib/lemon-ui/PaginationControl' +import { TZLabel } from 'lib/components/TZLabel' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import clsx from 'clsx' -import { ProductIntroduction } from '../ProductIntroduction/ProductIntroduction' +import { PaginationControl, usePagination } from 'lib/lemon-ui/PaginationControl' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' + import { ProductKey } from '~/types' -import { LemonDivider } from '@posthog/lemon-ui' + +import { ProductIntroduction } from '../ProductIntroduction/ProductIntroduction' export type ActivityLogProps = ActivityLogLogicProps & { startingPage?: number @@ -39,7 +42,7 @@ const SkeletonLog = (): JSX.Element => {
    - +
    diff --git a/frontend/src/lib/components/ActivityLog/SentenceList.stories.tsx b/frontend/src/lib/components/ActivityLog/SentenceList.stories.tsx index 665460d2fbc5b..a0e29d01d1bef 100644 --- a/frontend/src/lib/components/ActivityLog/SentenceList.stories.tsx +++ b/frontend/src/lib/components/ActivityLog/SentenceList.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { SentenceList, SentenceListProps } from './SentenceList' type Story = StoryObj diff --git a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts index 25a51bb284628..1e5502d8e8766 100644 --- a/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts +++ b/frontend/src/lib/components/ActivityLog/__mocks__/activityLogMocks.ts @@ -1,4 +1,5 @@ import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' + import { InsightShortId } from '~/types' export const featureFlagsActivityResponseJson: ActivityLogItem[] = [ diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.feature-flag.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.feature-flag.test.tsx index 151e3416219d3..df0cc4c530029 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.feature-flag.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.feature-flag.test.tsx @@ -1,7 +1,9 @@ -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { render } from '@testing-library/react' import '@testing-library/jest-dom' + +import { render } from '@testing-library/react' import { MOCK_TEAM_ID } from 'lib/api.mock' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' + import { makeTestSetup } from './activityLogLogic.test.setup' describe('the activity log logic', () => { diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx index 518eee2fedf77..12d7e59adc172 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.insight.test.tsx @@ -1,8 +1,9 @@ -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { render } from '@testing-library/react' import '@testing-library/jest-dom' + +import { render } from '@testing-library/react' import { MOCK_TEAM_ID } from 'lib/api.mock' import { makeTestSetup } from 'lib/components/ActivityLog/activityLogLogic.test.setup' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' jest.mock('lib/colors') diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.notebook.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.notebook.test.tsx index c3558523f195b..e0f884a96c2d4 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.notebook.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.notebook.test.tsx @@ -1,8 +1,10 @@ -import { ActivityLogItem, ActivityScope, humanize } from 'lib/components/ActivityLog/humanizeActivity' import '@testing-library/jest-dom' -import { InsightShortId } from '~/types' -import { describerFor } from 'lib/components/ActivityLog/activityLogLogic' + import { render } from '@testing-library/react' +import { describerFor } from 'lib/components/ActivityLog/activityLogLogic' +import { ActivityLogItem, ActivityScope, humanize } from 'lib/components/ActivityLog/humanizeActivity' + +import { InsightShortId } from '~/types' describe('the activity log logic', () => { describe('humanizing notebooks', () => { diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx index e63333d6126a9..62d58e6a90624 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.person.test.tsx @@ -1,8 +1,9 @@ -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { render } from '@testing-library/react' import '@testing-library/jest-dom' -import { makeTestSetup } from 'lib/components/ActivityLog/activityLogLogic.test.setup' + +import { render } from '@testing-library/react' import { MOCK_TEAM_ID } from 'lib/api.mock' +import { makeTestSetup } from 'lib/components/ActivityLog/activityLogLogic.test.setup' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' describe('the activity log logic', () => { describe('humanizing persons', () => { diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.plugin.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.plugin.test.tsx index 345f7adb3f3a1..5533218dfd758 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.plugin.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.plugin.test.tsx @@ -1,7 +1,8 @@ -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { render } from '@testing-library/react' import '@testing-library/jest-dom' + +import { render } from '@testing-library/react' import { makeTestSetup } from 'lib/components/ActivityLog/activityLogLogic.test.setup' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' describe('the activity log logic', () => { describe('humanizing plugins', () => { diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts index 42e23c2fc2da5..e947489ac1369 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts @@ -1,3 +1,5 @@ +import { expectLogic } from 'kea-test-utils' +import { activityLogLogic } from 'lib/components/ActivityLog/activityLogLogic' import { ActivityChange, ActivityLogItem, @@ -5,10 +7,9 @@ import { PersonMerge, Trigger, } from 'lib/components/ActivityLog/humanizeActivity' + import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import { activityLogLogic } from 'lib/components/ActivityLog/activityLogLogic' -import { expectLogic } from 'kea-test-utils' interface APIMockSetup { name: string diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.tsx index 5d8d0179e7d51..762412c406e48 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.tsx @@ -1,12 +1,14 @@ -import { initKeaTests } from '~/test/init' +import '@testing-library/jest-dom' + import { expectLogic } from 'kea-test-utils' -import { useMocks } from '~/mocks/jest' -import { ActivityLogItem, ActivityScope, humanize } from 'lib/components/ActivityLog/humanizeActivity' -import { activityLogLogic, describerFor } from 'lib/components/ActivityLog/activityLogLogic' +import { MOCK_TEAM_ID } from 'lib/api.mock' import { featureFlagsActivityResponseJson } from 'lib/components/ActivityLog/__mocks__/activityLogMocks' +import { activityLogLogic, describerFor } from 'lib/components/ActivityLog/activityLogLogic' +import { ActivityLogItem, ActivityScope, humanize } from 'lib/components/ActivityLog/humanizeActivity' import { flagActivityDescriber } from 'scenes/feature-flags/activityDescriptions' -import '@testing-library/jest-dom' -import { MOCK_TEAM_ID } from 'lib/api.mock' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' describe('the activity log logic', () => { let logic: ReturnType diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx index 2287859a196f4..e971c14f916e4 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx @@ -1,6 +1,7 @@ +import { actions, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, actions, reducers, selectors, listeners, events } from 'kea' -import api, { ACTIVITY_PAGE_SIZE, ActivityLogPaginatedResponse } from 'lib/api' +import { router, urlToAction } from 'kea-router' +import api, { ActivityLogPaginatedResponse } from 'lib/api' import { ActivityLogItem, ActivityScope, @@ -8,17 +9,17 @@ import { humanize, HumanizedActivityLogItem, } from 'lib/components/ActivityLog/humanizeActivity' - -import type { activityLogLogicType } from './activityLogLogicType' +import { ACTIVITY_PAGE_SIZE } from 'lib/constants' import { PaginationManual } from 'lib/lemon-ui/PaginationControl' -import { urls } from 'scenes/urls' -import { router, urlToAction } from 'kea-router' +import { dataManagementActivityDescriber } from 'scenes/data-management/dataManagementDescribers' import { flagActivityDescriber } from 'scenes/feature-flags/activityDescriptions' +import { notebookActivityDescriber } from 'scenes/notebooks/Notebook/notebookActivityDescriber' +import { personActivityDescriber } from 'scenes/persons/activityDescriptions' import { pluginActivityDescriber } from 'scenes/plugins/pluginActivityDescriptions' import { insightActivityDescriber } from 'scenes/saved-insights/activityDescriptions' -import { personActivityDescriber } from 'scenes/persons/activityDescriptions' -import { dataManagementActivityDescriber } from 'scenes/data-management/dataManagementDescribers' -import { notebookActivityDescriber } from 'scenes/notebooks/Notebook/notebookActivityDescriber' +import { urls } from 'scenes/urls' + +import type { activityLogLogicType } from './activityLogLogicType' /** * Having this function inside the `humanizeActivity module was causing very weird test errors in other modules @@ -104,7 +105,7 @@ export const activityLogLogic = kea([ })), listeners(({ actions }) => ({ setPage: async (_, breakpoint) => { - await breakpoint() + breakpoint() actions.fetchActivity() }, })), diff --git a/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx b/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx index f7a4c42e1b0af..cd8ef8a39e0fd 100644 --- a/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx +++ b/frontend/src/lib/components/ActivityLog/humanizeActivity.tsx @@ -1,4 +1,5 @@ import { dayjs } from 'lib/dayjs' + import { InsightShortId, PersonType } from '~/types' export interface ActivityChange { diff --git a/frontend/src/lib/components/AddToDashboard/AddToDashboard.tsx b/frontend/src/lib/components/AddToDashboard/AddToDashboard.tsx index a90631adf8f83..20e791542db48 100644 --- a/frontend/src/lib/components/AddToDashboard/AddToDashboard.tsx +++ b/frontend/src/lib/components/AddToDashboard/AddToDashboard.tsx @@ -1,8 +1,9 @@ -import { InsightModel } from '~/types' -import { dashboardsModel } from '~/models/dashboardsModel' import { useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconGauge, IconWithCount } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { InsightModel } from '~/types' interface SaveToDashboardProps { insight: Partial diff --git a/frontend/src/lib/components/AddToDashboard/AddToDashboardModal.tsx b/frontend/src/lib/components/AddToDashboard/AddToDashboardModal.tsx index a10f8c35338cc..ea45c93d16fe1 100644 --- a/frontend/src/lib/components/AddToDashboard/AddToDashboardModal.tsx +++ b/frontend/src/lib/components/AddToDashboard/AddToDashboardModal.tsx @@ -1,20 +1,20 @@ -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { addToDashboardModalLogic } from 'lib/components/AddToDashboard/addToDashboardModalLogic' -import { urls } from 'scenes/urls' -import './AddToDashboard.scss' import { IconCottage } from 'lib/lemon-ui/icons' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' -import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonModal } from 'lib/lemon-ui/LemonModal' import { Link } from 'lib/lemon-ui/Link' -import { DashboardBasicType, InsightModel } from '~/types' -import clsx from 'clsx' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { pluralize } from 'lib/utils' -import { teamLogic } from 'scenes/teamLogic' -import { LemonModal } from 'lib/lemon-ui/LemonModal' import { CSSProperties } from 'react' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { DashboardBasicType, InsightModel } from '~/types' interface SaveToDashboardModalProps { isOpen: boolean diff --git a/frontend/src/lib/components/AddToDashboard/addToDashboardModalLogic.ts b/frontend/src/lib/components/AddToDashboard/addToDashboardModalLogic.ts index c6e363e9fa415..c3d333e2acd47 100644 --- a/frontend/src/lib/components/AddToDashboard/addToDashboardModalLogic.ts +++ b/frontend/src/lib/components/AddToDashboard/addToDashboardModalLogic.ts @@ -1,13 +1,14 @@ -import { kea, props, key, path, connect, actions, reducers, selectors, listeners } from 'kea' -import { dashboardsModel } from '~/models/dashboardsModel' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' -import { DashboardBasicType, DashboardType, InsightModel, InsightType } from '~/types' import FuseClass from 'fuse.js' -import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { insightLogic } from 'scenes/insights/insightLogic' +import { urls } from 'scenes/urls' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { DashboardBasicType, DashboardType, InsightModel, InsightType } from '~/types' import type { addToDashboardModalLogicType } from './addToDashboardModalLogicType' diff --git a/frontend/src/lib/components/Animation/Animation.scss b/frontend/src/lib/components/Animation/Animation.scss index 3799b7dbf77e2..f60434aba11bb 100644 --- a/frontend/src/lib/components/Animation/Animation.scss +++ b/frontend/src/lib/components/Animation/Animation.scss @@ -1,11 +1,11 @@ .Animation { max-width: 300px; + // A correct aspect-ratio is be passed via a style prop. This is as a fallback. aspect-ratio: 1 / 1; overflow: hidden; opacity: 1; transition: 400ms ease opacity; - display: inline-flex; align-items: center; justify-content: center; @@ -18,6 +18,7 @@ display: block; width: 100%; height: 100%; + svg { display: block; } diff --git a/frontend/src/lib/components/Animation/Animation.stories.tsx b/frontend/src/lib/components/Animation/Animation.stories.tsx index aa253dda4377f..cd62f7369b638 100644 --- a/frontend/src/lib/components/Animation/Animation.stories.tsx +++ b/frontend/src/lib/components/Animation/Animation.stories.tsx @@ -1,5 +1,5 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react' import { AnimationType } from 'lib/animations/animations' -import { StoryFn, Meta, StoryObj } from '@storybook/react' import { Animation } from 'lib/components/Animation/Animation' type Story = StoryObj @@ -12,7 +12,6 @@ const meta: Meta = { 'Animations are [LottieFiles.com](https://lottiefiles.com/) animations that we load asynchronously.', }, }, - testOptions: { skip: true }, // Animations aren't particularly snapshotable }, argTypes: { size: { @@ -25,7 +24,7 @@ const meta: Meta = { control: { type: 'radio' }, }, }, - tags: ['autodocs'], + tags: ['autodocs', 'test-skip'], // Animations aren't particularly snapshotable } export default meta diff --git a/frontend/src/lib/components/Animation/Animation.tsx b/frontend/src/lib/components/Animation/Animation.tsx index ddda67a0060e7..df6fbadc929c7 100644 --- a/frontend/src/lib/components/Animation/Animation.tsx +++ b/frontend/src/lib/components/Animation/Animation.tsx @@ -1,9 +1,10 @@ import './Animation.scss' + import { Player } from '@lottiefiles/react-lottie-player' -import { useEffect, useState } from 'react' import clsx from 'clsx' -import { AnimationType, getAnimationSource, animations } from 'lib/animations/animations' +import { animations, AnimationType, getAnimationSource } from 'lib/animations/animations' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { useEffect, useState } from 'react' export interface AnimationProps { /** Animation to show */ @@ -39,7 +40,7 @@ export function Animation({ // Actually fetch the animation. Uses a cache to avoid multiple requests for the same file. // Show a fallback spinner if failed to fetch. useEffect(() => { - let unmounted = false + let unmounted = false // Poor person's abort controller async function loadAnimation(): Promise { try { const source = await getAnimationSource(type) @@ -48,7 +49,7 @@ export function Animation({ !unmounted && setShowFallbackSpinner(true) } } - loadAnimation() + void loadAnimation() return () => { unmounted = true } diff --git a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.scss b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.scss index 65ee0e5fade2a..51423b115d892 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.scss +++ b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.scss @@ -19,7 +19,7 @@ background: none; transform: translate(-50%, -50%); cursor: pointer; - -webkit-appearance: none !important; + appearance: none !important; > .LemonBadge { transition: transform 200ms ease; // Same as LemonBadge's transition @@ -31,6 +31,7 @@ .AnnotationsPopover { --annotations-popover-width: 30rem; + transition: left 200ms ease, opacity 100ms ease, transform 100ms ease; z-index: var(--z-annotation-popover) !important; @@ -55,6 +56,7 @@ .profile-package { vertical-align: bottom; } + h5 { margin: 0; } diff --git a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx index 03dca71fd5bf1..3f43c79ca46d0 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx +++ b/frontend/src/lib/components/AnnotationsOverlay/AnnotationsOverlay.tsx @@ -1,26 +1,29 @@ +import './AnnotationsOverlay.scss' + +import { Chart } from 'chart.js' import { BindLogic, useActions, useValues } from 'kea' import { dayjs } from 'lib/dayjs' -import { humanFriendlyDetailedTime, pluralize, shortTimeZone } from 'lib/utils' -import React, { useRef, useState } from 'react' -import { IntervalType, AnnotationType } from '~/types' import { IconDelete, IconEdit, IconPlusMini } from 'lib/lemon-ui/icons' import { LemonBadge } from 'lib/lemon-ui/LemonBadge/LemonBadge' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { humanFriendlyDetailedTime, pluralize, shortTimeZone } from 'lib/utils' +import React, { useRef, useState } from 'react' +import { AnnotationModal } from 'scenes/annotations/AnnotationModal' +import { annotationModalLogic, annotationScopeToName } from 'scenes/annotations/annotationModalLogic' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { annotationsModel } from '~/models/annotationsModel' +import { AnnotationType, IntervalType } from '~/types' + import { annotationsOverlayLogic, AnnotationsOverlayLogicProps, determineAnnotationsDateGroup, } from './annotationsOverlayLogic' -import './AnnotationsOverlay.scss' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { AnnotationModal } from 'scenes/annotations/AnnotationModal' -import { annotationModalLogic, annotationScopeToName } from 'scenes/annotations/annotationModalLogic' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { annotationsModel } from '~/models/annotationsModel' -import { Chart } from 'chart.js' import { useAnnotationsPositioning } from './useAnnotationsPositioning' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { insightLogic } from 'scenes/insights/insightLogic' /** User-facing format for annotation groups. */ const INTERVAL_UNIT_TO_HUMAN_DAYJS_FORMAT: Record = { diff --git a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts index 8e98373a9a201..02d59125c852f 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts +++ b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.test.ts @@ -1,10 +1,12 @@ import { expectLogic } from 'kea-test-utils' import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' import { insightLogic } from 'scenes/insights/insightLogic' + import { useMocks } from '~/mocks/jest' import { annotationsModel, deserializeAnnotation } from '~/models/annotationsModel' import { initKeaTests } from '~/test/init' -import { AnnotationScope, RawAnnotationType, InsightShortId, IntervalType, AnnotationType } from '~/types' +import { AnnotationScope, AnnotationType, InsightShortId, IntervalType, RawAnnotationType } from '~/types' + import { annotationsOverlayLogic } from './annotationsOverlayLogic' jest.spyOn(Storage.prototype, 'getItem') diff --git a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.ts b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.ts index 2411ba3614c24..881beaf6e3590 100644 --- a/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.ts +++ b/frontend/src/lib/components/AnnotationsOverlay/annotationsOverlayLogic.ts @@ -1,12 +1,14 @@ +import { Tick } from 'chart.js' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { Dayjs, dayjsLocalToTimezone } from 'lib/dayjs' -import { kea, path, selectors, key, props, connect, listeners, actions, reducers } from 'kea' import { groupBy } from 'lib/utils' -import { AnnotationScope, DatedAnnotationType, InsightLogicProps, InsightModel, IntervalType } from '~/types' -import type { annotationsOverlayLogicType } from './annotationsOverlayLogicType' import { insightLogic } from 'scenes/insights/insightLogic' -import { AnnotationDataWithoutInsight, annotationsModel } from '~/models/annotationsModel' import { teamLogic } from 'scenes/teamLogic' -import { Tick } from 'chart.js' + +import { AnnotationDataWithoutInsight, annotationsModel } from '~/models/annotationsModel' +import { AnnotationScope, DatedAnnotationType, InsightLogicProps, InsightModel, IntervalType } from '~/types' + +import type { annotationsOverlayLogicType } from './annotationsOverlayLogicType' export interface AnnotationsOverlayLogicProps extends InsightLogicProps { insightNumericId: InsightModel['id'] | 'new' diff --git a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx index e6f02e1239eb2..29fa6415988e1 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx +++ b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx @@ -1,14 +1,15 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { AuthorizedUrlListType as AuthorizedUrlListType, authorizedUrlListLogic } from './authorizedUrlListLogic' -import { IconDelete, IconEdit, IconOpenInApp, IconPlus } from 'lib/lemon-ui/icons' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { Form } from 'kea-forms' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' 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 { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' + +import { authorizedUrlListLogic, AuthorizedUrlListType as AuthorizedUrlListType } from './authorizedUrlListLogic' function EmptyState({ numberOfResults, diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts index 68a5209ee37f9..b21f9012925bb 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts @@ -1,3 +1,11 @@ +import { router } from 'kea-router' +import { expectLogic } from 'kea-test-utils' +import { api, MOCK_TEAM_ID } from 'lib/api.mock' +import { urls } from 'scenes/urls' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + import { appEditorUrl, authorizedUrlListLogic, @@ -5,12 +13,6 @@ import { filterNotAuthorizedUrls, validateProposedUrl, } from './authorizedUrlListLogic' -import { initKeaTests } from '~/test/init' -import { router } from 'kea-router' -import { expectLogic } from 'kea-test-utils' -import { useMocks } from '~/mocks/jest' -import { urls } from 'scenes/urls' -import { api, MOCK_TEAM_ID } from 'lib/api.mock' describe('the authorized urls list logic', () => { let logic: ReturnType diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts index c45561eef0882..652d357130452 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts @@ -1,3 +1,4 @@ +import Fuse from 'fuse.js' import { actions, afterMount, @@ -11,20 +12,20 @@ import { selectors, sharedListeners, } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import { encodeParams, urlToAction } from 'kea-router' +import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' import { isDomain, isURL } from 'lib/utils' -import { ToolbarParams } from '~/types' import { teamLogic } from 'scenes/teamLogic' -import Fuse from 'fuse.js' -import { encodeParams, urlToAction } from 'kea-router' import { urls } from 'scenes/urls' -import { loaders } from 'kea-loaders' -import { forms } from 'kea-forms' -import type { authorizedUrlListLogicType } from './authorizedUrlListLogicType' -import { subscriptions } from 'kea-subscriptions' import { HogQLQuery, NodeKind } from '~/queries/schema' import { hogql } from '~/queries/utils' +import { ToolbarParams } from '~/types' + +import type { authorizedUrlListLogicType } from './authorizedUrlListLogicType' export interface ProposeNewUrlFormType { url: string diff --git a/frontend/src/lib/components/BillingAlertsV2.tsx b/frontend/src/lib/components/BillingAlertsV2.tsx index 21a4023a6262a..0e9c84c4443c3 100644 --- a/frontend/src/lib/components/BillingAlertsV2.tsx +++ b/frontend/src/lib/components/BillingAlertsV2.tsx @@ -1,9 +1,9 @@ import { useActions, useValues } from 'kea' import { router } from 'kea-router' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { useEffect, useState } from 'react' import { billingLogic } from 'scenes/billing/billingLogic' import { urls } from 'scenes/urls' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' export function BillingAlertsV2(): JSX.Element | null { const { billingAlert } = useValues(billingLogic) @@ -12,18 +12,26 @@ export function BillingAlertsV2(): JSX.Element | null { const [alertHidden, setAlertHidden] = useState(false) useEffect(() => { + if (billingAlert?.pathName && currentLocation.pathname !== billingAlert?.pathName) { + setAlertHidden(true) + } else { + setAlertHidden(false) + } if (billingAlert) { reportBillingAlertShown(billingAlert) } - }, [billingAlert]) + }, [billingAlert, currentLocation]) if (!billingAlert || alertHidden) { return null } - const showButton = billingAlert.contactSupport || currentLocation.pathname !== urls.organizationBilling() + const showButton = + billingAlert.action || billingAlert.contactSupport || currentLocation.pathname !== urls.organizationBilling() - const buttonProps = billingAlert.contactSupport + const buttonProps = billingAlert.action + ? billingAlert.action + : billingAlert.contactSupport ? { to: 'mailto:sales@posthog.com', children: billingAlert.buttonCTA || 'Contact support', diff --git a/frontend/src/lib/components/BridgePage/BridgePage.scss b/frontend/src/lib/components/BridgePage/BridgePage.scss index e44d8333cf4a7..fb780e9cfe9c4 100644 --- a/frontend/src/lib/components/BridgePage/BridgePage.scss +++ b/frontend/src/lib/components/BridgePage/BridgePage.scss @@ -6,10 +6,14 @@ display: flex; flex-direction: column; flex: 1; - overflow: scroll; + overflow: hidden; + min-height: 100vh; + height: 100%; + &::-webkit-scrollbar { width: 0 !important; } + -ms-overflow-style: none; .BridgePage__main { @@ -83,7 +87,7 @@ font-size: 0.8em; font-weight: 600; - &:after { + &::after { position: absolute; top: 100%; left: 20px; @@ -104,7 +108,7 @@ &.BridgePage__left__message--enter-active, &.BridgePage__left__message--enter-done { opacity: 1; - transform: translateY(0px) rotate(5deg) scale(1); + transform: translateY(0) rotate(5deg) scale(1); transition: 200ms opacity, 200ms transform; } @@ -126,13 +130,16 @@ .BridgePage__header-logo { &.mobile { display: block; + @include screen($md) { display: none; } } + .header-logo { padding-bottom: 3rem; text-align: center; + img { height: 24px; } diff --git a/frontend/src/lib/components/BridgePage/BridgePage.tsx b/frontend/src/lib/components/BridgePage/BridgePage.tsx index d7c270d0f5fe1..dce4e1df9fd54 100644 --- a/frontend/src/lib/components/BridgePage/BridgePage.tsx +++ b/frontend/src/lib/components/BridgePage/BridgePage.tsx @@ -1,15 +1,17 @@ +import './BridgePage.scss' + import clsx from 'clsx' +import { useValues } from 'kea' import { useEffect, useState } from 'react' -import { WelcomeLogo } from 'scenes/authentication/WelcomeLogo' import { CSSTransition } from 'react-transition-group' -import './BridgePage.scss' -import { LaptopHog4, LaptopHogEU } from '../hedgehogs' -import { useValues } from 'kea' +import { WelcomeLogo } from 'scenes/authentication/WelcomeLogo' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { Region } from '~/types' +import { LaptopHog4, LaptopHogEU } from '../hedgehogs' + export type BridgePageCommonProps = { - className?: string children?: React.ReactNode footer?: React.ReactNode header?: React.ReactNode @@ -18,7 +20,6 @@ export type BridgePageCommonProps = { sideLogo?: boolean fixedWidth?: boolean leftContainerContent?: JSX.Element - fullScreen?: boolean } interface NoHedgehogProps extends BridgePageCommonProps { @@ -36,7 +37,6 @@ type BridgePageProps = NoHedgehogProps | YesHedgehogProps export function BridgePage({ children, - className, header, footer, view, @@ -46,7 +46,6 @@ export function BridgePage({ fixedWidth = true, leftContainerContent, hedgehog = false, - fullScreen = true, }: BridgePageProps): JSX.Element { const [messageShowing, setMessageShowing] = useState(false) const { preflight } = useValues(preflightLogic) @@ -59,14 +58,7 @@ export function BridgePage({ }, []) return ( -
    +
    {leftContainerContent || hedgehog ? (
    @@ -108,7 +100,7 @@ export function BridgePage({
    {children}
    -
    {footer}
    + {footer &&
    {footer}
    }
    ) } diff --git a/frontend/src/lib/components/Cards/CardMeta.scss b/frontend/src/lib/components/Cards/CardMeta.scss index ae3d46fb9efd9..2272b7acb2f78 100644 --- a/frontend/src/lib/components/Cards/CardMeta.scss +++ b/frontend/src/lib/components/Cards/CardMeta.scss @@ -9,19 +9,24 @@ height: 100%; pointer-events: none; z-index: var(--z-raised); + &.horizontal { svg { transform: rotate(90deg) translateX(0.75rem); } } + &.vertical { flex-direction: column; + svg { transform: translateX(0.5rem); } } + &.corner { justify-content: flex-end; + svg { transform: translate(0.5rem, 0.5rem); } @@ -30,22 +35,29 @@ } .CardMeta { - position: absolute; - top: 0; - left: 0; display: flex; + position: relative; + flex-shrink: 0; flex-direction: column; width: 100%; max-height: calc(100% - 2rem); background: var(--bg-light); z-index: 101; // Elevate above viz - overflow: hidden; border-radius: var(--radius); + + &--with-details { + .CardMeta__top { + // Reduced height so that, considering the padding set above, CardMeta__top doesn't have too much margin + height: 1.5rem; + } + } + h5 { color: var(--muted); line-height: 1.5rem; margin-bottom: 0; } + h4 { overflow: hidden; text-overflow: ellipsis; @@ -55,40 +67,54 @@ font-weight: 600; margin-bottom: 0.125rem; } -} -.CardMeta--with-details { - padding: 1rem; - .CardMeta__top { - // Reduced height so that, considering the padding set above, CardMeta__top doesn't have too much margin - height: 1.5rem; + .CardMeta__primary { + display: flex; + width: 100%; + padding: 1rem; } -} -.CardMeta--expansion-enter-active, -.CardMeta--expansion-exit-active, -.CardMeta--expansion-enter, -.CardMeta--expansion-exit { - transition: box-shadow 200ms ease, height 200ms ease; -} -.CardMeta--expansion-exit.CardMeta--expansion-exit-active, -.CardMeta--expansion-exit-done, -.CardMeta--expansion-enter { - box-shadow: none; -} -.CardMeta--expansion-enter.CardMeta--expansion-enter-active, -.CardMeta--expansion-enter-done, -.CardMeta--expansion-exit { - box-shadow: var(--shadow-elevation); -} -.CardMeta--expansion-enter-done { - overflow: auto; -} + .CardMeta__divider { + margin: 0 1rem; + height: 1px; + background: var(--border); + opacity: 0; + transition: opacity 200ms ease; + } -.CardMeta__primary { - display: flex; - width: 100%; - margin-bottom: 0.5rem; + .CardMeta__details { + position: absolute; + left: 0; + right: 0; + top: 100%; + max-height: 18rem; + margin-top: -1px; // To cause overlap with the divider when closed + border-bottom-width: 1px; + background: var(--bg-light); + transition: box-shadow 200ms ease, height 200ms ease, margin 200ms ease; + overflow-y: auto; + + .CardMeta__details__content { + pointer-events: none; + overflow-y: auto; + } + } + + &.CardMeta--details-shown { + .CardMeta__details { + box-shadow: var(--shadow-elevation); + margin-top: 0; + + .CardMeta__details__content { + opacity: 1; + pointer-events: all; + } + } + + .CardMeta__divider { + opacity: 1; + } + } } .CardMeta__ribbon { @@ -100,12 +126,15 @@ &.blue { background: var(--blue); } + &.purple { background: var(--purple); } + &.green { background: var(--green); } + &.black { background: var(--black); } @@ -118,6 +147,7 @@ width: 100%; height: fit-content; min-height: 2rem; + > * { max-width: 100%; // Make sure that horizontal overflow is capped, so that ellipsis on insight name works } diff --git a/frontend/src/lib/components/Cards/CardMeta.tsx b/frontend/src/lib/components/Cards/CardMeta.tsx index 7692089baeca9..c6b9bc50d8610 100644 --- a/frontend/src/lib/components/Cards/CardMeta.tsx +++ b/frontend/src/lib/components/Cards/CardMeta.tsx @@ -1,13 +1,13 @@ -import React, { useEffect } from 'react' +import './CardMeta.scss' + import clsx from 'clsx' import { useResizeObserver } from 'lib/hooks/useResizeObserver' -import { CSSTransition, Transition } from 'react-transition-group' -import { InsightColor } from '~/types' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconSubtitles, IconSubtitlesOff } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' -import './CardMeta.scss' +import { Transition } from 'react-transition-group' + +import { InsightColor } from '~/types' export interface Resizeable { showResizeHandles?: boolean @@ -15,7 +15,6 @@ export interface Resizeable { } export interface CardMetaProps extends Pick, 'className'> { - setPrimaryHeight?: (primaryHeight: number | undefined) => void areDetailsShown?: boolean setAreDetailsShown?: React.Dispatch> ribbonColor?: InsightColor | null @@ -31,7 +30,6 @@ export interface CardMetaProps extends Pick } export function CardMeta({ - setPrimaryHeight, ribbonColor, showEditingControls, showDetailsControls, @@ -44,76 +42,65 @@ export function CardMeta({ className, samplingNotice, }: CardMetaProps): JSX.Element { - const { ref: primaryRef, height: primaryHeight, width: primaryWidth } = useResizeObserver() + const { ref: primaryRef, width: primaryWidth } = useResizeObserver() const { ref: detailsRef, height: detailsHeight } = useResizeObserver() - useEffect(() => { - setPrimaryHeight?.(primaryHeight) - }, [primaryHeight]) - - const foldedHeight = `calc(${primaryHeight}px ${ - showDetailsControls ? '+ 2rem /* margins */' : '' - } + 1px /* border */)` - const unfoldedHeight = `calc(${primaryHeight}px + ${ - detailsHeight || 0 - }px + 3.5rem /* margins */ + 3px /* border and spacer */)` - const transitionStyles = primaryHeight - ? { - entering: { - height: unfoldedHeight, - }, - entered: { - height: unfoldedHeight, - }, - exiting: { height: foldedHeight }, - exited: { height: foldedHeight }, - } - : {} - const showDetailsButtonLabel = !!primaryWidth && primaryWidth > 480 return ( - - {(transitionState) => ( -
    -
    - {ribbonColor && - ribbonColor !== - InsightColor.White /* White has historically meant no color synonymously to null */ && ( -
    +
    +
    + {ribbonColor && + ribbonColor !== + InsightColor.White /* White has historically meant no color synonymously to null */ && ( +
    + )} +
    +
    +
    {topHeading}
    +
    + {showDetailsControls && setAreDetailsShown && ( + : } + onClick={() => setAreDetailsShown((state) => !state)} + type="tertiary" + status="muted" + size="small" + > + {showDetailsButtonLabel && `${!areDetailsShown ? 'Show' : 'Hide'} details`} + )} -
    -
    -
    {topHeading}
    -
    - {showDetailsControls && setAreDetailsShown && ( - : } - onClick={() => setAreDetailsShown((state) => !state)} - type="tertiary" - status="muted" - size="small" - > - {showDetailsButtonLabel && `${!areDetailsShown ? 'Show' : 'Hide'} details`} - - )} - {samplingNotice ? samplingNotice : null} - {showEditingControls && } -
    -
    - {meta} + {samplingNotice ? samplingNotice : null} + {showEditingControls && }
    - - -
    {metaDetails}
    -
    + {meta}
    - )} - +
    + +
    +
    + {/* By using a transition about displaying then we make sure we aren't rendering the content when not needed */} + +
    + {/* Stops the padding getting in the height calc */} +
    {metaDetails}
    +
    +
    +
    +
    ) } diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index 8176646fac6e3..b0f5f5a471e2c 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -8,10 +8,14 @@ border: 1px solid var(--border); z-index: 3; background: var(--bg-light); + display: flex; + flex-direction: column; + &--highlighted { - border-color: var(--primary); - outline: 1px solid var(--primary); + border-color: var(--primary-3000); + outline: 1px solid var(--primary-3000); } + .ant-alert { margin: 1rem; width: 100%; @@ -19,43 +23,45 @@ } } -.InsightViz { - position: absolute; - bottom: 0; - left: 0; +.InsightCard__viz { + position: relative; + flex: 1; display: flex; - align-items: center; - justify-content: center; - min-height: 0; + flex-direction: column; width: 100%; overflow: auto; + .LineGraph, .AnnotationsOverlay { - padding: 1rem; + padding: 0.5rem; } + .insight-empty-state { height: 100%; // Fix wonkiness when SpinnerOverlay is shown font-size: 0.875rem; // Reduce font size padding-top: 0; padding-bottom: 0; } - > :first-child { - margin-top: auto; - } - > :last-child { - margin-bottom: auto; + + .LemonTable { + border: none; + border-radius: 0; + background: none; } } .InsightDetails { font-size: 0.8125rem; line-height: 1.5rem; + h5 { margin-bottom: 0.125rem; } + section:not(:last-child) { margin-bottom: 0.5rem; } + .LemonRow { min-height: 2rem; font-size: inherit; @@ -67,6 +73,7 @@ padding: 0.5rem; border-radius: var(--radius); background: var(--side); + .LemonRow { padding-left: 0; padding-right: 0; @@ -82,9 +89,11 @@ .InsightDetails__series { margin: -0.125rem 0; + &:not(:first-child) { margin-top: 0.5rem; } + .LemonDivider { width: calc(100% - 1.5rem); margin-left: 1.5rem; @@ -104,13 +113,16 @@ .InsightDetails__footer { display: grid; grid-template-columns: repeat(2, 1fr); + .profile-package { vertical-align: middle; } + .tz-label { line-height: normal; vertical-align: middle; } + .taxonomic-breakdown-filter.tag-pill { font-size: 0.8125rem; padding: 0; @@ -136,9 +148,11 @@ font-size: 0.6875rem; font-weight: 600; line-height: 1rem; + &.SeriesDisplay__raw-name--action, &.SeriesDisplay__raw-name--event { padding: 0.25rem; + &::before { display: inline-block; flex-shrink: 0; @@ -146,16 +160,18 @@ width: 1rem; border-radius: 0.25rem; margin-right: 0.25rem; - background: var(--primary); + background: var(--primary-3000); color: var(--bg-light); line-height: 1rem; font-size: 0.625rem; font-weight: 700; } } + &.SeriesDisplay__raw-name--action::before { content: 'A'; } + &.SeriesDisplay__raw-name--event::before { content: 'E'; } diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx index 15224b0c3d747..9e36ec21d1f87 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx @@ -1,21 +1,22 @@ import { Meta, Story } from '@storybook/react' import { useState } from 'react' + import { ChartDisplayType, InsightColor, InsightModel, InsightShortId, TrendsFilterType } from '~/types' -import { InsightCard as InsightCardComponent } from './index' +import EXAMPLE_DATA_TABLE_NODE_EVENTS_QUERY from '../../../../mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json' +import EXAMPLE_DATA_TABLE_NODE_HOGQL_QUERY from '../../../../mocks/fixtures/api/projects/team_id/insights/dataTableHogQL.json' +import EXAMPLE_FUNNEL from '../../../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRight.json' +import EXAMPLE_LIFECYCLE from '../../../../mocks/fixtures/api/projects/team_id/insights/lifecycle.json' +import EXAMPLE_RETENTION from '../../../../mocks/fixtures/api/projects/team_id/insights/retention.json' +import EXAMPLE_STICKINESS from '../../../../mocks/fixtures/api/projects/team_id/insights/stickiness.json' import EXAMPLE_TRENDS from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json' import EXAMPLE_TRENDS_MULTI from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsLineMulti.json' -import EXAMPLE_TRENDS_HORIZONTAL_BAR from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsValue.json' -import EXAMPLE_TRENDS_TABLE from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsTable.json' import EXAMPLE_TRENDS_PIE from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsPie.json' +import EXAMPLE_TRENDS_TABLE from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsTable.json' +import EXAMPLE_TRENDS_HORIZONTAL_BAR from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsValue.json' import EXAMPLE_TRENDS_WORLD_MAP from '../../../../mocks/fixtures/api/projects/team_id/insights/trendsWorldMap.json' -import EXAMPLE_FUNNEL from '../../../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRight.json' -import EXAMPLE_RETENTION from '../../../../mocks/fixtures/api/projects/team_id/insights/retention.json' import EXAMPLE_PATHS from '../../../../mocks/fixtures/api/projects/team_id/insights/userPaths.json' -import EXAMPLE_STICKINESS from '../../../../mocks/fixtures/api/projects/team_id/insights/stickiness.json' -import EXAMPLE_LIFECYCLE from '../../../../mocks/fixtures/api/projects/team_id/insights/lifecycle.json' -import EXAMPLE_DATA_TABLE_NODE_HOGQL_QUERY from '../../../../mocks/fixtures/api/projects/team_id/insights/dataTableHogQL.json' -import EXAMPLE_DATA_TABLE_NODE_EVENTS_QUERY from '../../../../mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json' +import { InsightCard as InsightCardComponent } from './index' const examples = [ EXAMPLE_TRENDS, diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx index 7b1195510fab4..eacf36fb137eb 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx @@ -1,7 +1,15 @@ +import './InsightCard.scss' + import clsx from 'clsx' import { BindLogic, useValues } from 'kea' -import React, { useEffect, useState } from 'react' +import { Resizeable } from 'lib/components/Cards/CardMeta' +import { QueriesUnsupportedHere } from 'lib/components/Cards/InsightCard/QueriesUnsupportedHere' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import React, { useState } from 'react' import { Layout } from 'react-grid-layout' +import { Funnel } from 'scenes/funnels/Funnel' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { FunnelInvalidExclusionState, FunnelSingleStepState, @@ -9,7 +17,24 @@ import { InsightErrorState, InsightTimeoutState, } from 'scenes/insights/EmptyStates' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { isFilterWithDisplay, isFunnelsFilter, isPathsFilter, isRetentionFilter } from 'scenes/insights/sharedUtils' +import { BoldNumber } from 'scenes/insights/views/BoldNumber' +import { DashboardInsightsTable } from 'scenes/insights/views/InsightsTable/DashboardInsightsTable' +import { WorldMap } from 'scenes/insights/views/WorldMap' +import { Paths } from 'scenes/paths/Paths' +import { RetentionContainer } from 'scenes/retention/RetentionContainer' +import { ActionsHorizontalBar, ActionsLineGraph, ActionsPie } from 'scenes/trends/viz' + +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' +import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' +import { getCachedResults } from '~/queries/nodes/InsightViz/utils' +import { Query } from '~/queries/Query/Query' +import { InsightQueryNode } from '~/queries/schema' +import { QueryContext } from '~/queries/types' import { ChartDisplayType, ChartParams, @@ -23,39 +48,9 @@ import { InsightModel, InsightType, } from '~/types' -import { ResizeHandle1D, ResizeHandle2D } from '../handles' -import './InsightCard.scss' -import { ActionsHorizontalBar, ActionsLineGraph, ActionsPie } from 'scenes/trends/viz' -import { DashboardInsightsTable } from 'scenes/insights/views/InsightsTable/DashboardInsightsTable' -import { Funnel } from 'scenes/funnels/Funnel' -import { RetentionContainer } from 'scenes/retention/RetentionContainer' -import { Paths } from 'scenes/paths/Paths' - -import { WorldMap } from 'scenes/insights/views/WorldMap' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { BoldNumber } from 'scenes/insights/views/BoldNumber' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { - isFilterWithDisplay, - isFunnelsFilter, - isPathsFilter, - isRetentionFilter, - isTrendsFilter, -} from 'scenes/insights/sharedUtils' -import { Resizeable } from 'lib/components/Cards/CardMeta' -import { Query } from '~/queries/Query/Query' -import { QueriesUnsupportedHere } from 'lib/components/Cards/InsightCard/QueriesUnsupportedHere' -import { InsightQueryNode } from '~/queries/schema' -import { QueryContext } from '~/queries/types' +import { ResizeHandle1D, ResizeHandle2D } from '../handles' import { InsightMeta } from './InsightMeta' -import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' -import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' -import { getCachedResults } from '~/queries/nodes/InsightViz/utils' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' type DisplayedType = ChartDisplayType | 'RetentionContainer' | 'FunnelContainer' | 'PathsContainer' @@ -150,7 +145,7 @@ export interface InsightCardProps extends Resizeable, React.HTMLAttributes void removeFromDashboard?: () => void - deleteWithUndo?: () => void + deleteWithUndo?: () => Promise refresh?: () => void rename?: () => void duplicate?: () => void @@ -180,7 +175,6 @@ export function FilterBasedCardContent({ insightProps, loading, setAreDetailsShown, - style, apiErrored, timedOut, empty, @@ -197,25 +191,10 @@ export function FilterBasedCardContent({ cachedResults: getCachedResults(insightProps.cachedInsight, query), doNotLoad: insightProps.doNotLoad, } - useEffect(() => { - // If displaying a BoldNumber Trends insight, we need to fire the window resize event - // Without this, the value is only autosized before `metaPrimaryHeight` is determined, so it's wrong - // With this, autosizing runs again after `metaPrimaryHeight` is ready - if ( - // `display` should be ignored in non-Trends insight - isTrendsFilter(insight.filters) && - insight.filters.display === ChartDisplayType.BoldNumber - ) { - window.dispatchEvent(new Event('resize')) - } - }, [style?.height]) - return (
    { @@ -299,7 +278,6 @@ function InsightCardInternal( loading = true } - const [metaPrimaryHeight, setMetaPrimaryHeight] = useState(undefined) const [areDetailsShown, setAreDetailsShown] = useState(false) const canMakeQueryAPICalls = @@ -327,7 +305,6 @@ function InsightCardInternal( rename={rename} duplicate={duplicate} moveToDashboard={moveToDashboard} - setPrimaryHeight={setMetaPrimaryHeight} areDetailsShown={areDetailsShown} setAreDetailsShown={setAreDetailsShown} showEditingControls={showEditingControls} @@ -335,17 +312,7 @@ function InsightCardInternal( moreButtons={moreButtons} /> {insight.query ? ( -
    +
    {insight.result ? ( ) : canMakeQueryAPICalls ? ( @@ -364,13 +331,6 @@ function InsightCardInternal( empty={empty} tooFewFunnelSteps={tooFewFunnelSteps} invalidFunnelExclusion={invalidFunnelExclusion} - style={ - metaPrimaryHeight - ? { - height: `calc(100% - ${metaPrimaryHeight}px - 2rem /* margins */ - 1px /* border */)`, - } - : undefined - } setAreDetailsShown={setAreDetailsShown} /> ) : ( diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx index f594637987c79..fe1c16d3cd37d 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx @@ -1,9 +1,29 @@ import { useValues } from 'kea' -import { allOperatorsMapping, capitalizeFirstLetter, formatPropertyLabel } from 'lib/utils' +import { + formatPropertyLabel, + isAnyPropertyfilter, + isCohortPropertyFilter, + isPropertyFilterWithOperator, +} from 'lib/components/PropertyFilters/utils' +import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { IconCalculate, IconSubdirectoryArrowRight } from 'lib/lemon-ui/icons' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { Link } from 'lib/lemon-ui/Link' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { KEY_MAPPING } from 'lib/taxonomy' +import { allOperatorsMapping, capitalizeFirstLetter } from 'lib/utils' +import React from 'react' import { LocalFilter, toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' +import { BreakdownTag } from 'scenes/insights/filters/BreakdownFilter/BreakdownTag' +import { isPathsFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' import { humanizePathsEventTypes } from 'scenes/insights/utils' import { apiValueToMathType, MathCategory, MathDefinition, mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' + +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { filterForQuery, isInsightQueryNode } from '~/queries/utils' import { FilterLogicalOperator, FilterType, @@ -12,26 +32,9 @@ import { PathsFilterType, PropertyGroupFilter, } from '~/types' -import { IconCalculate, IconSubdirectoryArrowRight } from 'lib/lemon-ui/icons' -import { LemonRow } from 'lib/lemon-ui/LemonRow' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { SeriesLetter } from 'lib/components/SeriesGlyph' -import { Link } from 'lib/lemon-ui/Link' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' + import { PropertyKeyInfo } from '../../PropertyKeyInfo' -import { KEY_MAPPING } from 'lib/taxonomy' import { TZLabel } from '../../TZLabel' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { cohortsModel } from '~/models/cohortsModel' -import React from 'react' -import { isPathsFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' -import { - isAnyPropertyfilter, - isCohortPropertyFilter, - isPropertyFilterWithOperator, -} from 'lib/components/PropertyFilters/utils' -import { filterForQuery, isInsightQueryNode } from '~/queries/utils' -import { BreakdownTag } from 'scenes/insights/filters/BreakdownFilter/BreakdownTag' function CompactPropertyFiltersDisplay({ groupFilter, diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx index 73d5b42ac3841..fab06e2401974 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx @@ -1,29 +1,31 @@ +// eslint-disable-next-line no-restricted-imports +import { PieChartFilled } from '@ant-design/icons' import { useActions, useValues } from 'kea' +import { CardMeta } from 'lib/components/Cards/CardMeta' +import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' +import { ExportButton } from 'lib/components/ExportButton/ExportButton' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { DashboardPrivilegeLevel } from 'lib/constants' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { Link } from 'lib/lemon-ui/Link' +import { Splotch, SplotchColor } from 'lib/lemon-ui/Splotch' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { capitalizeFirstLetter } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React from 'react' import { insightLogic } from 'scenes/insights/insightLogic' +import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' + +import { cohortsModel } from '~/models/cohortsModel' import { dashboardsModel } from '~/models/dashboardsModel' -import { ExporterFormat, InsightColor } from '~/types' -import { Splotch, SplotchColor } from 'lib/lemon-ui/Splotch' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { Link } from 'lib/lemon-ui/Link' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { InsightDetails } from './InsightDetails' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { groupsModel } from '~/models/groupsModel' -import { cohortsModel } from '~/models/cohortsModel' -import { mathsLogic } from 'scenes/trends/mathsLogic' -import { ExportButton } from 'lib/components/ExportButton/ExportButton' -import { CardMeta } from 'lib/components/Cards/CardMeta' -import { DashboardPrivilegeLevel } from 'lib/constants' -// eslint-disable-next-line no-restricted-imports -import { PieChartFilled } from '@ant-design/icons' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' -import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { ExporterFormat, InsightColor } from '~/types' + import { InsightCardProps } from './InsightCard' +import { InsightDetails } from './InsightDetails' interface InsightMetaProps extends Pick< @@ -42,11 +44,6 @@ interface InsightMetaProps | 'showDetailsControls' | 'moreButtons' > { - /** - * Optional callback to update height of the primary InsightMeta div. Allow for coordinating InsightViz height - * with InsightMeta in a way that makes it possible for meta to overlay viz in expanded (InsightDetails) state. - */ - setPrimaryHeight?: (primaryHeight: number | undefined) => void areDetailsShown?: boolean setAreDetailsShown?: React.Dispatch> } @@ -62,7 +59,6 @@ export function InsightMeta({ rename, duplicate, moveToDashboard, - setPrimaryHeight, areDetailsShown, setAreDetailsShown, showEditingControls = true, @@ -91,13 +87,11 @@ export function InsightMeta({ return ( } meta={ <> @@ -115,7 +109,7 @@ export function InsightMeta({ samplingNotice={ insight.filters.sampling_factor && insight.filters.sampling_factor < 1 ? ( - + ) : null } diff --git a/frontend/src/lib/components/Cards/InsightCard/TopHeading.tsx b/frontend/src/lib/components/Cards/InsightCard/TopHeading.tsx index 731a9c3c8e781..3d059c2a11236 100644 --- a/frontend/src/lib/components/Cards/InsightCard/TopHeading.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/TopHeading.tsx @@ -1,7 +1,8 @@ -import { InsightModel, InsightType } from '~/types' +import { dateFilterToText } from 'lib/utils' import { INSIGHT_TYPES_METADATA, InsightTypeMetadata, QUERY_TYPES_METADATA } from 'scenes/saved-insights/SavedInsights' + import { containsHogQLQuery, dateRangeFor, isDataTableNode, isInsightQueryNode } from '~/queries/utils' -import { dateFilterToText } from 'lib/utils' +import { InsightModel, InsightType } from '~/types' export function TopHeading({ insight }: { insight: InsightModel }): JSX.Element { const { filters, query } = insight diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.scss b/frontend/src/lib/components/Cards/TextCard/TextCard.scss index f88af17286e05..c927ce873d37b 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.scss +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.scss @@ -2,10 +2,8 @@ background: var(--bg-light); } -.TextCard-Body { - position: absolute; - bottom: 0; - left: 0; +.TextCard__body { + flex: 1; overflow-y: auto; ul { diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.stories.tsx b/frontend/src/lib/components/Cards/TextCard/TextCard.stories.tsx index e6790f8446a6b..c8f1a2d8ee482 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.stories.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.stories.tsx @@ -1,5 +1,7 @@ import { Meta, Story } from '@storybook/react' + import { DashboardTile, InsightColor } from '~/types' + import { TextCard } from './TextCard' const meta: Meta = { diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx index 61d6eaee91c93..dba5699b07a7e 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx @@ -1,15 +1,18 @@ import './TextCard.scss' -import { ResizeHandle1D, ResizeHandle2D } from 'lib/components/Cards/handles' -import clsx from 'clsx' -import { DashboardBasicType, DashboardTile } from '~/types' + import { LemonButton, LemonButtonWithDropdown, LemonDivider } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { router } from 'kea-router' +import { Resizeable } from 'lib/components/Cards/CardMeta' +import { ResizeHandle1D, ResizeHandle2D } from 'lib/components/Cards/handles' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import React from 'react' import { urls } from 'scenes/urls' + import { dashboardsModel } from '~/models/dashboardsModel' -import React, { useState } from 'react' -import { CardMeta, Resizeable } from 'lib/components/Cards/CardMeta' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { DashboardBasicType, DashboardTile } from '~/types' interface TextCardProps extends React.HTMLAttributes, Resizeable { dashboardId?: string | number @@ -24,29 +27,19 @@ interface TextCardProps extends React.HTMLAttributes, Resizeable showEditingControls?: boolean } -interface TextCardBodyProps extends Pick, 'style' | 'className'> { +interface TextCardBodyProps extends Pick, 'className'> { text: string closeDetails?: () => void } -export function TextContent({ text, closeDetails, style, className }: TextCardBodyProps): JSX.Element { +export function TextContent({ text, closeDetails, className }: TextCardBodyProps): JSX.Element { return ( - // eslint-disable-next-line react/forbid-dom-props -
    closeDetails?.()} style={style}> +
    closeDetails?.()}> {text}
    ) } -export function TextCardBody({ text, closeDetails, style }: TextCardBodyProps): JSX.Element { - return ( - // eslint-disable-next-line react/forbid-dom-props -
    closeDetails?.()} style={style}> - -
    - ) -} - export function TextCardInternal( { textTile, @@ -67,8 +60,6 @@ export function TextCardInternal( const { push } = useActions(router) const { text } = textTile - const [metaPrimaryHeight, setMetaPrimaryHeight] = useState(undefined) - if (!text) { throw new Error('TextCard requires text') } @@ -82,83 +73,82 @@ export function TextCardInternal( {...divProps} ref={ref} > - - dashboardId && push(urls.dashboardTextTile(dashboardId, textTile.id))} - data-attr="edit-text" - > - Edit text - - - {moveToDashboard && otherDashboards.length > 0 && ( - ( - { - moveToDashboard(otherDashboard) - }} - fullWidth - > - {otherDashboard.name || Untitled} - - )), - placement: 'right-start', - fallbackPlacements: ['left-start'], - actionable: true, - closeParentPopoverOnClickInside: true, - }} - fullWidth - > - Move to - - )} - - Duplicate - - {moreButtons && ( + {showEditingControls && ( +
    + + + dashboardId && push(urls.dashboardTextTile(dashboardId, textTile.id)) + } + data-attr="edit-text" + > + Edit text + + + {moveToDashboard && otherDashboards.length > 0 && ( + ( + { + moveToDashboard(otherDashboard) + }} + fullWidth + > + {otherDashboard.name || Untitled} + + )), + placement: 'right-start', + fallbackPlacements: ['left-start'], + actionable: true, + closeParentPopoverOnClickInside: true, + }} + fullWidth + > + Move to + + )} + + Duplicate + + {moreButtons && ( + <> + + {moreButtons} + + )} - {moreButtons} + {removeFromDashboard && ( + + Remove from dashboard + + )} - )} - - {removeFromDashboard && ( - - Remove from dashboard - - )} - - } - setPrimaryHeight={setMetaPrimaryHeight} - /> + } + /> +
    + )} - +
    + +
    {showResizeHandles && ( <> diff --git a/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx b/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx index a29655f96afba..47fffc6cff914 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx @@ -1,13 +1,14 @@ -import { AvailableFeature, DashboardType } from '~/types' -import { textCardModalLogic } from 'lib/components/Cards/TextCard/textCardModalLogic' import { useActions, useValues } from 'kea' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { Field, Form } from 'kea-forms' -import { LemonTextAreaMarkdown } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { textCardModalLogic } from 'lib/components/Cards/TextCard/textCardModalLogic' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonTextAreaMarkdown } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' import { userLogic } from 'scenes/userLogic' +import { AvailableFeature, DashboardType } from '~/types' + export function TextCardModal({ isOpen, onClose, diff --git a/frontend/src/lib/components/Cards/TextCard/textCardModalLogic.ts b/frontend/src/lib/components/Cards/TextCard/textCardModalLogic.ts index 3b9a0514526db..15bde2f1a656e 100644 --- a/frontend/src/lib/components/Cards/TextCard/textCardModalLogic.ts +++ b/frontend/src/lib/components/Cards/TextCard/textCardModalLogic.ts @@ -1,6 +1,7 @@ import { lemonToast } from '@posthog/lemon-ui' -import { kea, props, path, connect, key, listeners } from 'kea' +import { connect, kea, key, listeners, path, props } from 'kea' import { forms } from 'kea-forms' + import { dashboardsModel } from '~/models/dashboardsModel' import { DashboardTile, DashboardType } from '~/types' diff --git a/frontend/src/lib/components/ChartFilter/ChartFilter.tsx b/frontend/src/lib/components/ChartFilter/ChartFilter.tsx index 89b5d39673e46..4cac6dcd3cec6 100644 --- a/frontend/src/lib/components/ChartFilter/ChartFilter.tsx +++ b/frontend/src/lib/components/ChartFilter/ChartFilter.tsx @@ -1,20 +1,20 @@ +import { LemonSelect, LemonSelectOptions } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { - IconShowChart, - IconCumulativeChart, - IconBarChart, - IconAreaChart, Icon123, + IconAreaChart, + IconBarChart, + IconCumulativeChart, IconPieChart, - IconTableChart, IconPublic, + IconShowChart, + IconTableChart, } from 'lib/lemon-ui/icons' - -import { ChartDisplayType } from '~/types' import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonSelect, LemonSelectOptions } from '@posthog/lemon-ui' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { ChartDisplayType } from '~/types' + export function ChartFilter(): JSX.Element { const { insightProps } = useValues(insightLogic) const { display } = useValues(insightVizDataLogic(insightProps)) diff --git a/frontend/src/lib/components/CodeEditors.tsx b/frontend/src/lib/components/CodeEditors.tsx index 1088a710d25e2..3fd7c96eecceb 100644 --- a/frontend/src/lib/components/CodeEditors.tsx +++ b/frontend/src/lib/components/CodeEditors.tsx @@ -1,8 +1,9 @@ +import MonacoEditor, { type EditorProps } from '@monaco-editor/react' +import { useValues } from 'kea' import { Spinner } from 'lib/lemon-ui/Spinner' import { inStorybookTestRunner } from 'lib/utils' -import MonacoEditor, { type EditorProps } from '@monaco-editor/react' + import { themeLogic } from '~/layout/navigation-3000/themeLogic' -import { useValues } from 'kea' export type CodeEditorProps = Omit diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss index f7d40aaf1a9b5..4fb411ba56520 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss @@ -1,19 +1,23 @@ .CodeSnippet { position: relative; font-size: 0.875rem; + &.CodeSnippet--compact { font-size: 0.8125rem; + .CodeSnippet__actions { top: 0.375rem; right: 0.375rem; } } + .CodeSnippet__actions { position: absolute; display: flex; top: 0.25rem; right: 0.25rem; gap: 0.5rem; + .LemonButton .LemonIcon { color: #fff; } diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index 180ac9e794838..68ae2f5f9a09d 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -1,34 +1,35 @@ +import './CodeSnippet.scss' + +import { Popconfirm } from 'antd' +import { PopconfirmProps } from 'antd/lib/popconfirm' +import clsx from 'clsx' +import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { IconCopy, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { useState } from 'react' import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter' -import okaidia from 'react-syntax-highlighter/dist/esm/styles/prism/okaidia' -import synthwave84 from 'react-syntax-highlighter/dist/esm/styles/prism/synthwave84' import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash' -import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx' -import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript' -import java from 'react-syntax-highlighter/dist/esm/languages/prism/java' -import ruby from 'react-syntax-highlighter/dist/esm/languages/prism/ruby' -import objectiveC from 'react-syntax-highlighter/dist/esm/languages/prism/objectivec' -import swift from 'react-syntax-highlighter/dist/esm/languages/prism/swift' -import elixir from 'react-syntax-highlighter/dist/esm/languages/prism/elixir' -import php from 'react-syntax-highlighter/dist/esm/languages/prism/php' -import python from 'react-syntax-highlighter/dist/esm/languages/prism/python' import dart from 'react-syntax-highlighter/dist/esm/languages/prism/dart' +import elixir from 'react-syntax-highlighter/dist/esm/languages/prism/elixir' import go from 'react-syntax-highlighter/dist/esm/languages/prism/go' +import http from 'react-syntax-highlighter/dist/esm/languages/prism/http' +import java from 'react-syntax-highlighter/dist/esm/languages/prism/java' +import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript' import json from 'react-syntax-highlighter/dist/esm/languages/prism/json' -import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml' +import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx' import markup from 'react-syntax-highlighter/dist/esm/languages/prism/markup' -import http from 'react-syntax-highlighter/dist/esm/languages/prism/http' +import objectiveC from 'react-syntax-highlighter/dist/esm/languages/prism/objectivec' +import php from 'react-syntax-highlighter/dist/esm/languages/prism/php' +import python from 'react-syntax-highlighter/dist/esm/languages/prism/python' +import ruby from 'react-syntax-highlighter/dist/esm/languages/prism/ruby' import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql' -import { copyToClipboard } from 'lib/utils' -import { Popconfirm } from 'antd' -import { PopconfirmProps } from 'antd/lib/popconfirm' -import './CodeSnippet.scss' -import { IconCopy, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { useValues } from 'kea' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { useState } from 'react' -import clsx from 'clsx' +import swift from 'react-syntax-highlighter/dist/esm/languages/prism/swift' +import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml' +import okaidia from 'react-syntax-highlighter/dist/esm/styles/prism/okaidia' +import synthwave84 from 'react-syntax-highlighter/dist/esm/styles/prism/synthwave84' export enum Language { Text = 'text', @@ -132,8 +133,10 @@ export function CodeSnippet({ } - onClick={async () => { - text && (await copyToClipboard(text, thing)) + onClick={() => { + if (text) { + void copyToClipboard(text, thing) + } }} size={compact ? 'small' : 'medium'} /> diff --git a/frontend/src/lib/components/CommandBar/ActionBar.tsx b/frontend/src/lib/components/CommandBar/ActionBar.tsx index c2a0a50cc7413..183f2d6bd4c3b 100644 --- a/frontend/src/lib/components/CommandBar/ActionBar.tsx +++ b/frontend/src/lib/components/CommandBar/ActionBar.tsx @@ -1,11 +1,10 @@ import { useValues } from 'kea' import { actionBarLogic } from './actionBarLogic' +import { ActionInput } from './ActionInput' +import { ActionResults } from './ActionResults' -import ActionInput from './ActionInput' -import ActionResults from './ActionResults' - -const ActionBar = (): JSX.Element => { +export const ActionBar = (): JSX.Element => { const { activeFlow } = useValues(actionBarLogic) return ( @@ -15,5 +14,3 @@ const ActionBar = (): JSX.Element => {
    ) } - -export default ActionBar diff --git a/frontend/src/lib/components/CommandBar/ActionInput.tsx b/frontend/src/lib/components/CommandBar/ActionInput.tsx index a19a72dde307f..6a262811197f3 100644 --- a/frontend/src/lib/components/CommandBar/ActionInput.tsx +++ b/frontend/src/lib/components/CommandBar/ActionInput.tsx @@ -1,16 +1,17 @@ -import React from 'react' +import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { CommandFlow } from 'lib/components/CommandPalette/commandPaletteLogic' +import { IconChevronRight, IconEdit } from 'lib/lemon-ui/icons' +import React from 'react' -import { LemonInput } from '@posthog/lemon-ui' import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { actionBarLogic } from './actionBarLogic' -import { IconChevronRight, IconEdit } from 'lib/lemon-ui/icons' -import { CommandFlow } from 'lib/components/CommandPalette/commandPaletteLogic' type PrefixIconProps = { activeFlow: CommandFlow | null } + const PrefixIcon = ({ activeFlow }: PrefixIconProps): React.ReactElement | null => { if (activeFlow) { return ?? @@ -19,19 +20,18 @@ const PrefixIcon = ({ activeFlow }: PrefixIconProps): React.ReactElement | null } } -const ActionInput = (): JSX.Element => { +export const ActionInput = (): JSX.Element => { const { input, activeFlow } = useValues(actionBarLogic) const { setInput } = useActions(actionBarLogic) return (
    } - suffix={} - placeholder={activeFlow?.instruction ?? 'What would you like to do? Try some suggestions…'} + suffix={} + placeholder={activeFlow?.instruction ?? 'Run a command…'} autoFocus value={input} onChange={setInput} @@ -39,5 +39,3 @@ const ActionInput = (): JSX.Element => {
    ) } - -export default ActionInput diff --git a/frontend/src/lib/components/CommandBar/ActionResult.tsx b/frontend/src/lib/components/CommandBar/ActionResult.tsx index 777e3b2e20889..e10144edb6b4f 100644 --- a/frontend/src/lib/components/CommandBar/ActionResult.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResult.tsx @@ -1,16 +1,15 @@ -import { useEffect, useRef } from 'react' import { useActions } from 'kea' +import { useEffect, useRef } from 'react' -import { actionBarLogic } from './actionBarLogic' -import { getNameFromActionScope } from './utils' import { CommandResultDisplayable } from '../CommandPalette/commandPaletteLogic' +import { actionBarLogic } from './actionBarLogic' type SearchResultProps = { result: CommandResultDisplayable focused: boolean } -const ActionResult = ({ result, focused }: SearchResultProps): JSX.Element => { +export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Element => { const { executeResult, onMouseEnterResult, onMouseLeaveResult } = useActions(actionBarLogic) const ref = useRef(null) @@ -23,10 +22,10 @@ const ActionResult = ({ result, focused }: SearchResultProps): JSX.Element => { }, [focused]) return ( -
    +
    { onMouseEnterResult(result.index) @@ -41,15 +40,12 @@ const ActionResult = ({ result, focused }: SearchResultProps): JSX.Element => { }} ref={ref} > -
    - {result.source.scope && ( - {getNameFromActionScope(result.source.scope)} - )} - {result.display} +
    + + {result.display}
    + {focused &&
    Run command
    }
    ) } - -export default ActionResult diff --git a/frontend/src/lib/components/CommandBar/ActionResults.tsx b/frontend/src/lib/components/CommandBar/ActionResults.tsx index 9eea58c79787c..c104546e9210f 100644 --- a/frontend/src/lib/components/CommandBar/ActionResults.tsx +++ b/frontend/src/lib/components/CommandBar/ActionResults.tsx @@ -1,10 +1,9 @@ import { useValues } from 'kea' +import { getNameFromActionScope } from 'lib/components/CommandBar/utils' import { CommandResultDisplayable } from '../CommandPalette/commandPaletteLogic' - import { actionBarLogic } from './actionBarLogic' -import ActionResult from './ActionResult' -import { getNameFromActionScope } from 'lib/components/CommandBar/utils' +import { ActionResult } from './ActionResult' type ResultsGroupProps = { scope: string @@ -15,7 +14,9 @@ type ResultsGroupProps = { const ResultsGroup = ({ scope, results, activeResultIndex }: ResultsGroupProps): JSX.Element => { return ( <> -
    {getNameFromActionScope(scope)}
    +
    + {getNameFromActionScope(scope)} +
    {results.map((result) => ( { +export const ActionResults = (): JSX.Element => { const { commandSearchResultsGrouped, activeResultIndex } = useValues(actionBarLogic) return ( @@ -38,5 +39,3 @@ const ActionResults = (): JSX.Element => {
    ) } - -export default ActionResults diff --git a/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx b/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx new file mode 100644 index 0000000000000..898e3bc1de0c4 --- /dev/null +++ b/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx @@ -0,0 +1,289 @@ +import { Meta } from '@storybook/react' +import { useActions } from 'kea' +import { commandBarLogic } from 'lib/components/CommandBar/commandBarLogic' +import { BarStatus } from 'lib/components/CommandBar/types' +import { useEffect } from 'react' + +import { mswDecorator } from '~/mocks/browser' + +import { CommandBar } from './CommandBar' + +const SEARCH_RESULT = { + results: [ + { + type: 'insight', + result_id: '3b7NrJXF', + extra_fields: { + name: '', + description: '', + derived_name: 'SQL query', + }, + }, + { + type: 'insight', + result_id: 'U2W7bAq1', + extra_fields: { + name: '', + description: '', + derived_name: 'All events → All events user conversion rate', + }, + }, + { + type: 'feature_flag', + result_id: '120', + extra_fields: { + key: 'person-on-events-enabled', + name: 'person-on-events-enabled', + }, + }, + { + type: 'insight', + result_id: '44fpCyF7', + extra_fields: { + name: '', + description: '', + derived_name: 'User lifecycle based on Pageview', + }, + }, + { + type: 'feature_flag', + result_id: '150', + extra_fields: { + key: 'cs-dashboards', + name: 'cs-dashboards', + }, + }, + { + type: 'notebook', + result_id: 'b1ZyFO6K', + extra_fields: { + title: 'Notes 27/09', + text_content: 'Notes 27/09\nasd\nas\nda\ns\nd\nlalala', + }, + }, + { + type: 'insight', + result_id: 'Ap5YYl2H', + extra_fields: { + name: '', + description: '', + derived_name: + 'Pageview count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count', + }, + }, + { + type: 'insight', + result_id: '4Xaltnro', + extra_fields: { + name: '', + description: '', + derived_name: 'User paths based on page views and custom events', + }, + }, + { + type: 'insight', + result_id: 'HUkkq7Au', + extra_fields: { + name: '', + description: '', + derived_name: + 'Pageview count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count', + }, + }, + { + type: 'insight', + result_id: 'hF5z02Iw', + extra_fields: { + name: '', + description: '', + derived_name: 'Pageview count & All events count', + }, + }, + { + type: 'feature_flag', + result_id: '143', + extra_fields: { + key: 'high-frequency-batch-exports', + name: 'high-frequency-batch-exports', + }, + }, + { + type: 'feature_flag', + result_id: '126', + extra_fields: { + key: 'onboarding-v2-demo', + name: 'onboarding-v2-demo', + }, + }, + { + type: 'feature_flag', + result_id: '142', + extra_fields: { + key: 'web-analytics', + name: 'web-analytics', + }, + }, + { + type: 'insight', + result_id: '94r9bOyB', + extra_fields: { + name: '', + description: '', + derived_name: 'Pageview count & All events count', + }, + }, + { + type: 'dashboard', + result_id: '1', + extra_fields: { + name: '🔑 Key metrics', + description: 'Company overview.', + }, + }, + { + type: 'notebook', + result_id: 'eq4n8PQY', + extra_fields: { + title: 'asd', + text_content: 'asd', + }, + }, + { + type: 'insight', + result_id: 'QcCPEk7d', + extra_fields: { + name: 'Daily unique visitors over time', + description: null, + derived_name: '$pageview unique users & All events count', + }, + }, + { + type: 'feature_flag', + result_id: '133', + extra_fields: { + key: 'feedback-scene', + name: 'feedback-scene', + }, + }, + { + type: 'insight', + result_id: 'PWwez0ma', + extra_fields: { + name: 'Most popular pages', + description: null, + derived_name: null, + }, + }, + { + type: 'insight', + result_id: 'HKTERZ40', + extra_fields: { + name: 'Feature Flag calls made by unique users per variant', + description: + 'Shows the number of unique user calls made on feature flag per variant with key: notebooks', + derived_name: null, + }, + }, + { + type: 'feature_flag', + result_id: '161', + extra_fields: { + key: 'console-recording-search', + name: 'console-recording-search', + }, + }, + { + type: 'feature_flag', + result_id: '134', + extra_fields: { + key: 'early-access-feature', + name: 'early-access-feature', + }, + }, + { + type: 'insight', + result_id: 'uE7xieYc', + extra_fields: { + name: '', + description: '', + derived_name: 'Pageview count', + }, + }, + { + type: 'feature_flag', + result_id: '159', + extra_fields: { + key: 'surveys-multiple-questions', + name: 'surveys-multiple-questions', + }, + }, + { + type: 'insight', + result_id: 'AVPsaax4', + extra_fields: { + name: 'Monthly app revenue', + description: null, + derived_name: null, + }, + }, + ], + counts: { + insight: 80, + dashboard: 14, + experiment: 1, + feature_flag: 66, + notebook: 2, + action: 4, + cohort: 3, + }, +} + +const meta: Meta = { + title: 'Components/Command Bar', + component: CommandBar, + decorators: [ + mswDecorator({ + get: { + '/api/projects/:team_id/search': SEARCH_RESULT, + }, + }), + ], + parameters: { + layout: 'fullscreen', + testOptions: { + snapshotTargetSelector: '[data-attr="command-bar"]', + }, + viewMode: 'story', + }, +} +export default meta + +export function Search(): JSX.Element { + const { setCommandBar } = useActions(commandBarLogic) + + useEffect(() => { + setCommandBar(BarStatus.SHOW_SEARCH) + }, []) + + return +} + +export function Actions(): JSX.Element { + const { setCommandBar } = useActions(commandBarLogic) + + useEffect(() => { + setCommandBar(BarStatus.SHOW_ACTIONS) + }, []) + + return +} + +export function Shortcuts(): JSX.Element { + const { setCommandBar } = useActions(commandBarLogic) + + useEffect(() => { + setCommandBar(BarStatus.SHOW_SHORTCUTS) + }, []) + + return +} diff --git a/frontend/src/lib/components/CommandBar/CommandBar.tsx b/frontend/src/lib/components/CommandBar/CommandBar.tsx index d293518e140bc..480cf294d9e3a 100644 --- a/frontend/src/lib/components/CommandBar/CommandBar.tsx +++ b/frontend/src/lib/components/CommandBar/CommandBar.tsx @@ -1,30 +1,64 @@ -import { useRef } from 'react' -import { useActions, useValues } from 'kea' +import './index.scss' +import { useActions, useValues } from 'kea' import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler' +import { forwardRef, useRef } from 'react' +import { ActionBar } from './ActionBar' import { commandBarLogic } from './commandBarLogic' +import { SearchBar } from './SearchBar' +import { Shortcuts } from './Shortcuts' import { BarStatus } from './types' -import './index.scss' -import SearchBar from './SearchBar' -import { LemonModal } from '@posthog/lemon-ui' -import ActionBar from './ActionBar' +interface CommandBarOverlayProps { + barStatus: BarStatus + children?: React.ReactNode +} + +const CommandBarOverlay = forwardRef(function CommandBarOverlayInternal( + { barStatus, children }, + ref +): JSX.Element { + return ( +
    +
    + {children} +
    +
    + ) +}) -function CommandBar(): JSX.Element | null { +export function CommandBar(): JSX.Element | null { const containerRef = useRef(null) const { barStatus } = useValues(commandBarLogic) const { hideCommandBar } = useActions(commandBarLogic) useOutsideClickHandler(containerRef, hideCommandBar, []) + if (barStatus === BarStatus.HIDDEN) { + return null + } + return ( - -
    - {barStatus === BarStatus.SHOW_SEARCH ? : } -
    -
    + + {barStatus === BarStatus.SHOW_SEARCH && } + {barStatus === BarStatus.SHOW_ACTIONS && } + {barStatus === BarStatus.SHOW_SHORTCUTS && } + ) } - -export default CommandBar diff --git a/frontend/src/lib/components/CommandBar/SearchBar.tsx b/frontend/src/lib/components/CommandBar/SearchBar.tsx index ac131202a9c4f..a7d1d6898913a 100644 --- a/frontend/src/lib/components/CommandBar/SearchBar.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBar.tsx @@ -1,21 +1,21 @@ import { useMountedLogic } from 'kea' +import { useRef } from 'react' import { searchBarLogic } from './searchBarLogic' +import { SearchInput } from './SearchInput' +import { SearchResults } from './SearchResults' +import { SearchTabs } from './SearchTabs' -import SearchInput from './SearchInput' -import SearchResults from './SearchResults' -import SearchTabs from './SearchTabs' - -const SearchBar = (): JSX.Element => { +export const SearchBar = (): JSX.Element => { useMountedLogic(searchBarLogic) // load initial results + const inputRef = useRef(null) + return (
    - + - +
    ) } - -export default SearchBar diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx index af878016e9bf8..e71cda427e5bd 100644 --- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx +++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx @@ -1,4 +1,6 @@ -import { useActions } from 'kea' +import { useActions, useValues } from 'kea' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { RefObject } from 'react' import { resultTypeToName } from './constants' import { searchBarLogic } from './searchBarLogic' @@ -8,16 +10,43 @@ type SearchBarTabProps = { type: ResultTypeWithAll active: boolean count?: number | null + inputRef: RefObject } -const SearchBarTab = ({ type, active, count }: SearchBarTabProps): JSX.Element => { +export const SearchBarTab = ({ type, active, count, inputRef }: SearchBarTabProps): JSX.Element => { const { setActiveTab } = useActions(searchBarLogic) return ( -
    setActiveTab(type)}> +
    { + setActiveTab(type) + inputRef.current?.focus() + }} + > {resultTypeToName[type]} - {count != null && {count}} +
    ) } -export default SearchBarTab +type CountProps = { + type: ResultTypeWithAll + active: boolean + count?: number | null +} + +const Count = ({ type, active, count }: CountProps): JSX.Element | null => { + const { searchResponseLoading } = useValues(searchBarLogic) + + if (type === 'all') { + return null + } else if (active && searchResponseLoading) { + return + } else if (count != null) { + return {count} + } else { + return + } +} diff --git a/frontend/src/lib/components/CommandBar/SearchInput.tsx b/frontend/src/lib/components/CommandBar/SearchInput.tsx index 01cf8f05d32ad..3d79b64531e78 100644 --- a/frontend/src/lib/components/CommandBar/SearchInput.tsx +++ b/frontend/src/lib/components/CommandBar/SearchInput.tsx @@ -1,28 +1,36 @@ +import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { isMac } from 'lib/utils' +import { forwardRef, Ref } from 'react' +import { teamLogic } from 'scenes/teamLogic' -import { LemonInput } from '@posthog/lemon-ui' import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { searchBarLogic } from './searchBarLogic' -const SearchInput = (): JSX.Element => { +export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref): JSX.Element { + const { currentTeam } = useValues(teamLogic) const { searchQuery } = useValues(searchBarLogic) const { setSearchQuery } = useActions(searchBarLogic) + const modifierKey = isMac() ? '⌘' : '^' + const placeholder = currentTeam + ? `Search the ${currentTeam.name} project or press ${modifierKey}⇧K to go to commands…` + : `Search or press ${modifierKey}⇧K to go to commands…` + return (
    } + suffix={} + placeholder={placeholder} autoFocus value={searchQuery} onChange={setSearchQuery} />
    ) -} - -export default SearchInput +}) diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx index 4d86c69704cfd..9a14a6203fa5a 100644 --- a/frontend/src/lib/components/CommandBar/SearchResult.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx @@ -1,10 +1,17 @@ -import { useLayoutEffect, useRef } from 'react' +import { LemonSkeleton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { useLayoutEffect, useRef } from 'react' +import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { mathsLogic } from 'scenes/trends/mathsLogic' + +import { cohortsModel } from '~/models/cohortsModel' +import { groupsModel } from '~/models/groupsModel' +import { Node } from '~/queries/schema' +import { FilterType } from '~/types' import { resultTypeToName } from './constants' import { searchBarLogic, urlForResult } from './searchBarLogic' import { SearchResult as SearchResultType } from './types' -import { LemonSkeleton } from '@posthog/lemon-ui' type SearchResultProps = { result: SearchResultType @@ -13,7 +20,7 @@ type SearchResultProps = { keyboardFocused: boolean } -const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchResultProps): JSX.Element => { +export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchResultProps): JSX.Element => { const { isAutoScrolling } = useValues(searchBarLogic) const { onMouseEnterResult, onMouseLeaveResult, openResult, setIsAutoScrolling } = useActions(searchBarLogic) @@ -41,9 +48,7 @@ const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchR return (
    { if (isAutoScrolling) { return @@ -63,7 +68,9 @@ const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchR >
    {resultTypeToName[result.type]} - {result.name} + + + {location.host} {urlForResult(result)} @@ -74,13 +81,55 @@ const SearchResult = ({ result, resultIndex, focused, keyboardFocused }: SearchR } export const SearchResultSkeleton = (): JSX.Element => ( -
    -
    - - - -
    +
    + + +
    ) -export default SearchResult +type ResultNameProps = { + result: SearchResultType +} + +export const ResultName = ({ result }: ResultNameProps): JSX.Element | null => { + const { aggregationLabel } = useValues(groupsModel) + const { cohortsById } = useValues(cohortsModel) + const { mathDefinitions } = useValues(mathsLogic) + + const { type, extra_fields } = result + if (type === 'insight') { + return extra_fields.name ? ( + {extra_fields.name} + ) : ( + + {summarizeInsight(extra_fields.query as Node | null, extra_fields.filters as Partial, { + aggregationLabel, + cohortsById, + mathDefinitions, + })} + + ) + } else if (type === 'feature_flag') { + return {extra_fields.key} + } else if (type === 'notebook') { + return {extra_fields.title} + } else { + return {extra_fields.name} + } +} + +export const ResultDescription = ({ result }: ResultNameProps): JSX.Element | null => { + const { type, extra_fields } = result + if (type === 'feature_flag') { + return extra_fields.name && extra_fields.name !== extra_fields.key ? ( + {extra_fields.name} + ) : ( + No description. + ) + } else if (type === 'notebook') { + return {extra_fields.text_content} + } else { + return extra_fields.description ? {extra_fields.description} : No description. + } +} diff --git a/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx new file mode 100644 index 0000000000000..57bd498e353ea --- /dev/null +++ b/frontend/src/lib/components/CommandBar/SearchResultPreview.tsx @@ -0,0 +1,27 @@ +import { useValues } from 'kea' +import { ResultDescription, ResultName } from 'lib/components/CommandBar/SearchResult' + +import { resultTypeToName } from './constants' +import { searchBarLogic } from './searchBarLogic' + +export const SearchResultPreview = (): JSX.Element | null => { + const { activeResultIndex, filterSearchResults } = useValues(searchBarLogic) + + if (!filterSearchResults || filterSearchResults.length === 0) { + return null + } + + const result = filterSearchResults[activeResultIndex] + + return ( +
    +
    {resultTypeToName[result.type]}
    +
    + +
    +
    + +
    +
    + ) +} diff --git a/frontend/src/lib/components/CommandBar/SearchResults.tsx b/frontend/src/lib/components/CommandBar/SearchResults.tsx index d8355a46597e8..c385bd315ad44 100644 --- a/frontend/src/lib/components/CommandBar/SearchResults.tsx +++ b/frontend/src/lib/components/CommandBar/SearchResults.tsx @@ -1,42 +1,45 @@ import { useValues } from 'kea' import { DetectiveHog } from '../hedgehogs' - import { searchBarLogic } from './searchBarLogic' -import SearchResult, { SearchResultSkeleton } from './SearchResult' +import { SearchResult, SearchResultSkeleton } from './SearchResult' +import { SearchResultPreview } from './SearchResultPreview' -const SearchResults = (): JSX.Element => { +export const SearchResults = (): JSX.Element => { const { filterSearchResults, searchResponseLoading, activeResultIndex, keyboardResultIndex } = useValues(searchBarLogic) return ( -
    - {searchResponseLoading && ( - <> - - - - - )} - {!searchResponseLoading && filterSearchResults?.length === 0 && ( -
    -

    No results

    -

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

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

    No results

    +

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

    + +
    + )} + {!searchResponseLoading && + filterSearchResults?.map((result, index) => ( + + ))} +
    +
    + +
    ) } - -export default SearchResults diff --git a/frontend/src/lib/components/CommandBar/SearchTabs.tsx b/frontend/src/lib/components/CommandBar/SearchTabs.tsx index e72700be4f1b1..4e3d65c29cadd 100644 --- a/frontend/src/lib/components/CommandBar/SearchTabs.tsx +++ b/frontend/src/lib/components/CommandBar/SearchTabs.tsx @@ -1,10 +1,15 @@ import { useValues } from 'kea' +import { RefObject } from 'react' import { searchBarLogic } from './searchBarLogic' -import SearchBarTab from './SearchBarTab' +import { SearchBarTab } from './SearchBarTab' import { ResultType } from './types' -const SearchTabs = (): JSX.Element | null => { +type SearchTabsProps = { + inputRef: RefObject +} + +export const SearchTabs = ({ inputRef }: SearchTabsProps): JSX.Element | null => { const { searchResponse, activeTab } = useValues(searchBarLogic) if (!searchResponse) { @@ -12,12 +17,17 @@ const SearchTabs = (): JSX.Element | null => { } return ( -
    - +
    + {Object.entries(searchResponse.counts).map(([type, count]) => ( - + ))}
    ) } -export default SearchTabs diff --git a/frontend/src/lib/components/CommandBar/Shortcuts.tsx b/frontend/src/lib/components/CommandBar/Shortcuts.tsx new file mode 100644 index 0000000000000..c39e7b597b3bb --- /dev/null +++ b/frontend/src/lib/components/CommandBar/Shortcuts.tsx @@ -0,0 +1,24 @@ +import { useMountedLogic } from 'kea' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' + +import { shortcutsLogic } from './shortcutsLogic' + +export const Shortcuts = (): JSX.Element => { + useMountedLogic(shortcutsLogic) + + return ( +
    +

    Keyboard shortcuts

    +

    Site-wide shortcuts

    +
    +
    + Open search +
    +
    + Open command palette +
    +
    +
    + ) +} diff --git a/frontend/src/lib/components/CommandBar/actionBarLogic.ts b/frontend/src/lib/components/CommandBar/actionBarLogic.ts index aaee36bc40e3b..c936929fed9c2 100644 --- a/frontend/src/lib/components/CommandBar/actionBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/actionBarLogic.ts @@ -1,12 +1,10 @@ -import { kea, path, listeners, connect, afterMount, beforeUnmount } from 'kea' +import { afterMount, beforeUnmount, connect, kea, listeners, path } from 'kea' import { commandPaletteLogic } from '../CommandPalette/commandPaletteLogic' +import type { actionBarLogicType } from './actionBarLogicType' import { commandBarLogic } from './commandBarLogic' - import { BarStatus } from './types' -import type { actionBarLogicType } from './actionBarLogicType' - export const actionBarLogic = kea([ path(['lib', 'components', 'CommandBar', 'actionBarLogic']), connect({ @@ -65,7 +63,7 @@ export const actionBarLogic = kea([ // navigate to previous result event.preventDefault() actions.onArrowUp() - } else if (event.key === 'Escape') { + } else if (event.key === 'Escape' && event.repeat === false) { event.preventDefault() if (values.activeFlow) { @@ -79,7 +77,7 @@ export const actionBarLogic = kea([ actions.hidePalette() } } else if (event.key === 'Backspace') { - if (values.input.length === 0) { + if (values.input.length === 0 && event.repeat === false) { // transition to search when pressing backspace with empty input actions.setCommandBar(BarStatus.SHOW_SEARCH) } diff --git a/frontend/src/lib/components/CommandBar/commandBarLogic.ts b/frontend/src/lib/components/CommandBar/commandBarLogic.ts index 59aeab9a38862..ef6df079ddea1 100644 --- a/frontend/src/lib/components/CommandBar/commandBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/commandBarLogic.ts @@ -1,7 +1,7 @@ -import { kea, path, actions, reducers, afterMount, beforeUnmount } from 'kea' -import { BarStatus } from './types' +import { actions, afterMount, beforeUnmount, kea, path, reducers } from 'kea' import type { commandBarLogicType } from './commandBarLogicType' +import { BarStatus } from './types' export const commandBarLogic = kea([ path(['lib', 'components', 'CommandBar', 'commandBarLogic']), @@ -10,6 +10,7 @@ export const commandBarLogic = kea([ hideCommandBar: true, toggleSearchBar: true, toggleActionsBar: true, + toggleShortcutOverview: true, }), reducers({ barStatus: [ @@ -18,9 +19,15 @@ export const commandBarLogic = kea([ setCommandBar: (_, { status }) => status, hideCommandBar: () => BarStatus.HIDDEN, toggleSearchBar: (previousState) => - previousState === BarStatus.HIDDEN ? BarStatus.SHOW_SEARCH : BarStatus.HIDDEN, + [BarStatus.HIDDEN, BarStatus.SHOW_SHORTCUTS].includes(previousState) + ? BarStatus.SHOW_SEARCH + : BarStatus.HIDDEN, toggleActionsBar: (previousState) => - previousState === BarStatus.HIDDEN ? BarStatus.SHOW_ACTIONS : BarStatus.HIDDEN, + [BarStatus.HIDDEN, BarStatus.SHOW_SHORTCUTS].includes(previousState) + ? BarStatus.SHOW_ACTIONS + : BarStatus.HIDDEN, + toggleShortcutOverview: (previousState) => + previousState === BarStatus.HIDDEN ? BarStatus.SHOW_SHORTCUTS : previousState, }, ], }), @@ -36,6 +43,8 @@ export const commandBarLogic = kea([ // cmd+k opens search actions.toggleSearchBar() } + } else if (event.shiftKey && event.key === '?') { + actions.toggleShortcutOverview() } } window.addEventListener('keydown', cache.onKeyDown) diff --git a/frontend/src/lib/components/CommandBar/constants.ts b/frontend/src/lib/components/CommandBar/constants.ts index 4ee4981b0715e..14396bb019f20 100644 --- a/frontend/src/lib/components/CommandBar/constants.ts +++ b/frontend/src/lib/components/CommandBar/constants.ts @@ -6,8 +6,9 @@ export const resultTypeToName: Record = { cohort: 'Cohorts', dashboard: 'Dashboards', experiment: 'Experiments', - feature_flag: 'Feature Flags', + feature_flag: 'Feature flags', insight: 'Insights', + notebook: 'Notebooks', } export const actionScopeToName: Record = { diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss index 80621cf83d7c9..c8a200a7f5740 100644 --- a/frontend/src/lib/components/CommandBar/index.scss +++ b/frontend/src/lib/components/CommandBar/index.scss @@ -1,4 +1,17 @@ .CommandBar__input { border-color: transparent !important; border-radius: 0; + height: 2.75rem; + padding-left: 0.75rem; + padding-right: 0.375rem; +} + +.SearchBarTab { + &:hover { + border-top: 2px solid var(--border-3000); + } + + &.SearchBarTab__active { + border-color: var(--primary-3000); + } } diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts index c97781bf2141d..2f3b04715c598 100644 --- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts @@ -1,12 +1,12 @@ -import { kea, path, actions, reducers, selectors, listeners, connect, afterMount, beforeUnmount } from 'kea' +import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { router } from 'kea-router' - import api from 'lib/api' import { urls } from 'scenes/urls' + import { InsightShortId } from '~/types' -import { commandBarLogic } from './commandBarLogic' +import { commandBarLogic } from './commandBarLogic' import type { searchBarLogicType } from './searchBarLogicType' import { BarStatus, ResultTypeWithAll, SearchResponse, SearchResult } from './types' @@ -25,17 +25,23 @@ export const searchBarLogic = kea([ setIsAutoScrolling: (scrolling: boolean) => ({ scrolling }), openResult: (index: number) => ({ index }), }), - loaders({ + loaders(({ values }) => ({ searchResponse: [ null as SearchResponse | null, { - setSearchQuery: async ({ query }, breakpoint) => { + loadSearchResponse: async (_, breakpoint) => { await breakpoint(300) - return await api.get(`api/projects/@current/search?q=${query}`) + if (values.activeTab === 'all') { + return await api.get(`api/projects/@current/search?q=${values.searchQuery}`) + } else { + return await api.get( + `api/projects/@current/search?q=${values.searchQuery}&entities=${values.activeTab}` + ) + } }, }, ], - }), + })), reducers({ searchQuery: ['', { setSearchQuery: (_, { query }) => query }], keyboardResultIndex: [ @@ -84,14 +90,19 @@ export const searchBarLogic = kea([ (s) => [s.keyboardResultIndex, s.hoverResultIndex], (keyboardResultIndex: number, hoverResultIndex: number | null) => hoverResultIndex || keyboardResultIndex, ], + tabs: [ + (s) => [s.searchCounts], + (counts): ResultTypeWithAll[] => ['all', ...(Object.keys(counts || {}) as ResultTypeWithAll[])], + ], }), listeners(({ values, actions }) => ({ openResult: ({ index }) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const result = values.searchResults![index] router.actions.push(urlForResult(result)) actions.hideCommandBar() }, + setSearchQuery: actions.loadSearchResponse, + setActiveTab: actions.loadSearchResponse, })), afterMount(({ actions, values, cache }) => { // load initial results @@ -111,15 +122,31 @@ export const searchBarLogic = kea([ // navigate to previous result event.preventDefault() actions.onArrowUp(values.activeResultIndex, values.maxIndex) - } else if (event.key === 'Escape') { + } else if (event.key === 'Escape' && event.repeat === false) { // hide command bar actions.hideCommandBar() } else if (event.key === '>') { - if (values.searchQuery.length === 0) { - // transition to actions when entering '>' with empty input + const { value, selectionStart, selectionEnd } = event.target as HTMLInputElement + if ( + values.searchQuery.length === 0 || + (selectionStart !== null && + selectionEnd !== null && + (value.substring(0, selectionStart) + value.substring(selectionEnd)).length === 0) + ) { + // transition to actions when entering '>' with empty input, or when replacing the whole input event.preventDefault() actions.setCommandBar(BarStatus.SHOW_ACTIONS) } + } else if (event.key === 'Tab') { + event.preventDefault() + const currentIndex = values.tabs.findIndex((tab) => tab === values.activeTab) + if (event.shiftKey) { + const prevIndex = currentIndex === 0 ? values.tabs.length - 1 : currentIndex - 1 + actions.setActiveTab(values.tabs[prevIndex]) + } else { + const nextIndex = currentIndex === values.tabs.length - 1 ? 0 : currentIndex + 1 + actions.setActiveTab(values.tabs[nextIndex]) + } } } window.addEventListener('keydown', cache.onKeyDown) @@ -144,6 +171,8 @@ export const urlForResult = (result: SearchResult): string => { return urls.featureFlag(result.result_id) case 'insight': return urls.insightView(result.result_id as InsightShortId) + case 'notebook': + return urls.notebook(result.result_id) default: throw new Error(`No action for type '${result.type}' defined.`) } diff --git a/frontend/src/lib/components/CommandBar/shortcutsLogic.ts b/frontend/src/lib/components/CommandBar/shortcutsLogic.ts new file mode 100644 index 0000000000000..4e70c5920f41c --- /dev/null +++ b/frontend/src/lib/components/CommandBar/shortcutsLogic.ts @@ -0,0 +1,25 @@ +import { afterMount, beforeUnmount, connect, kea, path } from 'kea' + +import { commandBarLogic } from './commandBarLogic' +import type { shortcutsLogicType } from './shortcutsLogicType' + +export const shortcutsLogic = kea([ + path(['lib', 'components', 'CommandBar', 'shortcutsLogic']), + connect({ + actions: [commandBarLogic, ['hideCommandBar']], + }), + afterMount(({ actions, cache }) => { + // register keyboard shortcuts + cache.onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + // hide command bar + actions.hideCommandBar() + } + } + window.addEventListener('keydown', cache.onKeyDown) + }), + beforeUnmount(({ cache }) => { + // unregister keyboard shortcuts + window.removeEventListener('keydown', cache.onKeyDown) + }), +]) diff --git a/frontend/src/lib/components/CommandBar/types.ts b/frontend/src/lib/components/CommandBar/types.ts index 4afc7e3ff66d2..3a6a482c26453 100644 --- a/frontend/src/lib/components/CommandBar/types.ts +++ b/frontend/src/lib/components/CommandBar/types.ts @@ -2,13 +2,19 @@ export enum BarStatus { HIDDEN = 'hidden', SHOW_SEARCH = 'show_search', SHOW_ACTIONS = 'show_actions', + SHOW_SHORTCUTS = 'show_shortcuts', } -export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experiment' | 'feature_flag' +export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experiment' | 'feature_flag' | 'notebook' export type ResultTypeWithAll = ResultType | 'all' -export type SearchResult = { result_id: string; type: ResultType; name: string | null } +export type SearchResult = { + result_id: string + type: ResultType + name: string | null + extra_fields: Record +} export type SearchResponse = { results: SearchResult[] diff --git a/frontend/src/lib/components/CommandPalette/CommandInput.tsx b/frontend/src/lib/components/CommandPalette/CommandInput.tsx index 38186438f41ac..57a6821ae27db 100644 --- a/frontend/src/lib/components/CommandPalette/CommandInput.tsx +++ b/frontend/src/lib/components/CommandPalette/CommandInput.tsx @@ -1,7 +1,8 @@ -import { useValues, useActions } from 'kea' -import { commandPaletteLogic } from './commandPaletteLogic' +import { useActions, useValues } from 'kea' import { IconEdit, IconExclamation, IconMagnifier } from 'lib/lemon-ui/icons' +import { commandPaletteLogic } from './commandPaletteLogic' + export function CommandInput(): JSX.Element { const { input, isSqueak, activeFlow } = useValues(commandPaletteLogic) const { setInput } = useActions(commandPaletteLogic) diff --git a/frontend/src/lib/components/CommandPalette/CommandPalette.scss b/frontend/src/lib/components/CommandPalette/CommandPalette.scss index 1ef01681263c1..55079ad3ac496 100644 --- a/frontend/src/lib/components/CommandPalette/CommandPalette.scss +++ b/frontend/src/lib/components/CommandPalette/CommandPalette.scss @@ -29,6 +29,7 @@ max-height: 80%; width: 100%; } + @media (max-height: 500px) { top: 0%; max-height: 100%; @@ -84,6 +85,7 @@ .palette__result--focused { background: var(--default-dark); + &::before, &::after { content: ''; @@ -93,28 +95,31 @@ bottom: 0; width: 0.375rem; } + &::before { - background: hsla(210, 10%, 19%, 1) !important; + background: hsl(210deg 10% 19% / 100%) !important; } + &::after { - background: rgba(255, 255, 255, 0.1); + background: rgb(255 255 255 / 10%); } } .palette__result--executable { cursor: pointer; + &::after { - background: var(--primary); + background: var(--primary-3000); } } .palette__scope { - background-color: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.8); + background-color: rgb(255 255 255 / 10%); + color: rgb(255 255 255 / 80%); } .palette__icon { display: flex; align-items: center; - font-size: 1rem; + font-size: 1.25rem; } diff --git a/frontend/src/lib/components/CommandPalette/CommandPalette.tsx b/frontend/src/lib/components/CommandPalette/CommandPalette.tsx index b1bb0b416ac12..20f801a408170 100644 --- a/frontend/src/lib/components/CommandPalette/CommandPalette.tsx +++ b/frontend/src/lib/components/CommandPalette/CommandPalette.tsx @@ -1,15 +1,17 @@ -import { useRef, useMemo } from 'react' +import './CommandPalette.scss' + +import { useActions, useMountedLogic, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { useEventListener } from 'lib/hooks/useEventListener' import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler' -import { useMountedLogic, useValues, useActions } from 'kea' -import { commandPaletteLogic } from './commandPaletteLogic' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import squeakFile from 'public/squeak.mp3' +import { useMemo, useRef } from 'react' + +import { CommandBar } from '../CommandBar/CommandBar' import { CommandInput } from './CommandInput' +import { commandPaletteLogic } from './commandPaletteLogic' import { CommandResults } from './CommandResults' -import { useEventListener } from 'lib/hooks/useEventListener' -import squeakFile from 'public/squeak.mp3' -import './CommandPalette.scss' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import CommandBar from '../CommandBar/CommandBar' /** Use the new Cmd+K search when the respective feature flag is enabled. */ export function CommandPalette(): JSX.Element { @@ -36,7 +38,7 @@ function _CommandPalette(): JSX.Element | null { useEventListener('keydown', (event) => { if (isSqueak && event.key === 'Enter') { - squeakAudio?.play() + void squeakAudio?.play() } else if (event.key === 'Escape') { event.preventDefault() // Return to previous flow diff --git a/frontend/src/lib/components/CommandPalette/CommandResults.tsx b/frontend/src/lib/components/CommandPalette/CommandResults.tsx index 81c7f2d087a0e..37d1814dcbc11 100644 --- a/frontend/src/lib/components/CommandPalette/CommandResults.tsx +++ b/frontend/src/lib/components/CommandPalette/CommandResults.tsx @@ -1,7 +1,8 @@ +import { useActions, useMountedLogic, useValues } from 'kea' +import { useEventListener } from 'lib/hooks/useEventListener' import { useEffect, useRef } from 'react' + import { CommandResultDisplayable } from './commandPaletteLogic' -import { useEventListener } from 'lib/hooks/useEventListener' -import { useActions, useMountedLogic, useValues } from 'kea' import { commandPaletteLogic } from './commandPaletteLogic' interface CommandResultProps { diff --git a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx index bef1adb4210d1..2a87d26d1deea 100644 --- a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx +++ b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx @@ -1,16 +1,17 @@ +import { actions, afterMount, kea, path, reducers, selectors, useActions, useValues } from 'kea' +import { loaders } from 'kea-loaders' import api from 'lib/api' import { dayjs } from 'lib/dayjs' +import { IconRefresh } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonTable } from 'lib/lemon-ui/LemonTable' + import { CodeSnippet, Language } from '../CodeSnippet' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { actions, afterMount, kea, reducers, selectors, useActions, useValues, path } from 'kea' -import { loaders } from 'kea-loaders' import type { debugCHQueriesLogicType } from './DebugCHQueriesType' -import { IconRefresh } from 'lib/lemon-ui/icons' -export async function debugCHQueries(): Promise { +export function openCHQueriesDebugModal(): void { LemonDialog.open({ title: 'ClickHouse queries recently executed for this user', content: , diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 6e83d77772135..8dedc08691066 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -1,53 +1,70 @@ -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import { router } from 'kea-router' -import type { commandPaletteLogicType } from './commandPaletteLogicType' -import Fuse from 'fuse.js' -import { dashboardsModel } from '~/models/dashboardsModel' -import { Parser } from 'expr-eval' -import { DashboardType, InsightType } from '~/types' -import api from 'lib/api' -import { copyToClipboard, isMobile, isURL, sample, uniqueBy } from 'lib/utils' -import { userLogic } from 'scenes/userLogic' -import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' -import { teamLogic } from 'scenes/teamLogic' -import posthog from 'posthog-js' -import { debugCHQueries } from './DebugCHQueries' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { urls } from 'scenes/urls' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { - IconAction, IconApps, - IconBarChart, - IconCalculate, - IconCheckmark, - IconCohort, - IconComment, - IconCorporate, - IconCottage, - IconEmojiPeople, - IconFlag, - IconFunnelHorizontal, - IconGauge, + IconAsterisk, + IconCalculator, + IconChat, + IconCheck, + IconCursor, + IconDashboard, + IconDatabase, + IconDay, + IconExternal, + IconFunnels, + IconGear, IconGithub, + IconGraph, + IconHogQL, + IconHome, IconKeyboard, + IconLeave, + IconLifecycle, + IconList, IconLive, - IconLockOpen, - IconLogout, - IconOpenInNew, - IconPerson, - IconPersonFilled, - IconRecording, + IconNight, + IconNotebook, + IconPageChart, + IconPeople, + IconPeopleFilled, + IconPieChart, + IconRetention, + IconRewindPlay, + IconRocket, IconServer, - IconSettings, - IconTableChart, - IconTools, - IconTrendingFlat, - IconTrendingUp, -} from 'lib/lemon-ui/icons' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' + IconStickiness, + IconTestTube, + IconThoughtBubble, + IconToggle, + IconToolbar, + IconTrends, + IconUnlock, + IconUserPaths, +} from '@posthog/icons' +import { Parser } from 'expr-eval' +import Fuse from 'fuse.js' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { router } from 'kea-router' +import api from 'lib/api' import { FEATURE_FLAGS } from 'lib/constants' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { isMobile, isURL, uniqueBy } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import posthog from 'posthog-js' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { insightTypeURL } from 'scenes/insights/utils' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { ThemeIcon } from '~/layout/navigation-3000/components/Navbar' +import { themeLogic } from '~/layout/navigation-3000/themeLogic' +import { dashboardsModel } from '~/models/dashboardsModel' +import { DashboardType, InsightType } from '~/types' + +import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' +import type { commandPaletteLogicType } from './commandPaletteLogicType' +import { openCHQueriesDebugModal } from './DebugCHQueries' // If CommandExecutor returns CommandFlow, flow will be entered export type CommandExecutor = () => CommandFlow | void @@ -117,7 +134,7 @@ function resolveCommand(source: Command | CommandFlow, argument?: string, prefix export const commandPaletteLogic = kea([ path(['lib', 'components', 'CommandPalette', 'commandPaletteLogic']), connect({ - actions: [personalAPIKeysLogic, ['createKey'], router, ['push']], + actions: [personalAPIKeysLogic, ['createKey'], router, ['push'], themeLogic, ['overrideTheme']], values: [teamLogic, ['currentTeam'], userLogic, ['user'], featureFlagLogic, ['featureFlags']], logic: [preflightLogic], }), @@ -239,7 +256,7 @@ export const commandPaletteLogic = kea([ key: 'custom_dashboards', resolver: dashboards.map((dashboard: DashboardType) => ({ key: `dashboard_${dashboard.id}`, - icon: IconTableChart, + icon: IconPageChart, display: `Go to dashboard: ${dashboard.name}`, executor: () => { const { push } = router.actions @@ -316,7 +333,7 @@ export const commandPaletteLogic = kea([ .search(argument) .slice(0, RESULTS_MAX) .map((result) => result.item) - : sample(fusableResults, RESULTS_MAX - guaranteedResults.length) + : fusableResults.slice(0, RESULTS_MAX) return guaranteedResults.concat(fusedResults) }, ], @@ -394,7 +411,7 @@ export const commandPaletteLogic = kea([ key: `person-${person.distinct_ids[0]}`, resolver: [ { - icon: IconPersonFilled, + icon: IconPeopleFilled, display: `View person ${input}`, executor: () => { const { push } = router.actions @@ -418,67 +435,128 @@ export const commandPaletteLogic = kea([ prefixes: ['open', 'visit'], resolver: [ { - icon: IconGauge, + icon: IconDashboard, display: 'Go to Dashboards', executor: () => { push(urls.dashboards()) }, }, { - icon: IconBarChart, + icon: IconHome, + display: 'Go to Project homepage', + executor: () => { + push(urls.projectHomepage()) + }, + }, + { + icon: IconGraph, display: 'Go to Insights', executor: () => { push(urls.savedInsights()) }, }, { - icon: IconTrendingUp, - display: 'Go to Trends', + icon: IconTrends, + display: 'Create a new Trend insight', executor: () => { // TODO: Don't reset insight on change push(urls.insightNew({ insight: InsightType.TRENDS })) }, }, { - icon: IconFunnelHorizontal, - display: 'Go to Funnels', + icon: IconFunnels, + display: 'Create a new Funnel insight', executor: () => { // TODO: Don't reset insight on change push(urls.insightNew({ insight: InsightType.FUNNELS })) }, }, { - icon: IconTrendingFlat, - display: 'Go to Retention', + icon: IconRetention, + display: 'Create a new Retention insight', executor: () => { // TODO: Don't reset insight on change push(urls.insightNew({ insight: InsightType.RETENTION })) }, }, { - icon: IconEmojiPeople, - display: 'Go to Paths', + icon: IconUserPaths, + display: 'Create a new Paths insight', executor: () => { // TODO: Don't reset insight on change push(urls.insightNew({ insight: InsightType.PATHS })) }, }, + { + icon: IconStickiness, + display: 'Create a new Stickiness insight', + executor: () => { + // TODO: Don't reset insight on change + push(urls.insightNew({ insight: InsightType.STICKINESS })) + }, + }, + { + icon: IconLifecycle, + display: 'Create a new Lifecycle insight', + executor: () => { + // TODO: Don't reset insight on change + push(urls.insightNew({ insight: InsightType.LIFECYCLE })) + }, + }, + { + icon: IconHogQL, + display: 'Create a new HogQL insight', + synonyms: ['hogql', 'sql'], + executor: () => { + // TODO: Don't reset insight on change + push(insightTypeURL[InsightType.SQL]) + }, + }, + { + icon: IconNotebook, + display: 'Go to Notebooks', + executor: () => { + push(urls.notebooks()) + }, + }, { icon: IconLive, - display: 'Go to Events', + display: 'Go to Events explorer', executor: () => { push(urls.events()) }, }, { - icon: IconAction, + icon: IconDatabase, + display: 'Go to Data management', + synonyms: ['events'], + executor: () => { + push(urls.eventDefinitions()) + }, + }, + { + icon: IconCursor, display: 'Go to Actions', executor: () => { push(urls.actions()) }, }, { - icon: IconPerson, + icon: IconList, + display: 'Go to Properties', + executor: () => { + push(urls.propertyDefinitions()) + }, + }, + { + icon: IconThoughtBubble, + display: 'Go to Annotations', + executor: () => { + push(urls.annotations()) + }, + }, + { + icon: IconPeople, display: 'Go to Persons', synonyms: ['people'], executor: () => { @@ -486,77 +564,110 @@ export const commandPaletteLogic = kea([ }, }, { - icon: IconCohort, + icon: IconPeople, display: 'Go to Cohorts', executor: () => { push(urls.cohorts()) }, }, + ...(values.featureFlags[FEATURE_FLAGS.WEB_ANALYTICS] + ? [ + { + icon: IconPieChart, + display: 'Go to Web analytics', + executor: () => { + push(urls.webAnalytics()) + }, + }, + ] + : []), + ...(values.featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE] + ? [ + { + icon: IconServer, + display: 'Go to Data warehouse', + executor: () => { + push(urls.dataWarehouse()) + }, + }, + ] + : []), + { + display: 'Go to Session replay', + icon: IconRewindPlay, + executor: () => { + push(urls.replay()) + }, + }, { - icon: IconFlag, - display: 'Go to Feature Flags', - synonyms: ['feature flags', 'a/b tests'], + display: 'Go to Surveys', + icon: IconChat, + executor: () => { + push(urls.surveys()) + }, + }, + { + icon: IconToggle, + display: 'Go to Feature flags', executor: () => { push(urls.featureFlags()) }, }, { - icon: IconComment, - display: 'Go to Annotations', + icon: IconTestTube, + display: 'Go to A/B testing', executor: () => { - push(urls.annotations()) + push(urls.experiments()) }, }, { - icon: IconCorporate, - display: 'Go to Team members', - synonyms: ['organization', 'members', 'invites', 'teammates'], + icon: IconRocket, + display: 'Go to Early access features', executor: () => { - push(urls.settings('organization')) + push(urls.earlyAccessFeatures()) }, }, { - icon: IconCottage, - display: 'Go to project homepage', + icon: IconApps, + display: 'Go to Apps', + synonyms: ['integrations'], executor: () => { - push(urls.projectHomepage()) + push(urls.projectApps()) }, }, { - icon: IconSettings, - display: 'Go to Project settings', + icon: IconToolbar, + display: 'Go to Toolbar', executor: () => { - push(urls.settings('project')) + push(urls.toolbarLaunch()) }, }, { - icon: () => ( - - ), - display: 'Go to My settings', - synonyms: ['account'], + icon: IconGear, + display: 'Go to Project settings', executor: () => { - push(urls.settings('user')) + push(urls.settings('project')) }, }, { - icon: IconApps, - display: 'Go to Apps', - synonyms: ['integrations'], + icon: IconGear, + display: 'Go to Organization settings', executor: () => { - push(urls.projectApps()) + push(urls.settings('organization')) }, }, { - icon: IconServer, - display: 'Go to Instance status & settings', - synonyms: ['redis', 'celery', 'django', 'postgres', 'backend', 'service', 'online'], + icon: () => ( + + ), + display: 'Go to User settings', + synonyms: ['account', 'profile'], executor: () => { - push(urls.instanceStatus()) + push(urls.settings('user')) }, }, { - icon: IconLogout, + icon: IconLeave, display: 'Log out', executor: () => { userLogic.actions.logout() @@ -574,11 +685,9 @@ export const commandPaletteLogic = kea([ preflightLogic.values.preflight?.is_debug || preflightLogic.values.preflight?.instance_preferences?.debug_queries ? { - icon: IconTools, + icon: IconDatabase, display: 'Debug ClickHouse Queries', - executor: () => { - debugCHQueries() - }, + executor: () => openCHQueriesDebugModal(), } : [], } @@ -587,7 +696,7 @@ export const commandPaletteLogic = kea([ key: 'debug-copy-session-recording-url', scope: GLOBAL_COMMAND_SCOPE, resolver: { - icon: IconRecording, + icon: IconRewindPlay, display: 'Debug: Copy the session recording link to clipboard', executor: () => { const url = posthog.get_session_replay_url({ withTimestamp: true, timestampLookBack: 30 }) @@ -609,7 +718,7 @@ export const commandPaletteLogic = kea([ return isNaN(result) ? null : { - icon: IconCalculate, + icon: IconCalculator, display: `= ${result}`, guarantee: true, executor: () => { @@ -629,7 +738,7 @@ export const commandPaletteLogic = kea([ resolver: (argument) => { const results: CommandResultTemplate[] = (teamLogic.values.currentTeam?.app_urls ?? []).map( (url: string) => ({ - icon: IconOpenInNew, + icon: IconExternal, display: `Open ${url}`, synonyms: [`Visit ${url}`], executor: () => { @@ -639,7 +748,7 @@ export const commandPaletteLogic = kea([ ) if (argument && isURL(argument)) { results.push({ - icon: IconOpenInNew, + icon: IconExternal, display: `Open ${argument}`, synonyms: [`Visit ${argument}`], executor: () => { @@ -648,7 +757,7 @@ export const commandPaletteLogic = kea([ }) } results.push({ - icon: IconOpenInNew, + icon: IconExternal, display: 'Open PostHog Docs', synonyms: ['technical documentation'], executor: () => { @@ -663,7 +772,7 @@ export const commandPaletteLogic = kea([ key: 'create-personal-api-key', scope: GLOBAL_COMMAND_SCOPE, resolver: { - icon: IconLockOpen, + icon: IconUnlock, display: 'Create Personal API Key', executor: () => ({ instruction: 'Give your key a label', @@ -672,7 +781,7 @@ export const commandPaletteLogic = kea([ resolver: (argument) => { if (argument?.length) { return { - icon: IconLockOpen, + icon: IconUnlock, display: `Create Key "${argument}"`, executor: () => { personalAPIKeysLogic.actions.createKey(argument) @@ -690,7 +799,7 @@ export const commandPaletteLogic = kea([ key: 'create-dashboard', scope: GLOBAL_COMMAND_SCOPE, resolver: { - icon: IconGauge, + icon: IconDashboard, display: 'Create Dashboard', executor: () => ({ instruction: 'Name your new dashboard', @@ -699,7 +808,7 @@ export const commandPaletteLogic = kea([ resolver: (argument) => { if (argument?.length) { return { - icon: IconGauge, + icon: IconDashboard, display: `Create Dashboard "${argument}"`, executor: () => { newDashboardLogic.actions.addDashboard({ name: argument }) @@ -716,7 +825,7 @@ export const commandPaletteLogic = kea([ key: 'share-feedback', scope: GLOBAL_COMMAND_SCOPE, resolver: { - icon: IconComment, + icon: IconThoughtBubble, display: 'Share Feedback', synonyms: ['send opinion', 'ask question', 'message posthog', 'github issue'], executor: () => ({ @@ -724,12 +833,12 @@ export const commandPaletteLogic = kea([ resolver: [ { display: 'Send Message Directly to PostHog', - icon: IconComment, + icon: IconThoughtBubble, executor: () => ({ instruction: "What's on your mind?", - icon: IconComment, + icon: IconThoughtBubble, resolver: (argument) => ({ - icon: IconComment, + icon: IconThoughtBubble, display: 'Send', executor: !argument?.length ? undefined @@ -737,7 +846,7 @@ export const commandPaletteLogic = kea([ posthog.capture('palette feedback', { message: argument }) return { resolver: { - icon: IconCheckmark, + icon: IconCheck, display: 'Message Sent!', executor: true, }, @@ -758,6 +867,42 @@ export const commandPaletteLogic = kea([ }, } + const toggleTheme: Command = { + key: 'toggle-theme', + scope: GLOBAL_COMMAND_SCOPE, + resolver: { + icon: ThemeIcon, + display: 'Switch theme', + synonyms: ['toggle theme', 'dark mode', 'light mode'], + executor: () => ({ + scope: 'Switch theme', + resolver: [ + { + icon: IconDay, + display: 'Light theme', + executor: () => { + actions.overrideTheme(false) + }, + }, + { + icon: IconNight, + display: 'Dark theme', + executor: () => { + actions.overrideTheme(true) + }, + }, + { + icon: IconAsterisk, + display: 'Sync with system settings', + executor: () => { + actions.overrideTheme(null) + }, + }, + ], + }), + }, + } + actions.registerCommand(goTo) actions.registerCommand(openUrls) actions.registerCommand(debugClickhouseQueries) @@ -766,6 +911,9 @@ export const commandPaletteLogic = kea([ actions.registerCommand(createDashboard) actions.registerCommand(shareFeedback) actions.registerCommand(debugCopySessionRecordingURL) + if (values.featureFlags[FEATURE_FLAGS.POSTHOG_3000]) { + actions.registerCommand(toggleTheme) + } }, beforeUnmount: () => { actions.deregisterCommand('go-to') @@ -776,6 +924,7 @@ export const commandPaletteLogic = kea([ actions.deregisterCommand('create-dashboard') actions.deregisterCommand('share-feedback') actions.deregisterCommand('debug-copy-session-recording-url') + actions.deregisterCommand('toggle-theme') }, })), ]) diff --git a/frontend/src/lib/components/CompactList/CompactList.scss b/frontend/src/lib/components/CompactList/CompactList.scss index 13dfe6b0dd1b2..5c0e76c6093a3 100644 --- a/frontend/src/lib/components/CompactList/CompactList.scss +++ b/frontend/src/lib/components/CompactList/CompactList.scss @@ -1,7 +1,7 @@ .compact-list { border-radius: var(--radius); border: 1px solid var(--border); - height: calc(19.5rem + 1px); + height: calc(19.25rem); background: var(--bg-light); box-sizing: content-box; display: flex; @@ -13,6 +13,7 @@ display: flex; align-items: center; justify-content: space-between; + h3 { margin-bottom: 0; font-weight: 600; @@ -21,14 +22,13 @@ } } - .spacer-container { - padding: 0 1rem; - } - .scrollable-list { flex: 1; - overflow-y: auto; - overflow-x: auto; - padding: 0 0.5rem 0.5rem; + overflow: auto; + padding: 0 0.5rem; + } + + .LemonButton { + font-family: var(--font-sans) !important; } } diff --git a/frontend/src/lib/components/CompactList/CompactList.stories.tsx b/frontend/src/lib/components/CompactList/CompactList.stories.tsx index dfce25599a81f..26d609a669a23 100644 --- a/frontend/src/lib/components/CompactList/CompactList.stories.tsx +++ b/frontend/src/lib/components/CompactList/CompactList.stories.tsx @@ -1,9 +1,9 @@ import { Meta } from '@storybook/react' - -import { CompactList } from './CompactList' -import { urls } from 'scenes/urls' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { PersonDisplay } from 'scenes/persons/PersonDisplay' +import { urls } from 'scenes/urls' + +import { CompactList } from './CompactList' const meta: Meta = { title: 'Components/Compact List', diff --git a/frontend/src/lib/components/CompactList/CompactList.tsx b/frontend/src/lib/components/CompactList/CompactList.tsx index 68cc77e0bb352..eaa3f392c1013 100644 --- a/frontend/src/lib/components/CompactList/CompactList.tsx +++ b/frontend/src/lib/components/CompactList/CompactList.tsx @@ -1,9 +1,11 @@ import './CompactList.scss' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { EmptyMessage, EmptyMessageProps } from '../EmptyMessage/EmptyMessage' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { EmptyMessage, EmptyMessageProps } from '../EmptyMessage/EmptyMessage' + interface CompactListProps { title: string viewAllURL?: string @@ -27,7 +29,7 @@ export function CompactList({

    {title}

    {viewAllURL && View all}
    -
    +
    diff --git a/frontend/src/lib/components/CompareFilter/CompareFilter.tsx b/frontend/src/lib/components/CompareFilter/CompareFilter.tsx index e3ecfa66a82ab..a70f38cc31c17 100644 --- a/frontend/src/lib/components/CompareFilter/CompareFilter.tsx +++ b/frontend/src/lib/components/CompareFilter/CompareFilter.tsx @@ -1,12 +1,15 @@ -import { useValues, useActions } from 'kea' -import { compareFilterLogic } from './compareFilterLogic' -import { insightLogic } from 'scenes/insights/insightLogic' import { LemonCheckbox } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' export function CompareFilter(): JSX.Element | null { - const { insightProps } = useValues(insightLogic) - const { compare, disabled } = useValues(compareFilterLogic(insightProps)) - const { setCompare } = useActions(compareFilterLogic(insightProps)) + const { insightProps, canEditInsight } = useValues(insightLogic) + + const { compare, supportsCompare } = useValues(insightVizDataLogic(insightProps)) + const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) + + const disabled: boolean = !canEditInsight || !supportsCompare // Hide compare filter control when disabled to avoid states where control is "disabled but checked" if (disabled) { @@ -15,9 +18,10 @@ export function CompareFilter(): JSX.Element | null { return ( { + updateInsightFilter({ compare }) + }} checked={!!compare} - disabled={disabled} label={Compare to previous period} bordered size="small" diff --git a/frontend/src/lib/components/CompareFilter/compareFilterLogic.ts b/frontend/src/lib/components/CompareFilter/compareFilterLogic.ts deleted file mode 100644 index 899d545987442..0000000000000 --- a/frontend/src/lib/components/CompareFilter/compareFilterLogic.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { kea, props, key, path, connect, actions, selectors, listeners } from 'kea' -import { ChartDisplayType, InsightLogicProps } from '~/types' -import type { compareFilterLogicType } from './compareFilterLogicType' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { insightLogic } from 'scenes/insights/insightLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' - -export const compareFilterLogic = kea([ - props({} as InsightLogicProps), - key(keyForInsightLogicProps('new')), - path((key) => ['lib', 'components', 'CompareFilter', 'compareFilterLogic', key]), - connect((props: InsightLogicProps) => ({ - values: [ - insightLogic(props), - ['canEditInsight'], - insightVizDataLogic(props), - ['compare', 'display', 'insightFilter', 'isLifecycle', 'dateRange'], - ], - actions: [insightVizDataLogic(props), ['updateInsightFilter']], - })), - actions(() => ({ - setCompare: (compare: boolean) => ({ compare }), - toggleCompare: true, - })), - selectors({ - disabled: [ - (s) => [s.canEditInsight, s.isLifecycle, s.display, s.dateRange], - (canEditInsight, isLifecycle, display, dateRange) => - !canEditInsight || - isLifecycle || - display === ChartDisplayType.WorldMap || - dateRange?.date_from === 'all', - ], - }), - listeners(({ values, actions }) => ({ - setCompare: ({ compare }) => { - actions.updateInsightFilter({ compare }) - }, - toggleCompare: () => { - actions.setCompare(!values.compare) - }, - })), -]) diff --git a/frontend/src/lib/components/CopyToClipboard.tsx b/frontend/src/lib/components/CopyToClipboard.tsx index e1525cee04b23..03480644f2d5b 100644 --- a/frontend/src/lib/components/CopyToClipboard.tsx +++ b/frontend/src/lib/components/CopyToClipboard.tsx @@ -1,12 +1,10 @@ -import { HTMLProps } from 'react' -import { copyToClipboard } from 'lib/utils' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { HTMLProps } from 'react' -interface InlineProps extends HTMLProps { - children?: JSX.Element | string - explicitValue?: string +interface InlinePropsBase extends HTMLProps { description?: string /** Makes text selectable instead of copying on click anywhere */ selectable?: boolean @@ -16,6 +14,15 @@ interface InlineProps extends HTMLProps { iconPosition?: 'end' | 'start' style?: React.CSSProperties } +interface InlinePropsWithStringInside extends InlinePropsBase { + children: string + explicitValue?: string +} +interface InlinePropsWithJSXInside extends InlinePropsBase { + children?: JSX.Element + explicitValue: string +} +type InlineProps = InlinePropsWithStringInside | InlinePropsWithJSXInside export function CopyToClipboardInline({ children, @@ -29,8 +36,7 @@ export function CopyToClipboardInline({ style, ...props }: InlineProps): JSX.Element { - const copy = async (): Promise => - await copyToClipboard(explicitValue ?? (children ? children.toString() : ''), description) + const copy = async (): Promise => await copyToClipboard((explicitValue ?? children) as string, description) const content = ( - {children} + {children && {children}} } diff --git a/frontend/src/lib/components/DateDisplay/index.tsx b/frontend/src/lib/components/DateDisplay/index.tsx index c5e35fd8944e2..55a48230e6782 100644 --- a/frontend/src/lib/components/DateDisplay/index.tsx +++ b/frontend/src/lib/components/DateDisplay/index.tsx @@ -1,6 +1,8 @@ +import './DateDisplay.scss' + import { dayjs } from 'lib/dayjs' + import { IntervalType } from '~/types' -import './DateDisplay.scss' interface DateDisplayProps { date: string diff --git a/frontend/src/lib/components/DateFilter/DateFilter.tsx b/frontend/src/lib/components/DateFilter/DateFilter.tsx index 01bb247117747..d647324c7e1ee 100644 --- a/frontend/src/lib/components/DateFilter/DateFilter.tsx +++ b/frontend/src/lib/components/DateFilter/DateFilter.tsx @@ -1,17 +1,25 @@ -import { useRef } from 'react' -import { dateMapping, dateFilterToText, uuid } from 'lib/utils' -import { DateMappingOption } from '~/types' -import { dayjs } from 'lib/dayjs' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { CUSTOM_OPTION_DESCRIPTION, CUSTOM_OPTION_KEY, CUSTOM_OPTION_VALUE, dateFilterLogic } from './dateFilterLogic' -import { RollingDateRangeFilter } from './RollingDateRangeFilter' +import { Placement } from '@floating-ui/react' +import { LemonButton, LemonButtonProps, LemonButtonWithDropdown, LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonButtonWithDropdown, LemonDivider, LemonButton, LemonButtonProps } from '@posthog/lemon-ui' +import { + CUSTOM_OPTION_DESCRIPTION, + CUSTOM_OPTION_KEY, + CUSTOM_OPTION_VALUE, + DateFilterLogicProps, + DateFilterView, +} from 'lib/components/DateFilter/types' +import { dayjs } from 'lib/dayjs' import { IconCalendar } from 'lib/lemon-ui/icons' import { LemonCalendarSelect } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' import { LemonCalendarRange } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' -import { DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' -import { Placement } from '@floating-ui/react' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { dateFilterToText, dateMapping, uuid } from 'lib/utils' +import { useRef } from 'react' + +import { DateMappingOption } from '~/types' + +import { dateFilterLogic } from './dateFilterLogic' +import { RollingDateRangeFilter } from './RollingDateRangeFilter' export interface DateFilterProps { showCustom?: boolean diff --git a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss index 99352b932cb9c..3d18b2e5b2d96 100644 --- a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss +++ b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.scss @@ -17,13 +17,13 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { - -webkit-appearance: none; + appearance: none; margin: 0; } /* Firefox */ input[type='number'] { - -moz-appearance: textfield; + appearance: textfield; } } @@ -40,22 +40,27 @@ border-radius: 0.25rem; background-color: var(--bg-light); box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; margin-right: 0.25rem; margin-left: 0.25rem; line-height: 1.25rem; align-items: center; - input { + .LemonInput { width: 3rem; - text-align: center; + min-height: 0; + padding: 0; + border: none; + + input { + text-align: center; + } } .RollingDateRangeFilter__counter__step { margin: 0 0.25rem; border-radius: var(--radius); padding: 0.25rem; + &:hover { background-color: var(--primary-highlight); } diff --git a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx index d4020f53d7292..f25ef9f06be6d 100644 --- a/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx +++ b/frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx @@ -1,11 +1,12 @@ -import { Input } from 'antd' -import { DateOption, rollingDateRangeFilterLogic } from './rollingDateRangeFilterLogic' +import './RollingDateRangeFilter.scss' + +import { LemonButton, LemonInput, LemonSelect, LemonSelectOptions } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { LemonButton, LemonSelect, LemonSelectOptions } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { dayjs } from 'lib/dayjs' -import clsx from 'clsx' -import './RollingDateRangeFilter.scss' +import { Tooltip } from 'lib/lemon-ui/Tooltip' + +import { DateOption, rollingDateRangeFilterLogic } from './rollingDateRangeFilterLogic' const dateOptions: LemonSelectOptions = [ { value: 'days', label: 'days' }, @@ -38,11 +39,6 @@ export function RollingDateRangeFilter({ useActions(rollingDateRangeFilterLogic(logicProps)) const { counter, dateOption, formattedDate } = useValues(rollingDateRangeFilterLogic(logicProps)) - const onInputChange = (event: React.ChangeEvent): void => { - const newValue = event.target.value ? parseFloat(event.target.value) : undefined - setCounter(newValue) - } - return ( - - setCounter(value)} /> setDateOption(newValue as string)} + onChange={(newValue): void => setDateOption(newValue)} onClick={(e): void => { e.stopPropagation() toggleDateOptionsSelector() diff --git a/frontend/src/lib/components/DateFilter/dateFilterLogic.test.ts b/frontend/src/lib/components/DateFilter/dateFilterLogic.test.ts index e03b8b768eb7a..26bd128545ea8 100644 --- a/frontend/src/lib/components/DateFilter/dateFilterLogic.test.ts +++ b/frontend/src/lib/components/DateFilter/dateFilterLogic.test.ts @@ -1,8 +1,9 @@ import { expectLogic } from 'kea-test-utils' +import { DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' import { dayjs } from 'lib/dayjs' import { dateMapping } from 'lib/utils' + import { dateFilterLogic } from './dateFilterLogic' -import { DateFilterView, DateFilterLogicProps } from 'lib/components/DateFilter/types' describe('dateFilterLogic', () => { let props: DateFilterLogicProps diff --git a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts index 4e8e6bf25b60b..77faff7074618 100644 --- a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts +++ b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts @@ -1,13 +1,11 @@ -import { actions, props, kea, listeners, path, reducers, selectors, key } from 'kea' -import { dayjs, Dayjs } from 'lib/dayjs' -import type { dateFilterLogicType } from './dateFilterLogicType' -import { isDate, dateFilterToText, dateStringToDayJs, formatDateRange, formatDate } from 'lib/utils' +import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { CUSTOM_OPTION_VALUE, DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' +import { Dayjs, dayjs } from 'lib/dayjs' +import { dateFilterToText, dateStringToDayJs, formatDate, formatDateRange, isDate } from 'lib/utils' + import { DateMappingOption } from '~/types' -import { DateFilterLogicProps, DateFilterView } from 'lib/components/DateFilter/types' -export const CUSTOM_OPTION_KEY = 'Custom' -export const CUSTOM_OPTION_VALUE = 'No date range override' -export const CUSTOM_OPTION_DESCRIPTION = 'Use the original date ranges of insights' +import type { dateFilterLogicType } from './dateFilterLogicType' export const dateFilterLogic = kea([ path(['lib', 'components', 'DateFilter', 'DateFilterLogic']), @@ -43,7 +41,7 @@ export const dateFilterLogic = kea([ }, ], rangeDateFrom: [ - (props.dateFrom && (dayjs.isDayjs(props.dateFrom) || isDate.test(props.dateFrom as string)) + (props.dateFrom && (dayjs.isDayjs(props.dateFrom) || isDate.test(props.dateFrom)) ? dayjs(props.dateFrom) : null) as Dayjs | null, { @@ -52,7 +50,7 @@ export const dateFilterLogic = kea([ }, ], rangeDateTo: [ - (props.dateTo && (dayjs.isDayjs(props.dateTo) || isDate.test(props.dateTo as string)) + (props.dateTo && (dayjs.isDayjs(props.dateTo) || isDate.test(props.dateTo)) ? dayjs(props.dateTo) : dayjs()) as Dayjs | null, { diff --git a/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.test.ts b/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.test.ts index f97bde9e94457..861274103fa93 100644 --- a/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.test.ts +++ b/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.test.ts @@ -1,5 +1,7 @@ import { expectLogic } from 'kea-test-utils' + import { initKeaTests } from '~/test/init' + import { rollingDateRangeFilterLogic } from './rollingDateRangeFilterLogic' describe('rollingDateRangeFilterLogic', () => { diff --git a/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.ts b/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.ts index 4600f12bc2b89..29bed83a7566f 100644 --- a/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.ts +++ b/frontend/src/lib/components/DateFilter/rollingDateRangeFilterLogic.ts @@ -1,9 +1,11 @@ -import { actions, props, kea, listeners, path, reducers, selectors } from 'kea' -import type { rollingDateRangeFilterLogicType } from './rollingDateRangeFilterLogicType' -import { Dayjs } from 'lib/dayjs' import './RollingDateRangeFilter.scss' + +import { actions, kea, listeners, path, props, reducers, selectors } from 'kea' +import { Dayjs } from 'lib/dayjs' import { dateFilterToText } from 'lib/utils' +import type { rollingDateRangeFilterLogicType } from './rollingDateRangeFilterLogicType' + const dateOptionsMap = { q: 'quarters', m: 'months', diff --git a/frontend/src/lib/components/DateFilter/types.ts b/frontend/src/lib/components/DateFilter/types.ts index 5d65aed3d4cfb..d3563cbbad0bf 100644 --- a/frontend/src/lib/components/DateFilter/types.ts +++ b/frontend/src/lib/components/DateFilter/types.ts @@ -1,4 +1,5 @@ import { Dayjs } from 'lib/dayjs' + import { DateMappingOption } from '~/types' export enum DateFilterView { @@ -15,3 +16,7 @@ export type DateFilterLogicProps = { dateOptions?: DateMappingOption[] isDateFormatted?: boolean } + +export const CUSTOM_OPTION_KEY = 'Custom' +export const CUSTOM_OPTION_VALUE = 'No date range override' +export const CUSTOM_OPTION_DESCRIPTION = 'Use the original date ranges of insights' diff --git a/frontend/src/lib/components/DatePicker.scss b/frontend/src/lib/components/DatePicker.scss index 0ac7693630d02..21ec676273bc1 100644 --- a/frontend/src/lib/components/DatePicker.scss +++ b/frontend/src/lib/components/DatePicker.scss @@ -7,25 +7,34 @@ color: var(--default); background: var(--secondary-3000); border-color: transparent; + .ant-picker-suffix { color: var(--default); } } + .ant-picker-panel-container { color: var(--default); background: var(--bg-3000); border: 1px solid 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-3000); } + .ant-picker-cell:hover:not(.ant-picker-cell-in-view) .ant-picker-cell-inner, - .ant-picker-cell:hover:not(.ant-picker-cell-selected):not(.ant-picker-cell-range-start):not( - .ant-picker-cell-range-end - ):not(.ant-picker-cell-range-hover-start):not(.ant-picker-cell-range-hover-end) + .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-time-panel-column > li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner:hover { background: var(--secondary-3000); diff --git a/frontend/src/lib/components/DatePicker.tsx b/frontend/src/lib/components/DatePicker.tsx index dfe194d66ab78..42ce83e78640a 100644 --- a/frontend/src/lib/components/DatePicker.tsx +++ b/frontend/src/lib/components/DatePicker.tsx @@ -1,6 +1,7 @@ -import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs' -import generatePicker from 'antd/lib/date-picker/generatePicker' import './DatePicker.scss' + +import generatePicker from 'antd/lib/date-picker/generatePicker' import { dayjs } from 'lib/dayjs' +import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs' export const DatePicker = generatePicker(dayjsGenerateConfig) diff --git a/frontend/src/lib/components/DebugNotice.tsx b/frontend/src/lib/components/DebugNotice.tsx index 11d70ba34054d..0f6f31011e404 100644 --- a/frontend/src/lib/components/DebugNotice.tsx +++ b/frontend/src/lib/components/DebugNotice.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react' import { IconClose } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useEffect, useState } from 'react' export function DebugNotice(): JSX.Element | null { const [debugInfo, setDebugInfo] = useState<{ branch: string; revision: string } | undefined>() @@ -29,7 +29,7 @@ export function DebugNotice(): JSX.Element | null { return (
    setNoticeHidden(true)}> -
    +
    DEBUG mode -
    {propertyType}
    -
    - ) : ( - <> - ) -} - -function OwnerDropdown(): JSX.Element { - const { members } = useValues(membersLogic) - const { localDefinition } = useValues(definitionPopoverLogic) - const { setLocalDefinition } = useActions(definitionPopoverLogic) - - return ( - - ) -} - export const DefinitionPopover = { Wrapper, Header, @@ -289,6 +248,4 @@ export const DefinitionPopover = { Grid, Section, Card, - OwnerDropdown, - Type, } diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx index cb4af914b655b..7193044205f95 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx @@ -1,26 +1,28 @@ +import { hide } from '@floating-ui/react' +import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { ActionPopoverInfo } from 'lib/components/DefinitionPopover/ActionPopoverInfo' +import { CohortPopoverInfo } from 'lib/components/DefinitionPopover/CohortPopoverInfo' +import { DefinitionPopover } from 'lib/components/DefinitionPopover/DefinitionPopover' +import { definitionPopoverLogic, DefinitionPopoverState } from 'lib/components/DefinitionPopover/definitionPopoverLogic' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SimpleOption, TaxonomicDefinitionTypes, TaxonomicFilterGroup, TaxonomicFilterGroupType, } from 'lib/components/TaxonomicFilter/types' -import { useActions, useValues } from 'kea' -import { definitionPopoverLogic, DefinitionPopoverState } from 'lib/components/DefinitionPopover/definitionPopoverLogic' -import { useEffect } from 'react' -import { isPostHogProp, KEY_MAPPING } from 'lib/taxonomy' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { DefinitionPopover } from 'lib/components/DefinitionPopover/DefinitionPopover' -import { Link } from 'lib/lemon-ui/Link' import { IconInfo, IconLock, IconOpenInNew } from 'lib/lemon-ui/icons' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { ActionType, CohortType, EventDefinition, PropertyDefinition } from '~/types' -import { ActionPopoverInfo } from 'lib/components/DefinitionPopover/ActionPopoverInfo' -import { CohortPopoverInfo } from 'lib/components/DefinitionPopover/CohortPopoverInfo' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { Link } from 'lib/lemon-ui/Link' import { Popover } from 'lib/lemon-ui/Popover' -import { hide } from '@floating-ui/react' -import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { isPostHogProp, KEY_MAPPING } from 'lib/taxonomy' +import { useEffect } from 'react' + +import { ActionType, CohortType, EventDefinition, PropertyDefinition } from '~/types' + import { TZLabel } from '../TZLabel' function TaxonomyIntroductionSection(): JSX.Element { diff --git a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts index 31347d3342cd0..1db1f33132221 100644 --- a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts +++ b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts @@ -1,5 +1,14 @@ -import { definitionPopoverLogic, DefinitionPopoverState } from 'lib/components/DefinitionPopover/definitionPopoverLogic' +import { expectLogic } from 'kea-test-utils' import api from 'lib/api' +import { definitionPopoverLogic, DefinitionPopoverState } from 'lib/components/DefinitionPopover/definitionPopoverLogic' +import { TaxonomicDefinitionTypes, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { urls } from 'scenes/urls' + +import { useMocks } from '~/mocks/jest' +import { actionsModel } from '~/models/actionsModel' +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { initKeaTests } from '~/test/init' import { mockActionDefinition, mockCohort, @@ -9,15 +18,7 @@ import { mockGroup, mockPersonProperty, } from '~/test/mocks' -import { initKeaTests } from '~/test/init' -import { TaxonomicDefinitionTypes, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { expectLogic } from 'kea-test-utils' -import { urls } from 'scenes/urls' -import { actionsModel } from '~/models/actionsModel' import { ActionType, CohortType, PersonProperty, PropertyDefinition } from '~/types' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { cohortsModel } from '~/models/cohortsModel' -import { useMocks } from '~/mocks/jest' describe('definitionPopoverLogic', () => { let logic: ReturnType @@ -65,8 +66,8 @@ describe('definitionPopoverLogic', () => { it('make local state dirty', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) }) .toDispatchActions(['setDefinition', 'setPopoverState']) .toMatchValues({ @@ -87,9 +88,9 @@ describe('definitionPopoverLogic', () => { it('cancel', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ description: 'new description' }) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ description: 'new description' }) }) .toDispatchActions(['setLocalDefinition']) .toMatchValues({ @@ -159,7 +160,7 @@ describe('definitionPopoverLogic', () => { }, { type: TaxonomicFilterGroupType.Cohorts, - definition: mockCohort as CohortType, + definition: mockCohort, url: `api/projects/@current/cohorts/${mockCohort.id}`, dispatchActions: [cohortsModel, ['updateCohort']], }, @@ -178,10 +179,10 @@ describe('definitionPopoverLogic', () => { logic.mount() const expectChain = expectLogic(logic, async () => { - await logic.actions.setDefinition(group.definition) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ description: 'new and improved description' }) - await logic.actions.handleSave({}) + logic.actions.setDefinition(group.definition) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ description: 'new and improved description' }) + logic.actions.handleSave({}) }).toDispatchActions(['setDefinitionSuccess', 'setPopoverState', 'handleSave']) if (group.dispatchActions.length > 0) { @@ -202,9 +203,9 @@ describe('definitionPopoverLogic', () => { it('add tags', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ tags: ['ohhello', 'ohwow'] }) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ tags: ['ohhello', 'ohwow'] }) }) .toDispatchActions(['setDefinitionSuccess', 'setLocalDefinition']) .toMatchValues({ @@ -221,8 +222,8 @@ describe('definitionPopoverLogic', () => { logic.mount() await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setDefinition(mockEventDefinitions[1]) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setDefinition(mockEventDefinitions[1]) }) .toDispatchActions(['setDefinitionSuccess']) .toMatchValues({ diff --git a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.ts b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.ts index 931d1b00114d6..38f907c0abbee 100644 --- a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.ts +++ b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.ts @@ -1,19 +1,21 @@ +import equal from 'fast-deep-equal' +import { actions, connect, events, kea, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import type { definitionPopoverLogicType } from './definitionPopoverLogicType' +import api from 'lib/api' +import { getSingularType } from 'lib/components/DefinitionPopover/utils' import { TaxonomicDefinitionTypes, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { capitalizeFirstLetter } from 'lib/utils' -import { getSingularType } from 'lib/components/DefinitionPopover/utils' -import { ActionType, AvailableFeature, CohortType, EventDefinition, PropertyDefinition } from '~/types' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { urls } from 'scenes/urls' -import api from 'lib/api' +import { userLogic } from 'scenes/userLogic' + import { actionsModel } from '~/models/actionsModel' -import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' import { cohortsModel } from '~/models/cohortsModel' -import equal from 'fast-deep-equal' -import { userLogic } from 'scenes/userLogic' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' +import { ActionType, AvailableFeature, CohortType, EventDefinition, PropertyDefinition } from '~/types' + +import type { definitionPopoverLogicType } from './definitionPopoverLogicType' const IS_TEST_MODE = process.env.NODE_ENV === 'test' diff --git a/frontend/src/lib/components/DefinitionPopover/utils.ts b/frontend/src/lib/components/DefinitionPopover/utils.ts index 1a7ec529cc0f3..f828aea981fc2 100644 --- a/frontend/src/lib/components/DefinitionPopover/utils.ts +++ b/frontend/src/lib/components/DefinitionPopover/utils.ts @@ -1,7 +1,8 @@ -import { AnyPropertyFilter, PropertyFilterValue, PropertyOperator } from '~/types' -import { allOperatorsMapping, genericOperatorMap } from 'lib/utils' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { isPropertyFilterWithOperator } from 'lib/components/PropertyFilters/utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { allOperatorsMapping, genericOperatorMap } from 'lib/utils' + +import { AnyPropertyFilter, PropertyFilterValue, PropertyOperator } from '~/types' export function operatorToHumanName(operator?: string): string { if (operator === 'gte') { diff --git a/frontend/src/lib/components/Drawer.tsx b/frontend/src/lib/components/Drawer.tsx index 2275e52121054..bc20fba4bff30 100644 --- a/frontend/src/lib/components/Drawer.tsx +++ b/frontend/src/lib/components/Drawer.tsx @@ -1,6 +1,6 @@ -import { PropsWithChildren } from 'react' import { Drawer as AntDrawer } from 'antd' import { DrawerProps } from 'antd/lib/drawer' +import { PropsWithChildren } from 'react' export function Drawer(props: PropsWithChildren): JSX.Element { return diff --git a/frontend/src/lib/components/DropdownSelector/DropdownSelector.scss b/frontend/src/lib/components/DropdownSelector/DropdownSelector.scss deleted file mode 100644 index b8ffa8d145235..0000000000000 --- a/frontend/src/lib/components/DropdownSelector/DropdownSelector.scss +++ /dev/null @@ -1,25 +0,0 @@ -.dropdown-selector { - padding: 0.5rem; - border: 1px solid var(--border-light); - border-radius: var(--radius); - display: flex; - align-items: center; - cursor: pointer; - - &.disabled { - color: var(--muted); - cursor: not-allowed; - } - - &.compact { - padding: 0.333rem 0.5rem; - } - - .dropdown-arrow { - display: flex; - align-items: center; - padding-left: 4px; - font-size: 1.2em; - color: var(--muted-alt); - } -} diff --git a/frontend/src/lib/components/DropdownSelector/DropdownSelector.tsx b/frontend/src/lib/components/DropdownSelector/DropdownSelector.tsx deleted file mode 100644 index abe0e061c8827..0000000000000 --- a/frontend/src/lib/components/DropdownSelector/DropdownSelector.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* Custom dropdown selector with an icon a help caption */ -import { Dropdown, Menu } from 'antd' -import clsx from 'clsx' -import { IconArrowDropDown } from 'lib/lemon-ui/icons' -import './DropdownSelector.scss' - -interface DropdownSelectorProps { - label?: string - value: string | null - onValueChange: (value: string) => void - options: DropdownOption[] - hideDescriptionOnDisplay?: boolean // Hides the description support text on the main display component (i.e. only shown in the dropdown menu) - disabled?: boolean - compact?: boolean -} - -interface DropdownOption { - key: string - label: string - description?: string - icon: JSX.Element - hidden?: boolean -} - -interface SelectItemInterface { - icon: JSX.Element - label: string - description?: string - onClick: () => void -} - -function SelectItem({ icon, label, description, onClick }: SelectItemInterface): JSX.Element { - return ( -
    -
    - {icon} -
    {label}
    -
    - {description &&
    {description}
    } -
    - ) -} - -export function DropdownSelector({ - label, - value, - onValueChange, - options, - hideDescriptionOnDisplay, - disabled, - compact, -}: DropdownSelectorProps): JSX.Element { - const selectedOption = options.find((opt) => opt.key === value) - - const menu = ( - - {options.map(({ key, hidden, ...props }) => { - if (hidden) { - return null - } - return ( - - onValueChange(key)} /> - - ) - })} - - ) - - return ( - <> - {label && } - -
    e.preventDefault()} - > -
    - {selectedOption && ( - {}} - description={hideDescriptionOnDisplay ? undefined : selectedOption.description} - /> - )} -
    -
    - -
    -
    -
    - - ) -} diff --git a/frontend/src/lib/components/DurationPicker/DurationPicker.tsx b/frontend/src/lib/components/DurationPicker/DurationPicker.tsx index bf4829b86bb4e..234cce3fe91c4 100644 --- a/frontend/src/lib/components/DurationPicker/DurationPicker.tsx +++ b/frontend/src/lib/components/DurationPicker/DurationPicker.tsx @@ -1,6 +1,7 @@ +import { LemonInput, LemonSelect } from '@posthog/lemon-ui' import { useEffect, useState } from 'react' + import { Duration, SmallTimeUnit } from '~/types' -import { LemonSelect, LemonInput } from '@posthog/lemon-ui' interface DurationPickerProps { onChange: (value_seconds: number) => void diff --git a/frontend/src/lib/components/EditableField/EditableField.scss b/frontend/src/lib/components/EditableField/EditableField.scss index e853c9c625741..3bd0610a9ff90 100644 --- a/frontend/src/lib/components/EditableField/EditableField.scss +++ b/frontend/src/lib/components/EditableField/EditableField.scss @@ -2,30 +2,34 @@ display: flex; align-items: center; max-width: 100%; + &:not(.EditableField--multiline) { - line-height: 2rem; + line-height: 1.15em; } + i { color: var(--muted); } + .EditableField__notice { font-size: 1.5rem; color: var(--muted); margin-left: 0.5rem; } + .EditableField__highlight { display: flex; flex-direction: row; align-items: center; width: fit-content; max-width: calc(100% + 0.5rem); - min-height: 2rem; padding: 0.25rem; // Some padding to give the focus outline more breathing space margin: -0.25rem; white-space: pre-wrap; overflow: auto; } - &--editing .EditableField__highlight { + + &.EditableField--editing .EditableField__highlight { flex-grow: 1; align-items: flex-end; width: auto; @@ -33,10 +37,29 @@ outline: 1px solid var(--border); border-radius: var(--radius); } + + &.EditableField--underlined { + .EditableField__highlight { + padding: 0; + margin: 0; + } + + &.EditableField--editing .EditableField__highlight { + outline: none; + + input { + text-decoration: underline; + text-decoration-color: var(--muted); + text-underline-offset: 0.5em; + } + } + } + .EditableField__autosize { align-self: center; min-width: 0; } + .EditableField__autosize__sizer { position: absolute; top: 0; @@ -46,6 +69,7 @@ overflow: scroll; white-space: pre; } + .EditableField__actions { flex-shrink: 0; display: flex; @@ -53,6 +77,7 @@ gap: 0.5rem; margin-left: 0.5rem; } + input, textarea { max-width: 100%; @@ -63,6 +88,7 @@ outline: none; padding: 0; } + textarea { align-self: center; width: 100%; diff --git a/frontend/src/lib/components/EditableField/EditableField.stories.tsx b/frontend/src/lib/components/EditableField/EditableField.stories.tsx index 50b2d02b1cba8..47452fb1f9584 100644 --- a/frontend/src/lib/components/EditableField/EditableField.stories.tsx +++ b/frontend/src/lib/components/EditableField/EditableField.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryFn } from '@storybook/react' +import { useState } from 'react' import { EditableField as EditableFieldComponent } from './EditableField' -import { useState } from 'react' const meta: Meta = { title: 'Components/Editable Field', diff --git a/frontend/src/lib/components/EditableField/EditableField.tsx b/frontend/src/lib/components/EditableField/EditableField.tsx index 40e61b3e57d08..369af42b55c30 100644 --- a/frontend/src/lib/components/EditableField/EditableField.tsx +++ b/frontend/src/lib/components/EditableField/EditableField.tsx @@ -1,12 +1,13 @@ -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import './EditableField.scss' + +import clsx from 'clsx' import { IconEdit, IconMarkdown } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import TextareaAutosize from 'react-textarea-autosize' -import clsx from 'clsx' -import { pluralize } from 'lib/utils' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { pluralize } from 'lib/utils' +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import TextareaAutosize from 'react-textarea-autosize' export interface EditableFieldProps { /** What this field stands for. */ @@ -22,11 +23,14 @@ export interface EditableFieldProps { multiline?: boolean /** Whether to render the content as Markdown in view mode. */ markdown?: boolean - compactButtons?: boolean + compactButtons?: boolean | 'xsmall' // The 'xsmall' is somewhat hacky, but necessary for 3000 breadcrumbs /** Whether this field should be gated behind a "paywall". */ paywall?: boolean /** Controlled mode. */ mode?: 'view' | 'edit' + onModeToggle?: (newMode: 'view' | 'edit') => void + /** @default 'outlined' */ + editingIndication?: 'outlined' | 'underlined' className?: string style?: React.CSSProperties 'data-attr'?: string @@ -53,6 +57,8 @@ export function EditableField({ compactButtons = false, paywall = false, mode, + onModeToggle, + editingIndication = 'outlined', className, style, 'data-attr': dataAttr, @@ -60,13 +66,16 @@ export function EditableField({ notice, }: EditableFieldProps): JSX.Element { const [localIsEditing, setLocalIsEditing] = useState(false) - const [tentativeValue, setTentativeValue] = useState(value) + const [localTentativeValue, setLocalTentativeValue] = useState(value) useEffect(() => { - setTentativeValue(value) + setLocalTentativeValue(value) }, [value]) + useEffect(() => { + setLocalIsEditing(mode === 'edit') + }, [mode]) - const isSaveable = !minLength || tentativeValue.length >= minLength + const isSaveable = !minLength || localTentativeValue.length >= minLength const mouseDownOnCancelButton = (e: React.MouseEvent): void => { // if saveOnBlur is set the onBlur handler of the input fires before the onClick event of the button @@ -76,12 +85,14 @@ export function EditableField({ const cancel = (): void => { setLocalIsEditing(false) - setTentativeValue(value) + setLocalTentativeValue(value) + onModeToggle?.('view') } const save = (): void => { - onSave?.(tentativeValue) + onSave?.(localTentativeValue) setLocalIsEditing(false) + onModeToggle?.('view') } const isEditing = !paywall && (mode === 'edit' || localIsEditing) @@ -107,6 +118,7 @@ export function EditableField({ 'EditableField', multiline && 'EditableField--multiline', isEditing && 'EditableField--editing', + editingIndication === 'underlined' && 'EditableField--underlined', className )} data-attr={dataAttr} @@ -127,12 +139,12 @@ export function EditableField({ {multiline ? ( { onChange?.(e.target.value) - setTentativeValue(e.target.value) + setLocalTentativeValue(e.target.value) }} - onBlur={saveOnBlur ? (tentativeValue !== value ? save : cancel) : undefined} + onBlur={saveOnBlur ? (localTentativeValue !== value ? save : cancel) : undefined} onKeyDown={handleKeyDown} placeholder={placeholder} minLength={minLength} @@ -142,12 +154,12 @@ export function EditableField({ ) : ( { onChange?.(e.target.value) - setTentativeValue(e.target.value) + setLocalTentativeValue(e.target.value) }} - onBlur={saveOnBlur ? (tentativeValue !== value ? save : cancel) : undefined} + onBlur={saveOnBlur ? (localTentativeValue !== value ? save : cancel) : undefined} onKeyDown={handleKeyDown} placeholder={placeholder} minLength={minLength} @@ -155,7 +167,7 @@ export function EditableField({ autoFocus={autoFocus} /> )} - {!mode && ( + {(!mode || !!onModeToggle) && (
    {markdown && ( @@ -164,7 +176,7 @@ export function EditableField({ )} ) : ( <> - {tentativeValue && markdown ? ( - {tentativeValue} + {localTentativeValue && markdown ? ( + {localTentativeValue} ) : ( - tentativeValue || {placeholder} + localTentativeValue || {placeholder} )} - {!mode && ( + {(!mode || !!onModeToggle) && (
    } - size={compactButtons ? 'small' : undefined} - onClick={() => setLocalIsEditing(true)} + size={ + typeof compactButtons === 'string' + ? compactButtons + : compactButtons + ? 'small' + : undefined + } + onClick={() => { + setLocalIsEditing(true) + onModeToggle?.('edit') + }} data-attr={`edit-prop-${name}`} disabled={paywall} noPadding diff --git a/frontend/src/lib/components/EmptyMessage/EmptyMessage.scss b/frontend/src/lib/components/EmptyMessage/EmptyMessage.scss index a338a5a94800c..beebb4188566a 100644 --- a/frontend/src/lib/components/EmptyMessage/EmptyMessage.scss +++ b/frontend/src/lib/components/EmptyMessage/EmptyMessage.scss @@ -8,6 +8,7 @@ .title { text-align: center; } + .description { text-align: center; } diff --git a/frontend/src/lib/components/EmptyMessage/EmptyMessage.tsx b/frontend/src/lib/components/EmptyMessage/EmptyMessage.tsx index 5635871c84a77..c60ae07df27b7 100644 --- a/frontend/src/lib/components/EmptyMessage/EmptyMessage.tsx +++ b/frontend/src/lib/components/EmptyMessage/EmptyMessage.tsx @@ -1,4 +1,5 @@ import './EmptyMessage.scss' + import { LemonButton } from 'lib/lemon-ui/LemonButton' export interface EmptyMessageProps { diff --git a/frontend/src/lib/components/EntityFilterInfo.tsx b/frontend/src/lib/components/EntityFilterInfo.tsx index e1e628b61951d..4c3753a461132 100644 --- a/frontend/src/lib/components/EntityFilterInfo.tsx +++ b/frontend/src/lib/components/EntityFilterInfo.tsx @@ -1,7 +1,8 @@ -import { ActionFilter, EntityFilter } from '~/types' +import clsx from 'clsx' import { getKeyMapping } from 'lib/taxonomy' import { getDisplayNameFromEntityFilter, isAllEventsEntityFilter } from 'scenes/insights/utils' -import clsx from 'clsx' + +import { ActionFilter, EntityFilter } from '~/types' interface EntityFilterInfoProps { filter: EntityFilter | ActionFilter diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx index bd8ffc5935d81..86915996ae5f6 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx @@ -1,5 +1,6 @@ import { Meta } from '@storybook/react' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' + import { EventType, RecordingEventType } from '~/types' const meta: Meta = { diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.tsx index 4c14a6e44412a..9b8c461090f42 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.tsx @@ -1,10 +1,11 @@ -import { EventType, RecordingEventType } from '~/types' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { IconFlag } from 'lib/lemon-ui/icons' import clsx from 'clsx' +import { IconFlag } from 'lib/lemon-ui/icons' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Link } from 'lib/lemon-ui/Link' import posthog from 'posthog-js' +import { EventType, RecordingEventType } from '~/types' + interface StackFrame { filename: string lineno: number diff --git a/frontend/src/lib/components/EventSelect/EventSelect.stories.tsx b/frontend/src/lib/components/EventSelect/EventSelect.stories.tsx index b13517b039dd5..767397adbf7eb 100644 --- a/frontend/src/lib/components/EventSelect/EventSelect.stories.tsx +++ b/frontend/src/lib/components/EventSelect/EventSelect.stories.tsx @@ -1,6 +1,8 @@ import { Meta } from '@storybook/react' import { useState } from 'react' + import { mswDecorator } from '~/mocks/browser' + import { EventSelect } from './EventSelect' const eventDefinitions = [ diff --git a/frontend/src/lib/components/EventSelect/EventSelect.tsx b/frontend/src/lib/components/EventSelect/EventSelect.tsx index fa988eee99b84..fe4db309d44bc 100644 --- a/frontend/src/lib/components/EventSelect/EventSelect.tsx +++ b/frontend/src/lib/components/EventSelect/EventSelect.tsx @@ -1,7 +1,7 @@ -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' -import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' +import { Popover } from 'lib/lemon-ui/Popover/Popover' import React, { useState } from 'react' interface EventSelectProps { diff --git a/frontend/src/lib/components/ExportButton/ExportButton.tsx b/frontend/src/lib/components/ExportButton/ExportButton.tsx index b4d6e7e052452..6f2b40f8d0628 100644 --- a/frontend/src/lib/components/ExportButton/ExportButton.tsx +++ b/frontend/src/lib/components/ExportButton/ExportButton.tsx @@ -1,6 +1,8 @@ -import { ExporterFormat, OnlineExportContext } from '~/types' import { LemonButton, LemonButtonProps, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + +import { ExporterFormat, OnlineExportContext } from '~/types' + import { triggerExport, TriggerExportProps } from './exporter' export interface ExportButtonItem { @@ -51,7 +53,7 @@ export function ExportButton({ items, ...buttonProps }: ExportButtonProps): JSX. key={i} fullWidth status="stealth" - onClick={() => triggerExport(triggerExportProps)} + onClick={() => void triggerExport(triggerExportProps)} data-attr={`export-button-${exportFormatExtension}`} data-ph-capture-attribute-export-target={target} data-ph-capture-attribute-export-body={ diff --git a/frontend/src/lib/components/ExportButton/exporter.tsx b/frontend/src/lib/components/ExportButton/exporter.tsx index 84ebf2a5eecb9..97cff3343e00c 100644 --- a/frontend/src/lib/components/ExportButton/exporter.tsx +++ b/frontend/src/lib/components/ExportButton/exporter.tsx @@ -1,13 +1,14 @@ +import { AnimationType } from 'lib/animations/animations' import api from 'lib/api' +import { Animation } from 'lib/components/Animation/Animation' +import { dayjs } from 'lib/dayjs' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { delay } from 'lib/utils' import posthog from 'posthog-js' -import { ExportContext, ExportedAssetType, ExporterFormat, LocalExportContext } from '~/types' -import { lemonToast } from 'lib/lemon-ui/lemonToast' import { useEffect, useState } from 'react' -import { AnimationType } from 'lib/animations/animations' -import { Animation } from 'lib/components/Animation/Animation' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { dayjs } from 'lib/dayjs' + +import { ExportContext, ExportedAssetType, ExporterFormat, LocalExportContext } from '~/types' const POLL_DELAY_MS = 1000 const MAX_PNG_POLL = 10 @@ -48,8 +49,8 @@ export async function triggerExport(asset: TriggerExportProps): Promise { lemonToast.error('Export failed!') } } else { - // eslint-disable-next-line no-async-promise-executor - const poller = new Promise(async (resolve, reject) => { + // eslint-disable-next-line no-async-promise-executor,@typescript-eslint/no-misused-promises + const poller = new Promise(async (resolve, reject) => { const trackingProperties = { export_format: asset.export_format, dashboard: asset.dashboard, diff --git a/frontend/src/lib/components/Fade/Fade.scss b/frontend/src/lib/components/Fade/Fade.scss index ae50db416ae05..3a37304c0f8e1 100644 --- a/frontend/src/lib/components/Fade/Fade.scss +++ b/frontend/src/lib/components/Fade/Fade.scss @@ -1,16 +1,18 @@ -@keyframes fadeComponentFadeIn { +@keyframes Fade__fade-in { 0% { opacity: 0; } + 100% { opacity: 1; } } -@keyframes fadeComponentFadeOut { +@keyframes Fade__fade-out { 0% { opacity: 1; } + 100% { opacity: 0; } diff --git a/frontend/src/lib/components/Fade/Fade.tsx b/frontend/src/lib/components/Fade/Fade.tsx index d611ead1591a0..2ac4faad950c7 100644 --- a/frontend/src/lib/components/Fade/Fade.tsx +++ b/frontend/src/lib/components/Fade/Fade.tsx @@ -1,4 +1,5 @@ import './Fade.scss' + import { useEffect, useState } from 'react' export function Fade({ @@ -31,7 +32,7 @@ export function Fade({
    diff --git a/frontend/src/lib/components/FilterPropertyLink.tsx b/frontend/src/lib/components/FilterPropertyLink.tsx index 7db8edfe9ef76..a6253218953e6 100644 --- a/frontend/src/lib/components/FilterPropertyLink.tsx +++ b/frontend/src/lib/components/FilterPropertyLink.tsx @@ -1,9 +1,9 @@ import { combineUrl } from 'kea-router' - import { Property } from 'lib/components/Property' +import { parseProperties } from 'lib/components/PropertyFilters/utils' import { Link } from 'lib/lemon-ui/Link' + import { FilterType } from '~/types' -import { parseProperties } from 'lib/components/PropertyFilters/utils' export function FilterPropertyLink({ property, diff --git a/frontend/src/lib/components/FlagSelector.tsx b/frontend/src/lib/components/FlagSelector.tsx index 2c14b90d98b03..15c5dfb02f069 100644 --- a/frontend/src/lib/components/FlagSelector.tsx +++ b/frontend/src/lib/components/FlagSelector.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' import { useValues } from 'kea' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' -import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps } from 'lib/components/TaxonomicFilter/types' -import { Popover } from 'lib/lemon-ui/Popover' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps } from 'lib/components/TaxonomicFilter/types' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Popover } from 'lib/lemon-ui/Popover' +import { useState } from 'react' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' interface FlagSelectorProps { value: number | undefined diff --git a/frontend/src/lib/components/FullScreen.tsx b/frontend/src/lib/components/FullScreen.tsx index 2ca3ec95706e0..95859d4efe88e 100644 --- a/frontend/src/lib/components/FullScreen.tsx +++ b/frontend/src/lib/components/FullScreen.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' export function FullScreen({ onExit }: { onExit?: () => any }): null { const selector = 'aside.ant-layout-sider, .layout-top-content' useEffect(() => { - const myClasses = window.document.querySelectorAll(selector) as NodeListOf + const myClasses = window.document.querySelectorAll(selector) for (let i = 0; i < myClasses.length; i++) { myClasses[i].style.display = 'none' @@ -16,7 +16,7 @@ export function FullScreen({ onExit }: { onExit?: () => any }): null { } try { - window.document.body.requestFullscreen().then(() => { + void document.body.requestFullscreen().then(() => { window.addEventListener('fullscreenchange', handler, false) }) } catch { @@ -31,15 +31,15 @@ export function FullScreen({ onExit }: { onExit?: () => any }): null { } return () => { - const elements = window.document.querySelectorAll(selector) as NodeListOf + const elements = window.document.querySelectorAll(selector) for (let i = 0; i < elements.length; i++) { elements[i].style.display = 'block' } try { window.removeEventListener('fullscreenchange', handler, false) - if (window.document.fullscreenElement !== null) { - window.document.exitFullscreen() + if (document.fullscreenElement !== null) { + void document.exitFullscreen() } } catch { // will break on IE11 diff --git a/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories.tsx b/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories.tsx index 7a234ce903e49..451f5d7a84c10 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories.tsx +++ b/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories.tsx @@ -1,5 +1,7 @@ import { Meta } from '@storybook/react' + import { ElementType } from '~/types' + import { HTMLElementsDisplay } from './HTMLElementsDisplay' const meta: Meta = { diff --git a/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.tsx b/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.tsx index efb03c2927016..73f5a6e0d0544 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.tsx +++ b/frontend/src/lib/components/HTMLElementsDisplay/HTMLElementsDisplay.tsx @@ -1,12 +1,14 @@ -import { ElementType } from '~/types' -import { SelectableElement } from './SelectableElement' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { htmlElementsDisplayLogic } from 'lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic' import { useActions, useValues } from 'kea' -import { useState } from 'react' import { CodeSnippet } from 'lib/components/CodeSnippet' +import { htmlElementsDisplayLogic } from 'lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic' import { ParsedCSSSelector } from 'lib/components/HTMLElementsDisplay/preselectWithCSS' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { useState } from 'react' + +import { ElementType } from '~/types' + import { Fade } from '../Fade/Fade' +import { SelectableElement } from './SelectableElement' function indent(level: number): string { return Array(level).fill(' ').join('') diff --git a/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.scss b/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.scss index 8221fb35faf4d..c0264a96288f6 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.scss +++ b/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.scss @@ -4,12 +4,14 @@ border-radius: 4px; &.SelectableElement--selected { - background: var(--primary); + background: var(--primary-3000); } + &:hover { - background: var(--primary-light); + background: var(--primary-3000-hover); } + &:active { - background: var(--primary-dark); + background: var(--primary-3000-active); } } diff --git a/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.tsx b/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.tsx index 37aa45c5f5710..805a02a1042f1 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.tsx +++ b/frontend/src/lib/components/HTMLElementsDisplay/SelectableElement.tsx @@ -1,9 +1,11 @@ -import { ElementType } from '~/types' -import clsx from 'clsx' import './SelectableElement.scss' + +import clsx from 'clsx' import { ParsedCSSSelector } from 'lib/components/HTMLElementsDisplay/preselectWithCSS' import { objectsEqual } from 'lib/utils' +import { ElementType } from '~/types' + export function TagPart({ tagName, selectedParts, diff --git a/frontend/src/lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic.ts b/frontend/src/lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic.ts index 12d73a9792a86..80b8d769ff2a5 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic.ts +++ b/frontend/src/lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic.ts @@ -1,14 +1,15 @@ import { actions, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' - -import type { htmlElementsDisplayLogicType } from './htmlElementsDisplayLogicType' -import { ElementType } from '~/types' -import { objectsEqual } from 'lib/utils' +import { subscriptions } from 'kea-subscriptions' import { ParsedCSSSelector, parsedSelectorToSelectorString, preselect, } from 'lib/components/HTMLElementsDisplay/preselectWithCSS' -import { subscriptions } from 'kea-subscriptions' +import { objectsEqual } from 'lib/utils' + +import { ElementType } from '~/types' + +import type { htmlElementsDisplayLogicType } from './htmlElementsDisplayLogicType' export interface HtmlElementDisplayLogicProps { checkUniqueness: boolean diff --git a/frontend/src/lib/components/HTMLElementsDisplay/preselectWithCSS.test.ts b/frontend/src/lib/components/HTMLElementsDisplay/preselectWithCSS.test.ts index 03fffb3b47f33..31fc2adb144b5 100644 --- a/frontend/src/lib/components/HTMLElementsDisplay/preselectWithCSS.test.ts +++ b/frontend/src/lib/components/HTMLElementsDisplay/preselectWithCSS.test.ts @@ -1,12 +1,13 @@ -import { ElementType } from '~/types' +import { EXAMPLE_ELEMENTS } from 'lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories' +import { elementsChain } from 'lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic' import { - parseCSSSelector, matchesSelector, - preselect, + parseCSSSelector, parsedSelectorToSelectorString, + preselect, } from 'lib/components/HTMLElementsDisplay/preselectWithCSS' -import { EXAMPLE_ELEMENTS } from 'lib/components/HTMLElementsDisplay/HTMLElementsDisplay.stories' -import { elementsChain } from 'lib/components/HTMLElementsDisplay/htmlElementsDisplayLogic' + +import { ElementType } from '~/types' const elements = [ { diff --git a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.scss b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.scss index 8c9452943f1f3..a16ea6a598aae 100644 --- a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.scss +++ b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.scss @@ -4,7 +4,7 @@ cursor: pointer; margin: 0; - &:after { + &::after { // Hack for preloading images position: absolute; width: 0; diff --git a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.stories.tsx b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.stories.tsx index 63e648a6b07f2..a8b5efdd4cfc8 100644 --- a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.stories.tsx +++ b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.stories.tsx @@ -1,12 +1,11 @@ import { Meta, StoryFn } from '@storybook/react' + import { HedgehogBuddy } from './HedgehogBuddy' const meta: Meta = { title: 'Components/Hedgehog Buddy', component: HedgehogBuddy, - parameters: { - testOptions: { skip: true }, // Hedgehogs aren't particularly snapshotable - }, + tags: ['test-skip'], // Hedgehogs aren't particularly snapshotable } export default meta diff --git a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx index c777e1d06c0a9..d4974f649824a 100644 --- a/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx +++ b/frontend/src/lib/components/HedgehogBuddy/HedgehogBuddy.tsx @@ -1,27 +1,29 @@ -import { MutableRefObject, useEffect, useRef, useState } from 'react' +import './HedgehogBuddy.scss' -import { capitalizeFirstLetter, range, sampleOne } from 'lib/utils' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { useActions, useValues } from 'kea' -import { hedgehogbuddyLogic } from './hedgehogbuddyLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { capitalizeFirstLetter, range, sampleOne } from 'lib/utils' +import { MutableRefObject, useEffect, useRef, useState } from 'react' + +import { themeLogic } from '~/layout/navigation-3000/themeLogic' + +import { FlaggedFeature } from '../FlaggedFeature' +import { HedgehogBuddyAccessory } from './components/AccessoryButton' +import { hedgehogbuddyLogic } from './hedgehogbuddyLogic' import { + accessoryGroups, + AccessoryInfo, + baseSpriteAccessoriesPath, + baseSpritePath, SHADOW_HEIGHT, SPRITE_SHEET_WIDTH, SPRITE_SIZE, - standardAnimations, standardAccessories, - AccessoryInfo, - accessoryGroups, - baseSpritePath, - baseSpriteAccessoriesPath, + standardAnimations, } from './sprites/sprites' -import { FlaggedFeature } from '../FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' -import { HedgehogBuddyAccessory } from './components/AccessoryButton' -import './HedgehogBuddy.scss' -import { themeLogic } from '~/layout/navigation-3000/themeLogic' const xFrames = SPRITE_SHEET_WIDTH / SPRITE_SIZE const boundaryPadding = 20 diff --git a/frontend/src/lib/components/HedgehogBuddy/components/AccessoryButton.tsx b/frontend/src/lib/components/HedgehogBuddy/components/AccessoryButton.tsx index 2d62f3242945a..852a790607bc1 100644 --- a/frontend/src/lib/components/HedgehogBuddy/components/AccessoryButton.tsx +++ b/frontend/src/lib/components/HedgehogBuddy/components/AccessoryButton.tsx @@ -1,10 +1,12 @@ -import { capitalizeFirstLetter } from 'lib/utils' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { useActions, useValues } from 'kea' import { IconLock } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { capitalizeFirstLetter } from 'lib/utils' + +import { themeLogic } from '~/layout/navigation-3000/themeLogic' + import { hedgehogbuddyLogic } from '../hedgehogbuddyLogic' import { AccessoryInfo, baseSpriteAccessoriesPath } from '../sprites/sprites' -import { themeLogic } from '~/layout/navigation-3000/themeLogic' export type HedgehogBuddyAccessoryProps = { accessory: AccessoryInfo diff --git a/frontend/src/lib/components/HedgehogBuddy/hedgehogbuddyLogic.ts b/frontend/src/lib/components/HedgehogBuddy/hedgehogbuddyLogic.ts index e2c1215290f2f..e010b8914077c 100644 --- a/frontend/src/lib/components/HedgehogBuddy/hedgehogbuddyLogic.ts +++ b/frontend/src/lib/components/HedgehogBuddy/hedgehogbuddyLogic.ts @@ -1,7 +1,7 @@ import { actions, kea, listeners, path, reducers, selectors } from 'kea' +import posthog from 'posthog-js' import type { hedgehogbuddyLogicType } from './hedgehogbuddyLogicType' -import posthog from 'posthog-js' import { AccessoryInfo, standardAccessories } from './sprites/sprites' export const hedgehogbuddyLogic = kea([ diff --git a/frontend/src/lib/components/HelpButton/HelpButton.tsx b/frontend/src/lib/components/HelpButton/HelpButton.tsx index e15f1b62664f4..18ce8adba63e7 100644 --- a/frontend/src/lib/components/HelpButton/HelpButton.tsx +++ b/frontend/src/lib/components/HelpButton/HelpButton.tsx @@ -1,29 +1,28 @@ import './HelpButton.scss' -import { kea, useActions, useValues, props, key, path, connect, actions, reducers, listeners } from 'kea' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { HelpType } from '~/types' -import type { helpButtonLogicType } from './HelpButtonType' + +import { Placement } from '@floating-ui/react' +import clsx from 'clsx' +import { actions, connect, kea, key, listeners, path, props, reducers, useActions, useValues } from 'kea' import { IconArrowDropDown, IconArticle, + IconBugReport, + IconFeedback, IconHelpOutline, - IconQuestionAnswer, IconMessages, - IconFlare, - IconLive, + IconQuestionAnswer, IconSupport, - IconFeedback, - IconBugReport, } from 'lib/lemon-ui/icons' -import clsx from 'clsx' -import { Placement } from '@floating-ui/react' +import { LemonMenu } from 'lib/lemon-ui/LemonMenu' import { DefaultAction, inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' -import { hedgehogbuddyLogic } from '../HedgehogBuddy/hedgehogbuddyLogic' -import { HedgehogBuddyWithLogic } from '../HedgehogBuddy/HedgehogBuddy' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { HelpType } from '~/types' + import { supportLogic } from '../Support/supportLogic' import { SupportModal } from '../Support/SupportModal' -import { LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import type { helpButtonLogicType } from './HelpButtonType' const HELP_UTM_TAGS = '?utm_medium=in-product&utm_campaign=help-button-top' @@ -88,8 +87,6 @@ export function HelpButton({ const { validProductTourSequences } = useValues(inAppPromptLogic) const { runFirstValidSequence, promptAction } = useActions(inAppPromptLogic) const { isPromptVisible } = useValues(inAppPromptLogic) - const { hedgehogModeEnabled } = useValues(hedgehogbuddyLogic) - const { setHedgehogModeEnabled } = useActions(hedgehogbuddyLogic) const { openSupportForm } = useActions(supportLogic) const { isCloudOrDev } = useValues(preflightLogic) @@ -103,20 +100,6 @@ export function HelpButton({ <> , - label: "What's new?", - onClick: () => { - reportHelpButtonUsed(HelpType.Updates) - hideHelp() - }, - to: 'https://posthog.com/changelog', - targetBlank: true, - }, - ], - }, showSupportOptions && { items: [ { @@ -182,14 +165,6 @@ export function HelpButton({ hideHelp() }, }, - { - label: `${hedgehogModeEnabled ? 'Disable' : 'Enable'} hedgehog mode`, - icon: , - onClick: () => { - setHedgehogModeEnabled(!hedgehogModeEnabled) - hideHelp() - }, - }, ], }, ]} @@ -208,7 +183,6 @@ export function HelpButton({ )}
    - ) diff --git a/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx b/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx index e05346f298650..43fc695208b90 100644 --- a/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx +++ b/frontend/src/lib/components/HogQLEditor/HogQLEditor.stories.tsx @@ -1,7 +1,8 @@ -import { StoryFn, Meta, StoryObj } from '@storybook/react' -import { HogQLEditor } from './HogQLEditor' +import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useState } from 'react' +import { HogQLEditor } from './HogQLEditor' + type Story = StoryObj const meta: Meta = { title: 'Components/HogQLEditor', diff --git a/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx b/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx index 0177f2345311c..44bea60e144d4 100644 --- a/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx +++ b/frontend/src/lib/components/HogQLEditor/HogQLEditor.tsx @@ -1,11 +1,12 @@ -import { useRef, useState } from 'react' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { CLICK_OUTSIDE_BLOCK_CLASS } from 'lib/hooks/useOutsideClickHandler' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconErrorOutline, IconInfo } from 'lib/lemon-ui/icons' -import { useActions, useValues } from 'kea' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { useRef, useState } from 'react' + import { hogQLEditorLogic } from './hogQLEditorLogic' -import { Link } from '@posthog/lemon-ui' export interface HogQLEditorProps { onChange: (value: string) => void diff --git a/frontend/src/lib/components/HogQLEditor/hogQLEditorLogic.ts b/frontend/src/lib/components/HogQLEditor/hogQLEditorLogic.ts index 0507e78595775..9e6ce67595653 100644 --- a/frontend/src/lib/components/HogQLEditor/hogQLEditorLogic.ts +++ b/frontend/src/lib/components/HogQLEditor/hogQLEditorLogic.ts @@ -1,10 +1,11 @@ import { actions, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import React from 'react' + import { query } from '~/queries/query' +import { HogQLMetadata, HogQLMetadataResponse, NodeKind } from '~/queries/schema' import type { hogQLEditorLogicType } from './hogQLEditorLogicType' -import { HogQLMetadata, HogQLMetadataResponse, NodeKind } from '~/queries/schema' -import { loaders } from 'kea-loaders' -import React from 'react' export interface HogQLEditorLogicProps { key: string diff --git a/frontend/src/lib/components/InsightLabel/InsightLabel.scss b/frontend/src/lib/components/InsightLabel/InsightLabel.scss index dfc7d94f59df0..811e8e1456c8e 100644 --- a/frontend/src/lib/components/InsightLabel/InsightLabel.scss +++ b/frontend/src/lib/components/InsightLabel/InsightLabel.scss @@ -25,7 +25,7 @@ border-radius: 50%; margin-left: 2px; margin-right: 6px; - border: 2px solid #ffffff; + border: 2px solid #fff; box-sizing: border-box; } } diff --git a/frontend/src/lib/components/InsightLabel/index.tsx b/frontend/src/lib/components/InsightLabel/index.tsx index 2f11144d7c2cf..82adff76f6f38 100644 --- a/frontend/src/lib/components/InsightLabel/index.tsx +++ b/frontend/src/lib/components/InsightLabel/index.tsx @@ -1,15 +1,17 @@ -import { ActionFilter, BreakdownKeyType } from '~/types' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { capitalizeFirstLetter, hexToRGBA, midEllipsis } from 'lib/utils' import './InsightLabel.scss' -import { SeriesLetter } from 'lib/components/SeriesGlyph' -import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' + +import { LemonTag } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useValues } from 'kea' +import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter, hexToRGBA, midEllipsis } from 'lib/utils' import { mathsLogic } from 'scenes/trends/mathsLogic' -import clsx from 'clsx' + import { groupsModel } from '~/models/groupsModel' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { LemonTag } from '@posthog/lemon-ui' +import { ActionFilter, BreakdownKeyType } from '~/types' export enum IconSize { Small = 'small', diff --git a/frontend/src/lib/components/InsightLegend/InsightLegend.tsx b/frontend/src/lib/components/InsightLegend/InsightLegend.tsx index ef82a062c8b95..30f1db7ac0245 100644 --- a/frontend/src/lib/components/InsightLegend/InsightLegend.tsx +++ b/frontend/src/lib/components/InsightLegend/InsightLegend.tsx @@ -1,10 +1,12 @@ import './InsightLegend.scss' + +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' -import clsx from 'clsx' +import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' + import { InsightLegendRow } from './InsightLegendRow' import { shouldHighlightThisRow } from './utils' -import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' export interface InsightLegendProps { readOnly?: boolean diff --git a/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx b/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx index 900dec1969031..cf20e9d60d5e5 100644 --- a/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx +++ b/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx @@ -1,12 +1,13 @@ -import { InsightLabel } from 'lib/components/InsightLabel' import { getSeriesColor } from 'lib/colors' +import { InsightLabel } from 'lib/components/InsightLabel' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { formatCompareLabel } from 'scenes/insights/views/InsightsTable/columns/SeriesColumn' -import { ChartDisplayType } from '~/types' +import { useEffect, useRef } from 'react' import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' +import { formatCompareLabel } from 'scenes/insights/views/InsightsTable/columns/SeriesColumn' import { IndexedTrendResult } from 'scenes/trends/types' -import { useEffect, useRef } from 'react' + import { TrendsFilter } from '~/queries/schema' +import { ChartDisplayType } from '~/types' type InsightLegendRowProps = { hiddenLegendKeys: Record diff --git a/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx b/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx index b8a95dc839191..1dddc064cb281 100644 --- a/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx +++ b/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx @@ -1,7 +1,8 @@ +import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonSelect } from '@posthog/lemon-ui' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + import { InsightQueryNode } from '~/queries/schema' interface IntervalFilterProps { diff --git a/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts index d2136d8d8a682..e864d54edfc39 100644 --- a/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts +++ b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts @@ -1,14 +1,16 @@ -import { kea, props, key, path, connect, actions, reducers, listeners } from 'kea' -import { objectsEqual, dateMapping } from 'lib/utils' -import type { intervalFilterLogicType } from './intervalFilterLogicType' +import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea' import { IntervalKeyType, Intervals, intervals } from 'lib/components/IntervalFilter/intervals' -import { BaseMathType, InsightLogicProps, IntervalType } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { dayjs } from 'lib/dayjs' -import { InsightQueryNode, TrendsQuery } from '~/queries/schema' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { BASE_MATH_DEFINITIONS } from 'scenes/trends/mathsLogic' +import { dateMapping, objectsEqual } from 'lib/utils' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { BASE_MATH_DEFINITIONS } from 'scenes/trends/mathsLogic' + +import { InsightQueryNode, TrendsQuery } from '~/queries/schema' +import { BaseMathType, InsightLogicProps, IntervalType } from '~/types' + +import type { intervalFilterLogicType } from './intervalFilterLogicType' export const intervalFilterLogic = kea([ props({} as InsightLogicProps), diff --git a/frontend/src/lib/components/JSBookmarklet.tsx b/frontend/src/lib/components/JSBookmarklet.tsx index 54d87d84cc9af..c0cee95aed608 100644 --- a/frontend/src/lib/components/JSBookmarklet.tsx +++ b/frontend/src/lib/components/JSBookmarklet.tsx @@ -1,9 +1,10 @@ -import { TeamBasicType } from '~/types' import { useActions } from 'kea' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { IconBookmarkBorder } from 'lib/lemon-ui/icons' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { useEffect, useRef } from 'react' +import { TeamBasicType } from '~/types' + export function JSBookmarklet({ team }: { team: TeamBasicType }): JSX.Element { const initCall = `posthog.init('${team?.api_token}',{api_host:'${location.origin}', loaded: () => alert('PostHog is now tracking events!')})` const href = `javascript:(function()%7Bif%20(window.posthog)%20%7Balert(%22Error%3A%20PostHog%20already%20is%20installed%20on%20this%20site%22)%7D%20else%20%7B!function(t%2Ce)%7Bvar%20o%2Cn%2Cp%2Cr%3Be.__SV%7C%7C(window.posthog%3De%2Ce._i%3D%5B%5D%2Ce.init%3Dfunction(i%2Cs%2Ca)%7Bfunction%20g(t%2Ce)%7Bvar%20o%3De.split(%22.%22)%3B2%3D%3Do.length%26%26(t%3Dt%5Bo%5B0%5D%5D%2Ce%3Do%5B1%5D)%2Ct%5Be%5D%3Dfunction()%7Bt.push(%5Be%5D.concat(Array.prototype.slice.call(arguments%2C0)))%7D%7D(p%3Dt.createElement(%22script%22)).type%3D%22text%2Fjavascript%22%2Cp.async%3D!0%2Cp.src%3Ds.api_host%2B%22%2Fstatic%2Farray.js%22%2C(r%3Dt.getElementsByTagName(%22script%22)%5B0%5D).parentNode.insertBefore(p%2Cr)%3Bvar%20u%3De%3Bfor(void%200!%3D%3Da%3Fu%3De%5Ba%5D%3D%5B%5D%3Aa%3D%22posthog%22%2Cu.people%3Du.people%7C%7C%5B%5D%2Cu.toString%3Dfunction(t)%7Bvar%20e%3D%22posthog%22%3Breturn%22posthog%22!%3D%3Da%26%26(e%2B%3D%22.%22%2Ba)%2Ct%7C%7C(e%2B%3D%22%20(stub)%22)%2Ce%7D%2Cu.people.toString%3Dfunction()%7Breturn%20u.toString(1)%2B%22.people%20(stub)%22%7D%2Co%3D%22capture%20identify%20alias%20people.set%20people.set_once%20set_config%20register%20register_once%20unregister%20opt_out_capturing%20has_opted_out_capturing%20opt_in_capturing%20reset%20isFeatureEnabled%20onFeatureFlags%22.split(%22%20%22)%2Cn%3D0%3Bn%3Co.length%3Bn%2B%2B)g(u%2Co%5Bn%5D)%3Be._i.push(%5Bi%2Cs%2Ca%5D)%7D%2Ce.__SV%3D1)%7D(document%2Cwindow.posthog%7C%7C%5B%5D)%3B${encodeURIComponent( diff --git a/frontend/src/lib/components/JSSnippet.tsx b/frontend/src/lib/components/JSSnippet.tsx index 8458e79ad9c1b..1f0751187ef7e 100644 --- a/frontend/src/lib/components/JSSnippet.tsx +++ b/frontend/src/lib/components/JSSnippet.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' export function JSSnippet(): JSX.Element { diff --git a/frontend/src/lib/components/Map/Map.stories.tsx b/frontend/src/lib/components/Map/Map.stories.tsx index e7e120a8244a3..0fd7feb8fad7e 100644 --- a/frontend/src/lib/components/Map/Map.stories.tsx +++ b/frontend/src/lib/components/Map/Map.stories.tsx @@ -7,18 +7,13 @@ const coordinates: [number, number] = [0.119167, 52.205276] const meta: Meta = { title: 'Components/Map', component: Map, - tags: ['autodocs'], + tags: ['autodocs', 'test-skip'], // :TRICKY: We can't use markers in Storybook stories, as the Marker class is // not JSON-serializable (circular structure). args: { center: coordinates, className: 'h-60', }, - parameters: { - testOptions: { - skip: true, - }, - }, } type Story = StoryObj diff --git a/frontend/src/lib/components/Map/Map.tsx b/frontend/src/lib/components/Map/Map.tsx index a7365d53f2773..53686f8da2f18 100644 --- a/frontend/src/lib/components/Map/Map.tsx +++ b/frontend/src/lib/components/Map/Map.tsx @@ -1,15 +1,15 @@ -import { useEffect, useRef } from 'react' +import 'maplibre-gl/dist/maplibre-gl.css' +import './Maplibre.scss' + import { useValues } from 'kea' import maplibregl, { Map as RawMap, Marker } from 'maplibre-gl' import { Protocol } from 'pmtiles' import layers from 'protomaps-themes-base' +import { useEffect, useRef } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import useResizeObserver from 'use-resize-observer' -import 'maplibre-gl/dist/maplibre-gl.css' -import './Maplibre.scss' - import { themeLogic } from '~/layout/navigation-3000/themeLogic' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' const protocol = new Protocol() maplibregl.addProtocol('pmtiles', protocol.tile) diff --git a/frontend/src/lib/components/Map/Maplibre.scss b/frontend/src/lib/components/Map/Maplibre.scss index 27a9336e839ca..2a7688a3f23af 100644 --- a/frontend/src/lib/components/Map/Maplibre.scss +++ b/frontend/src/lib/components/Map/Maplibre.scss @@ -1,6 +1,6 @@ .maplibregl-ctrl-attrib-button:focus, .maplibregl-ctrl-group button:focus { - box-shadow: 0 0 2px 2px var(--primary); + box-shadow: 0 0 2px 2px var(--primary-3000); } @media screen { @@ -11,6 +11,7 @@ .maplibregl-ctrl-attrib .maplibregl-ctrl-attrib-button { background-color: var(--bg-3000); + [theme='dark'] & { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E"); } @@ -18,6 +19,7 @@ .maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button { background-color: var(--bg-3000); + [theme='dark'] & { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E"); } diff --git a/frontend/src/lib/components/NotFound/NotFound.scss b/frontend/src/lib/components/NotFound/NotFound.scss index d9a3d97ced82f..1d8d678a36110 100644 --- a/frontend/src/lib/components/NotFound/NotFound.scss +++ b/frontend/src/lib/components/NotFound/NotFound.scss @@ -1,6 +1,5 @@ .NotFoundComponent { text-align: center; - margin-top: 2rem; margin: 6rem auto; max-width: 600px; diff --git a/frontend/src/lib/components/NotFound/NotFound.stories.tsx b/frontend/src/lib/components/NotFound/NotFound.stories.tsx index 948b590749404..0450fd868427f 100644 --- a/frontend/src/lib/components/NotFound/NotFound.stories.tsx +++ b/frontend/src/lib/components/NotFound/NotFound.stories.tsx @@ -1,4 +1,4 @@ -import { StoryFn, Meta, StoryObj } from '@storybook/react' +import { Meta, StoryFn, StoryObj } from '@storybook/react' import { NotFound } from './index' diff --git a/frontend/src/lib/components/NotFound/index.tsx b/frontend/src/lib/components/NotFound/index.tsx index 1c1a20c595925..f9539f64c0185 100644 --- a/frontend/src/lib/components/NotFound/index.tsx +++ b/frontend/src/lib/components/NotFound/index.tsx @@ -1,11 +1,13 @@ -import { capitalizeFirstLetter } from 'lib/utils' -import { Link } from 'lib/lemon-ui/Link' import './NotFound.scss' + +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { supportLogic } from '../Support/supportLogic' +import { Link } from 'lib/lemon-ui/Link' +import { capitalizeFirstLetter } from 'lib/utils' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' -import { LemonButton } from '@posthog/lemon-ui' + +import { supportLogic } from '../Support/supportLogic' interface NotFoundProps { object: string // Type of object that was not found (e.g. `dashboard`, `insight`, `action`, ...) diff --git a/frontend/src/lib/components/ObjectTags/ObjectTags.stories.tsx b/frontend/src/lib/components/ObjectTags/ObjectTags.stories.tsx index d8df145c5094e..4acbe9e6c3909 100644 --- a/frontend/src/lib/components/ObjectTags/ObjectTags.stories.tsx +++ b/frontend/src/lib/components/ObjectTags/ObjectTags.stories.tsx @@ -1,4 +1,5 @@ -import { StoryFn, Meta, StoryObj } from '@storybook/react' +import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { ObjectTags, ObjectTagsProps } from './ObjectTags' type Story = StoryObj diff --git a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx index 00d447b71ff0c..f0bcb900d0a6e 100644 --- a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx +++ b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx @@ -1,16 +1,18 @@ -import { Tag, Select } from 'antd' -import { colorForString } from 'lib/utils' -import { CSSProperties, useMemo } from 'react' // eslint-disable-next-line no-restricted-imports -import { SyncOutlined, CloseOutlined } from '@ant-design/icons' -import { SelectGradientOverflow } from '../SelectGradientOverflow' +import { CloseOutlined, SyncOutlined } from '@ant-design/icons' +import { IconPlus } from '@posthog/icons' +import { Select, Tag } from 'antd' +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { objectTagsLogic } from 'lib/components/ObjectTags/objectTagsLogic' -import { AvailableFeature } from '~/types' -import { sceneLogic } from 'scenes/sceneLogic' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import clsx from 'clsx' -import { IconPlus } from '@posthog/icons' +import { colorForString } from 'lib/utils' +import { CSSProperties, useMemo } from 'react' +import { sceneLogic } from 'scenes/sceneLogic' + +import { AvailableFeature } from '~/types' + +import { SelectGradientOverflow } from '../SelectGradientOverflow' interface ObjectTagsPropsBase { tags: string[] diff --git a/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts b/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts index 41ebccae03559..4564a203301c0 100644 --- a/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts +++ b/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts @@ -1,7 +1,8 @@ -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' import { objectTagsLogic, ObjectTagsLogicProps } from 'lib/components/ObjectTags/objectTagsLogic' +import { initKeaTests } from '~/test/init' + describe('objectTagsLogic', () => { let logic: ReturnType let props: ObjectTagsLogicProps @@ -28,7 +29,7 @@ describe('objectTagsLogic', () => { }) it('handle adding a new tag', async () => { await expectLogic(logic, async () => { - await logic.actions.setNewTag('Nigh') + logic.actions.setNewTag('Nigh') logic.actions.handleAdd('Nightly') }) .toDispatchActions(['setNewTag']) @@ -43,7 +44,7 @@ describe('objectTagsLogic', () => { newTag: '', }) // @ts-expect-error - const mockedOnChange = props.onChange?.mock as any + const mockedOnChange = props.onChange?.mock expect(mockedOnChange.calls.length).toBe(1) expect(mockedOnChange.calls[0][0]).toBe('nightly') expect(mockedOnChange.calls[0][1]).toEqual(['a', 'b', 'c', 'nightly']) @@ -69,7 +70,7 @@ describe('objectTagsLogic', () => { tags: ['b', 'c'], }) // @ts-expect-error - const mockedOnChange = props.onChange?.mock as any + const mockedOnChange = props.onChange?.mock expect(mockedOnChange.calls.length).toBe(1) expect(mockedOnChange.calls[0][0]).toBe('a') expect(mockedOnChange.calls[0][1]).toEqual(['b', 'c']) diff --git a/frontend/src/lib/components/ObjectTags/objectTagsLogic.ts b/frontend/src/lib/components/ObjectTags/objectTagsLogic.ts index f1dbffcf57473..3770b27a8a491 100644 --- a/frontend/src/lib/components/ObjectTags/objectTagsLogic.ts +++ b/frontend/src/lib/components/ObjectTags/objectTagsLogic.ts @@ -1,7 +1,8 @@ +import equal from 'fast-deep-equal' import { actions, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' -import type { objectTagsLogicType } from './objectTagsLogicType' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import equal from 'fast-deep-equal' + +import type { objectTagsLogicType } from './objectTagsLogicType' export interface ObjectTagsLogicProps { id: number diff --git a/frontend/src/lib/components/PageHeader.tsx b/frontend/src/lib/components/PageHeader.tsx index 5aee5b5d0bc01..365b00def6c2a 100644 --- a/frontend/src/lib/components/PageHeader.tsx +++ b/frontend/src/lib/components/PageHeader.tsx @@ -1,9 +1,11 @@ import clsx from 'clsx' import { useValues } from 'kea' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { Within3000PageHeaderContext } from 'lib/lemon-ui/LemonButton/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { createPortal } from 'react-dom' import { DraggableToNotebook, DraggableToNotebookProps } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' + import { breadcrumbsLogic } from '~/layout/navigation/Breadcrumbs/breadcrumbsLogic' interface PageHeaderProps { @@ -32,7 +34,6 @@ export function PageHeader({ return ( <> - {} {(!is3000 || description) && (
    @@ -49,10 +50,18 @@ export function PageHeader({ {!is3000 &&
    {buttons}
    }
    )} - {is3000 && buttons && actionsContainer && createPortal(buttons, actionsContainer)} + {is3000 && + buttons && + actionsContainer && + createPortal( + + {buttons} + , + actionsContainer + )} {caption &&
    {caption}
    } - {delimited && } + {delimited && } ) } diff --git a/frontend/src/lib/components/PasswordStrength.tsx b/frontend/src/lib/components/PasswordStrength.tsx index 30e373dbaba51..f3f5be49ef656 100644 --- a/frontend/src/lib/components/PasswordStrength.tsx +++ b/frontend/src/lib/components/PasswordStrength.tsx @@ -1,6 +1,6 @@ import { Progress } from 'antd' -import zxcvbn from 'zxcvbn' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import zxcvbn from 'zxcvbn' export default function PasswordStrength({ password = '', diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx index cd01609e82f53..91317e1d559ea 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx @@ -1,9 +1,9 @@ +import { IconPlus } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Popover } from 'lib/lemon-ui/Popover/Popover' import { useState } from 'react' import { PathCleaningFilter } from '~/types' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconPlus } from 'lib/lemon-ui/icons' import { PathRegexPopover } from './PathRegexPopover' diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx index e4d73e9fd6f6b..588890a85b426 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx @@ -1,9 +1,9 @@ -import { useState } from 'react' - -import { PathCleaningFilter } from '~/types' import { LemonSnack } from '@posthog/lemon-ui' import { Popover } from 'lib/lemon-ui/Popover/Popover' import { midEllipsis } from 'lib/utils' +import { useState } from 'react' + +import { PathCleaningFilter } from '~/types' import { PathRegexPopover } from './PathRegexPopover' diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.stories.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.stories.tsx index 02ab50eb82b24..de1b7320c6290 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.stories.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useState } from 'react' + import { PathCleaningFilter } from '~/types' import { PathCleanFilters, PathCleanFiltersProps } from './PathCleanFilters' diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.tsx index fb2bd5ef60895..01b26636feef9 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilters.tsx @@ -1,6 +1,7 @@ import { PathCleaningFilter } from '~/types' -import { PathCleanFilterItem } from './PathCleanFilterItem' + import { PathCleanFilterAddItemButton } from './PathCleanFilterAddItemButton' +import { PathCleanFilterItem } from './PathCleanFilterItem' export interface PathCleanFiltersProps { filters?: PathCleaningFilter[] diff --git a/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx b/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx index e1a97e01f391b..70720f11a30cb 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx @@ -1,6 +1,6 @@ +import { LemonButton, LemonDivider, LemonInput } from '@posthog/lemon-ui' import { useState } from 'react' -import { LemonInput, LemonButton, LemonDivider } from '@posthog/lemon-ui' import { PathCleaningFilter } from '~/types' interface PathRegexPopoverProps { diff --git a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx index 46e5f491da484..f801f600cf045 100644 --- a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx +++ b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx @@ -1,13 +1,15 @@ +import './PayGateMini.scss' + +import { Link } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { AvailableFeature } from '~/types' -import { userLogic } from 'scenes/userLogic' +import { FEATURE_MINIMUM_PLAN, POSTHOG_CLOUD_STANDARD_PLAN } from 'lib/constants' import { IconEmojiPeople, IconLightBulb, IconLock, IconPremium } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './PayGateMini.scss' -import { FEATURE_MINIMUM_PLAN, POSTHOG_CLOUD_STANDARD_PLAN } from 'lib/constants' -import clsx from 'clsx' -import { Link } from '@posthog/lemon-ui' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature } from '~/types' type PayGateSupportedFeatures = | AvailableFeature.DASHBOARD_PERMISSIONING diff --git a/frontend/src/lib/components/PayGatePage/PayGatePage.tsx b/frontend/src/lib/components/PayGatePage/PayGatePage.tsx index bc33e212df053..c579d564cbe1a 100644 --- a/frontend/src/lib/components/PayGatePage/PayGatePage.tsx +++ b/frontend/src/lib/components/PayGatePage/PayGatePage.tsx @@ -1,11 +1,13 @@ +import './PayGatePage.scss' + import { useValues } from 'kea' -import { identifierToHuman } from 'lib/utils' import { IconOpenInNew } from 'lib/lemon-ui/icons' -import './PayGatePage.scss' -import { AvailableFeature } from '~/types' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { identifierToHuman } from 'lib/utils' import { billingLogic } from 'scenes/billing/billingLogic' +import { AvailableFeature } from '~/types' + interface PayGatePageInterface { header: string | JSX.Element caption: string | JSX.Element diff --git a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.stories.tsx b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.stories.tsx index c65b8df3f901c..868c62de41edd 100644 --- a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.stories.tsx +++ b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.stories.tsx @@ -1,6 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useState } from 'react' + import { mswDecorator } from '~/mocks/browser' + import { PersonPropertySelect, PersonPropertySelectProps } from './PersonPropertySelect' type Story = StoryObj diff --git a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx index d14d233f90f3b..55afbbc8fc898 100644 --- a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx +++ b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx @@ -1,17 +1,16 @@ -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' -import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import { restrictToHorizontalAxis, restrictToParentElement } from '@dnd-kit/modifiers' +import { horizontalListSortingStrategy, SortableContext, useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { LemonButton } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { IconPlus } from 'lib/lemon-ui/icons' import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' -import clsx from 'clsx' +import { Popover } from 'lib/lemon-ui/Popover/Popover' import { useState } from 'react' -import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core' -import { useSortable, SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' -import { restrictToHorizontalAxis, restrictToParentElement } from '@dnd-kit/modifiers' - export interface PersonPropertySelectProps { addText: string onChange: (names: string[]) => void diff --git a/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.stories.tsx b/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.stories.tsx index 5c8e18d6c063f..659e1a1bc4d9e 100644 --- a/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.stories.tsx +++ b/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.stories.tsx @@ -1,7 +1,9 @@ import { Meta } from '@storybook/react' -import { ProductIntroduction } from './ProductIntroduction' + import { ProductKey } from '~/types' +import { ProductIntroduction } from './ProductIntroduction' + const meta: Meta = { title: 'Components/Product Empty State', component: ProductIntroduction, diff --git a/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.tsx b/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.tsx index 125c31d54eb0d..02e59aeca94db 100644 --- a/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.tsx +++ b/frontend/src/lib/components/ProductIntroduction/ProductIntroduction.tsx @@ -1,10 +1,12 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useActions } from 'kea' import { IconClose, IconOpenInNew, IconPlus } from 'lib/lemon-ui/icons' -import { BuilderHog3, DetectiveHog } from '../hedgehogs' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { userLogic } from 'scenes/userLogic' -import { useActions } from 'kea' + import { ProductKey } from '~/types' +import { BuilderHog3, DetectiveHog } from '../hedgehogs' + export const ProductIntroduction = ({ productName, productKey, @@ -85,7 +87,7 @@ export const ProductIntroduction = ({ {action ? ( } + icon={} onClick={() => { updateHasSeenProductIntroFor(productKey, true) action && action() diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss b/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss index fcf66fec67796..f530132d670a6 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.scss @@ -5,36 +5,48 @@ } .property-value-type { - display: flex; align-items: center; - width: fit-content; - height: 1.25rem; - padding: 0.125rem 0.25rem; - letter-spacing: 0.25px; + background: var(--mid); border-radius: var(--radius); border: 1px solid var(--border-light); - background: var(--mid); color: var(--muted-alt); + cursor: default; + display: flex; font-size: 0.625rem; font-weight: 500; + height: 1.25rem; + letter-spacing: 0.25px; + padding: 0.125rem 0.25rem; text-transform: uppercase; white-space: nowrap; - cursor: default; + width: fit-content; + + .posthog-3000 & { + background: none; + border-radius: calc(var(--radius) * 0.75); + border-style: solid; + border-width: 1px; + font-family: var(--font-mono); + font-size: 0.688rem; + padding: 0.075rem 0.25rem; + } + &:not(:first-child) { margin-left: 0.25rem; } } .properties-table-value { - min-width: 12rem; - max-width: fit-content; - display: flex; align-items: center; + display: flex; + max-width: fit-content; + min-width: 12rem; .value-link { > * { vertical-align: middle; } + > svg { margin-left: 0.25rem; font-size: 1rem; @@ -43,7 +55,20 @@ .editable { text-decoration: underline dotted; - text-decoration-color: var(--primary); + text-decoration-color: var(--primary-3000); cursor: pointer; + + .posthog-3000 & { + border: 1px solid transparent; + border-radius: calc(var(--radius) * 0.75); + margin-left: -0.25rem; + padding: 0.125rem 0.25rem; + text-decoration: none; + + &:hover { + background: var(--bg-light); + border: 1px solid var(--border-light); + } + } } } diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.stories.tsx b/frontend/src/lib/components/PropertiesTable/PropertiesTable.stories.tsx new file mode 100644 index 0000000000000..732ff6542b17b --- /dev/null +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, StoryFn } from '@storybook/react' + +import { PropertyDefinitionType } from '~/types' + +import { PropertiesTable as PropertiesTableComponent } from '.' + +const meta: Meta = { + title: 'Components/Properties Table', + component: PropertiesTableComponent, +} +export default meta + +export const PropertiesTable: StoryFn = () => { + const properties = { + name: 'John Doe', + age: 30, + url: 'https://www.google.com', + is_good: true, + evil_level: null, + tags: ['best', 'cool', 'awesome'], + location: { + city: 'Prague', + country: 'Czechia', + }, + } + return +} diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx index c074b490fb35a..eb327e49bf990 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx @@ -1,21 +1,24 @@ -import { useMemo, useState } from 'react' +import './PropertiesTable.scss' -import { KEY_MAPPING, keyMappingKeys } from 'lib/taxonomy' -import { PropertyKeyInfo } from '../PropertyKeyInfo' +import { IconPencil } from '@posthog/icons' +import { LemonCheckbox, LemonInput, Link } from '@posthog/lemon-ui' import { Dropdown, Input, Menu, Popconfirm } from 'antd' -import { isURL } from 'lib/utils' -import { IconDeleteForever } from 'lib/lemon-ui/icons' -import './PropertiesTable.scss' -import { LemonTable, LemonTableColumns, LemonTableProps } from 'lib/lemon-ui/LemonTable' -import { CopyToClipboardInline } from '../CopyToClipboard' +import clsx from 'clsx' import { useValues } from 'kea' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { IconDeleteForever } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonTable, LemonTableColumns, LemonTableProps } from 'lib/lemon-ui/LemonTable' +import { KEY_MAPPING, keyMappingKeys } from 'lib/taxonomy' +import { isURL } from 'lib/utils' +import { useMemo, useState } from 'react' import { NewProperty } from 'scenes/persons/NewProperty' -import { LemonCheckbox, LemonInput, Link } from '@posthog/lemon-ui' -import clsx from 'clsx' + +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { PropertyDefinitionType } from '~/types' +import { CopyToClipboardInline } from '../CopyToClipboard' +import { PropertyKeyInfo } from '../PropertyKeyInfo' + type HandledType = 'string' | 'number' | 'bigint' | 'boolean' | 'undefined' | 'null' type Type = HandledType | 'symbol' | 'object' | 'function' @@ -85,18 +88,19 @@ function ValueDisplay({ const valueComponent = ( canEdit && textBasedTypes.includes(valueType) && setEditing(true)} > {!isURL(value) ? ( - valueString + {valueString} ) : ( {valueString} )} + {canEdit && } ) @@ -283,13 +287,10 @@ export function PropertiesTable({ title: '', width: 0, render: function Copy(_, item: any): JSX.Element | false { - if (Array.isArray(item[1]) || item[1] instanceof Object) { - return false - } return ( = { title: 'Filters/PropertyFilters', @@ -31,6 +33,11 @@ const propertyFilters = [ ] as AnyPropertyFilter[] export function ComparingPropertyFilters(): JSX.Element { + useStorybookMocks({ + get: { + '/api/event/values/': [], + }, + }) return ( <>

    Pop-over enabled

    diff --git a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx index dc9506368a0cd..24cf0b470087c 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx @@ -1,13 +1,16 @@ -import React, { useEffect } from 'react' -import { useValues, BindLogic, useActions } from 'kea' -import { propertyFilterLogic } from './propertyFilterLogic' -import { FilterRow } from './components/FilterRow' -import { AnyPropertyFilter, FilterLogicalOperator } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { TaxonomicPropertyFilter } from 'lib/components/PropertyFilters/components/TaxonomicPropertyFilter' import './PropertyFilters.scss' + +import { BindLogic, useActions, useValues } from 'kea' +import { TaxonomicPropertyFilter } from 'lib/components/PropertyFilters/components/TaxonomicPropertyFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import React, { useEffect } from 'react' import { LogicalRowDivider } from 'scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder' +import { AnyPropertyFilter, FilterLogicalOperator } from '~/types' + +import { FilterRow } from './components/FilterRow' +import { propertyFilterLogic } from './propertyFilterLogic' + interface PropertyFiltersProps { endpoint?: string | null propertyFilters?: AnyPropertyFilter[] | null diff --git a/frontend/src/lib/components/PropertyFilters/components/FilterRow.tsx b/frontend/src/lib/components/PropertyFilters/components/FilterRow.tsx index 4b8f10651e8d4..882af64fe0b7c 100644 --- a/frontend/src/lib/components/PropertyFilters/components/FilterRow.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/FilterRow.tsx @@ -1,13 +1,16 @@ -import React, { useState } from 'react' -import { AnyPropertyFilter, PathCleaningFilter } from '~/types' -import { PropertyFilterButton } from './PropertyFilterButton' -import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' -import { Popover } from 'lib/lemon-ui/Popover/Popover' import './FilterRow.scss' + import clsx from 'clsx' +import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' import { IconClose, IconDelete, IconPlus } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import React, { useState } from 'react' + +import { AnyPropertyFilter, PathCleaningFilter } from '~/types' + import { OperandTag } from './OperandTag' +import { PropertyFilterButton } from './PropertyFilterButton' interface FilterRowProps { item: Record diff --git a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.stories.tsx b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.stories.tsx index f3b0ef68557e7..545264a067bfe 100644 --- a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.stories.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.stories.tsx @@ -3,6 +3,7 @@ import { OperatorValueSelect, OperatorValueSelectProps, } from 'lib/components/PropertyFilters/components/OperatorValueSelect' + import { PropertyDefinition, PropertyType } from '~/types' const meta: Meta = { diff --git a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx index 817677a1f7f8a..5afaea8c4faeb 100644 --- a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react' -import { PropertyDefinition, PropertyFilterType, PropertyFilterValue, PropertyOperator, PropertyType } from '~/types' +import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui' +import { dayjs } from 'lib/dayjs' import { allOperatorsMapping, chooseOperatorMap, @@ -9,9 +9,11 @@ import { isOperatorRange, isOperatorRegex, } from 'lib/utils' +import { useEffect, useState } from 'react' + +import { PropertyDefinition, PropertyFilterType, PropertyFilterValue, PropertyOperator, PropertyType } from '~/types' + import { PropertyValue } from './PropertyValue' -import { dayjs } from 'lib/dayjs' -import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui' export interface OperatorValueSelectProps { type?: PropertyFilterType diff --git a/frontend/src/lib/components/PropertyFilters/components/PathItemSelector.tsx b/frontend/src/lib/components/PropertyFilters/components/PathItemSelector.tsx index 0efc10d4e9c90..8e23d445872e5 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PathItemSelector.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PathItemSelector.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react' -import { Popover } from 'lib/lemon-ui/Popover/Popover' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' import { SimpleOption, TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { useState } from 'react' interface PathItemSelectorProps { pathItem: TaxonomicFilterValue | undefined diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx index 959a8a47831ed..eea640e7c1b60 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx @@ -1,14 +1,18 @@ import './PropertyFilterButton.scss' + import { Button } from 'antd' -import { AnyPropertyFilter } from '~/types' -import { CloseButton } from 'lib/components/CloseButton' -import { cohortsModel } from '~/models/cohortsModel' import { useValues } from 'kea' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatPropertyLabel, midEllipsis } from 'lib/utils' +import { CloseButton } from 'lib/components/CloseButton' +import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon' import { KEY_MAPPING } from 'lib/taxonomy' +import { midEllipsis } from 'lib/utils' import React from 'react' -import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon' + +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { AnyPropertyFilter } from '~/types' + +import { formatPropertyLabel } from '../utils' export interface PropertyFilterButtonProps { onClick?: () => void diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterDatePicker.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterDatePicker.tsx index 2a2dd82c72c68..889e511a613e4 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterDatePicker.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterDatePicker.tsx @@ -1,10 +1,11 @@ +import { DatePicker } from 'lib/components/DatePicker' +import { PropertyValueProps } from 'lib/components/PropertyFilters/components/PropertyValue' import { dayjs } from 'lib/dayjs' -import { useEffect, useState } from 'react' -import { isOperatorDate } from 'lib/utils' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' +import { isOperatorDate } from 'lib/utils' +import { useEffect, useState } from 'react' + import { PropertyOperator } from '~/types' -import { PropertyValueProps } from 'lib/components/PropertyFilters/components/PropertyValue' -import { DatePicker } from 'lib/components/DatePicker' const dayJSMightParse = ( candidateDateTimeValue: string | number | (string | number)[] | null | undefined diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterIcon.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterIcon.tsx index 4b8cbeea830f0..fab60bffd85e2 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterIcon.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterIcon.tsx @@ -1,6 +1,7 @@ -import { PropertyFilterType } from '~/types' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { IconCohort, IconPerson, IconUnverifiedEvent } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' + +import { PropertyFilterType } from '~/types' export function PropertyFilterIcon({ type }: { type?: PropertyFilterType }): JSX.Element { let iconElement = <> diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFiltersDisplay.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFiltersDisplay.tsx index 1db0201362ea1..aef0dcb659a0a 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyFiltersDisplay.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFiltersDisplay.tsx @@ -1,4 +1,5 @@ import { AnyPropertyFilter } from '~/types' + import { PropertyFilterButton } from './PropertyFilterButton' const PropertyFiltersDisplay = ({ filters }: { filters: AnyPropertyFilter[] }): JSX.Element => { diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertySelect.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertySelect.tsx index 3cc2e1b93a9e0..0ad81b779db76 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertySelect.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertySelect.tsx @@ -1,11 +1,12 @@ -import { useState } from 'react' -import Fuse from 'fuse.js' import { Select } from 'antd' +import Fuse from 'fuse.js' +import { useActions } from 'kea' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow' -import { SelectOption } from '~/types' -import { useActions } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { useState } from 'react' + +import { SelectOption } from '~/types' interface Props { optionGroups: Array diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss index 7834113d53f3f..0a33bc26c1aae 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss @@ -29,7 +29,7 @@ background-color: inherit; .ant-select-selection-search { - padding-left: 0px !important; + padding-left: 0 !important; } .ant-select-selection-placeholder { diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx index 3d07a4bed1261..033e0b2680b1e 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx @@ -1,15 +1,17 @@ -import { useEffect, useRef, useState } from 'react' +import './PropertyValue.scss' + import { AutoComplete } from 'antd' -import { isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils' -import { PropertyFilterType, PropertyOperator, PropertyType } from '~/types' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { PropertyFilterDatePicker } from 'lib/components/PropertyFilters/components/PropertyFilterDatePicker' import { DurationPicker } from 'lib/components/DurationPicker/DurationPicker' -import './PropertyValue.scss' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' -import clsx from 'clsx' +import { PropertyFilterDatePicker } from 'lib/components/PropertyFilters/components/PropertyFilterDatePicker' import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils' +import { useEffect, useRef, useState } from 'react' + +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { PropertyFilterType, PropertyOperator, PropertyType } from '~/types' export interface PropertyValueProps { propertyKey: string diff --git a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx index 0c03aa12551b7..a3ff2475480fd 100644 --- a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx @@ -1,30 +1,33 @@ import './TaxonomicPropertyFilter.scss' -import { useMemo } from 'react' + +import { LemonButtonWithDropdown } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useMountedLogic, useValues } from 'kea' +import { OperatorValueSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect' import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' -import { taxonomicPropertyFilterLogic } from './taxonomicPropertyFilterLogic' +import { PropertyFilterInternalProps } from 'lib/components/PropertyFilters/types' +import { + isGroupPropertyFilter, + isPropertyFilterWithOperator, + propertyFilterTypeToTaxonomicFilterType, +} from 'lib/components/PropertyFilters/utils' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { OperatorValueSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect' -import { isOperatorMulti, isOperatorRegex } from 'lib/utils' import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' import { TaxonomicFilterGroup, TaxonomicFilterGroupType, TaxonomicFilterValue, } from 'lib/components/TaxonomicFilter/types' -import { - isGroupPropertyFilter, - isPropertyFilterWithOperator, - propertyFilterTypeToTaxonomicFilterType, -} from 'lib/components/PropertyFilters/utils' -import { PropertyFilterInternalProps } from 'lib/components/PropertyFilters/types' -import clsx from 'clsx' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { AnyPropertyFilter, FilterLogicalOperator, PropertyDefinitionType, PropertyFilterType } from '~/types' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { LemonButtonWithDropdown } from '@posthog/lemon-ui' import { IconPlusMini } from 'lib/lemon-ui/icons' +import { isOperatorMulti, isOperatorRegex } from 'lib/utils' +import { useMemo } from 'react' + +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { AnyPropertyFilter, FilterLogicalOperator, PropertyDefinitionType, PropertyFilterType } from '~/types' + import { OperandTag } from './OperandTag' +import { taxonomicPropertyFilterLogic } from './taxonomicPropertyFilterLogic' let uniqueMemoizedIndex = 0 diff --git a/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.test.ts b/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.test.ts index d6ab1b8e0b53e..08bfe4962ba44 100644 --- a/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.test.ts +++ b/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.test.ts @@ -1,8 +1,9 @@ -import { initKeaTests } from '~/test/init' -import { taxonomicPropertyFilterLogic } from 'lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic' import { expectLogic } from 'kea-test-utils' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { taxonomicPropertyFilterLogic } from 'lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic' import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' + +import { initKeaTests } from '~/test/init' describe('the taxonomic property filter', () => { let logic: ReturnType diff --git a/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.ts b/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.ts index 6361dfb89489c..9ab0b1d638e77 100644 --- a/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.ts +++ b/frontend/src/lib/components/PropertyFilters/components/taxonomicPropertyFilterLogic.ts @@ -1,21 +1,5 @@ -import { kea, props, key, path, connect, actions, reducers, selectors, listeners } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { TaxonomicPropertyFilterLogicProps } from 'lib/components/PropertyFilters/types' -import { - AnyPropertyFilter, - CohortPropertyFilter, - HogQLPropertyFilter, - PropertyDefinitionType, - PropertyFilterType, - PropertyOperator, - PropertyType, -} from '~/types' -import type { taxonomicPropertyFilterLogicType } from './taxonomicPropertyFilterLogicType' -import { cohortsModel } from '~/models/cohortsModel' -import { - TaxonomicFilterGroup, - TaxonomicFilterLogicProps, - TaxonomicFilterValue, -} from 'lib/components/TaxonomicFilter/types' import { isGroupPropertyFilter, isPropertyFilterWithOperator, @@ -25,7 +9,25 @@ import { taxonomicFilterTypeToPropertyFilterType, } from 'lib/components/PropertyFilters/utils' import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' +import { + TaxonomicFilterGroup, + TaxonomicFilterLogicProps, + TaxonomicFilterValue, +} from 'lib/components/TaxonomicFilter/types' + +import { cohortsModel } from '~/models/cohortsModel' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { + AnyPropertyFilter, + CohortPropertyFilter, + HogQLPropertyFilter, + PropertyDefinitionType, + PropertyFilterType, + PropertyOperator, + PropertyType, +} from '~/types' + +import type { taxonomicPropertyFilterLogicType } from './taxonomicPropertyFilterLogicType' export const taxonomicPropertyFilterLogic = kea([ props({} as TaxonomicPropertyFilterLogicProps), diff --git a/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.ts b/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.ts index ad46588a1338e..39e8083a7b071 100644 --- a/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.ts +++ b/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.ts @@ -1,9 +1,10 @@ import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { PropertyFilterLogicProps } from 'lib/components/PropertyFilters/types' +import { isValidPropertyFilter, parseProperties } from 'lib/components/PropertyFilters/utils' -import type { propertyFilterLogicType } from './propertyFilterLogicType' import { AnyPropertyFilter, EmptyPropertyFilter } from '~/types' -import { isValidPropertyFilter, parseProperties } from 'lib/components/PropertyFilters/utils' -import { PropertyFilterLogicProps } from 'lib/components/PropertyFilters/types' + +import type { propertyFilterLogicType } from './propertyFilterLogicType' export const propertyFilterLogic = kea([ path((key) => ['lib', 'components', 'PropertyFilters', 'propertyFilterLogic', key]), diff --git a/frontend/src/lib/components/PropertyFilters/types.ts b/frontend/src/lib/components/PropertyFilters/types.ts index 48af43e5573c7..0b7b671461f3e 100644 --- a/frontend/src/lib/components/PropertyFilters/types.ts +++ b/frontend/src/lib/components/PropertyFilters/types.ts @@ -1,11 +1,12 @@ -import { PropertyGroupFilter, AnyPropertyFilter, FilterLogicalOperator } from '~/types' +import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' +import { SelectGradientOverflowProps } from 'lib/components/SelectGradientOverflow' import { TaxonomicFilterGroup, TaxonomicFilterGroupType, TaxonomicFilterValue, } from 'lib/components/TaxonomicFilter/types' -import { SelectGradientOverflowProps } from 'lib/components/SelectGradientOverflow' -import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' + +import { AnyPropertyFilter, FilterLogicalOperator, PropertyGroupFilter } from '~/types' export interface PropertyFilterBaseProps { pageKey: string diff --git a/frontend/src/lib/components/PropertyFilters/utils.test.ts b/frontend/src/lib/components/PropertyFilters/utils.test.ts index f8ecda127588b..33ad74f8e35d6 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.test.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.test.ts @@ -1,19 +1,25 @@ +import { + breakdownFilterToTaxonomicFilterType, + convertPropertiesToPropertyGroup, + convertPropertyGroupToProperties, + isValidPropertyFilter, + propertyFilterTypeToTaxonomicFilterType, +} from 'lib/components/PropertyFilters/utils' + +import { BreakdownFilter } from '~/queries/schema' + import { AnyPropertyFilter, CohortPropertyFilter, ElementPropertyFilter, EmptyPropertyFilter, + FilterLogicalOperator, PropertyFilterType, + PropertyGroupFilter, PropertyOperator, SessionPropertyFilter, } from '../../../types' -import { - isValidPropertyFilter, - propertyFilterTypeToTaxonomicFilterType, - breakdownFilterToTaxonomicFilterType, -} from 'lib/components/PropertyFilters/utils' import { TaxonomicFilterGroupType } from '../TaxonomicFilter/types' -import { BreakdownFilter } from '~/queries/schema' describe('isValidPropertyFilter()', () => { it('returns values correctly', () => { @@ -123,3 +129,67 @@ describe('breakdownFilterToTaxonomicFilterType()', () => { ) }) }) + +describe('convertPropertyGroupToProperties()', () => { + it('converts a single layer property group into an array of properties', () => { + const propertyGroup = { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [ + { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ] as AnyPropertyFilter[], + }, + { + type: FilterLogicalOperator.And, + values: [ + { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ] as AnyPropertyFilter[], + }, + ], + } + expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([ + { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, + ]) + }) + + it('converts a deeply nested property group into an array of properties', () => { + const propertyGroup: PropertyGroupFilter = { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [{ type: FilterLogicalOperator.And, values: [{ key: '$lib' } as any] }], + }, + { type: FilterLogicalOperator.And, values: [{ key: '$browser' } as any] }, + ], + } + expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([{ key: '$lib' }, { key: '$browser' }]) + }) +}) + +describe('convertPropertiesToPropertyGroup', () => { + it('converts properties to one AND operator property group', () => { + const properties: any[] = [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }] + expect(convertPropertiesToPropertyGroup(properties)).toEqual({ + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }], + }, + ], + }) + }) + + it('converts properties to one AND operator property group', () => { + expect(convertPropertiesToPropertyGroup(undefined)).toEqual({ + type: FilterLogicalOperator.And, + values: [], + }) + }) +}) diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index b6371b8881c31..e4bb8cbba1c7f 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -1,26 +1,110 @@ +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { allOperatorsMapping, isOperatorFlag } from 'lib/utils' + +import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' +import { BreakdownFilter } from '~/queries/schema' import { AnyFilterLike, AnyPropertyFilter, CohortPropertyFilter, + CohortType, ElementPropertyFilter, + EmptyPropertyFilter, EventDefinition, EventPropertyFilter, FeaturePropertyFilter, FilterLogicalOperator, GroupPropertyFilter, HogQLPropertyFilter, + KeyMappingInterface, PersonPropertyFilter, PropertyDefinitionType, PropertyFilterType, + PropertyFilterValue, PropertyGroupFilter, PropertyGroupFilterValue, PropertyOperator, RecordingDurationFilter, SessionPropertyFilter, } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { flattenPropertyGroup, isPropertyGroup } from 'lib/utils' -import { BreakdownFilter } from '~/queries/schema' + +export function isPropertyGroup( + properties: + | PropertyGroupFilter + | PropertyGroupFilterValue + | AnyPropertyFilter[] + | AnyPropertyFilter + | Record + | null + | undefined +): properties is PropertyGroupFilter { + return ( + (properties as PropertyGroupFilter)?.type !== undefined && + (properties as PropertyGroupFilter)?.values !== undefined + ) +} + +function flattenPropertyGroup( + flattenedProperties: AnyPropertyFilter[], + propertyGroup: PropertyGroupFilter | PropertyGroupFilterValue | AnyPropertyFilter +): AnyPropertyFilter[] { + const obj: AnyPropertyFilter = {} as EmptyPropertyFilter + Object.keys(propertyGroup).forEach(function (k) { + obj[k] = propertyGroup[k] + }) + if (isValidPropertyFilter(obj)) { + flattenedProperties.push(obj) + } + if (isPropertyGroup(propertyGroup)) { + return propertyGroup.values.reduce(flattenPropertyGroup, flattenedProperties) + } + return flattenedProperties +} + +export function convertPropertiesToPropertyGroup( + properties: PropertyGroupFilter | AnyPropertyFilter[] | undefined +): PropertyGroupFilter { + if (isPropertyGroup(properties)) { + return properties + } + if (properties && properties.length > 0) { + return { type: FilterLogicalOperator.And, values: [{ type: FilterLogicalOperator.And, values: properties }] } + } + return { type: FilterLogicalOperator.And, values: [] } +} + +/** Flatten a filter group into an array of filters. NB: Logical operators (AND/OR) are lost in the process. */ +export function convertPropertyGroupToProperties( + properties?: PropertyGroupFilter | AnyPropertyFilter[] +): AnyPropertyFilter[] | undefined { + if (isPropertyGroup(properties)) { + return flattenPropertyGroup([], properties).filter(isValidPropertyFilter) + } + if (properties) { + return properties.filter(isValidPropertyFilter) + } + return properties +} + +export function formatPropertyLabel( + item: Record, + cohortsById: Partial>, + keyMapping: KeyMappingInterface, + valueFormatter: (value: PropertyFilterValue | undefined) => string | string[] | null = (s) => [String(s)] +): string { + if (isHogQLPropertyFilter(item as AnyFilterLike)) { + return extractExpressionComment(item.key) + } + const { value, key, operator, type } = item + return type === 'cohort' + ? cohortsById[value]?.name || `ID ${value}` + : (keyMapping[type === 'element' ? 'element' : 'event'][key]?.label || key) + + (isOperatorFlag(operator) + ? ` ${allOperatorsMapping[operator]}` + : ` ${(allOperatorsMapping[operator || 'exact'] || '?').split(' ')[0]} ${ + value && value.length === 1 && value[0] === '' ? '(empty string)' : valueFormatter(value) || '' + } `) +} /** Make sure unverified user property filter input has at least a "type" */ export function sanitizePropertyFilter(propertyFilter: AnyPropertyFilter): AnyPropertyFilter { @@ -40,7 +124,7 @@ export function parseProperties( return input || [] } if (input && !Array.isArray(input) && isPropertyGroup(input)) { - return flattenPropertyGroup([], input as PropertyGroupFilter) + return flattenPropertyGroup([], input) } // Old style dict properties return Object.entries(input).map(([inputKey, value]) => { diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss index 6bfa3e13ada30..fa5bdc421a424 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss @@ -7,12 +7,12 @@ .property-group-and-or-separator { color: var(--primary-alt); - padding: 0.5rem 0px; + padding: 0.5rem 0; font-size: 12px; font-weight: 600; position: relative; - &:before { + &::before { position: absolute; left: 17px; top: 0; @@ -63,7 +63,7 @@ font-size: 12px; &.selected { - background-color: var(--primary); + background-color: var(--primary-3000); color: #fff; } } diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.stories.tsx b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.stories.tsx index b7013d3b5dfb7..2a3896de8c889 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.stories.tsx +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.stories.tsx @@ -1,10 +1,13 @@ -import { useState } from 'react' import { Meta } from '@storybook/react' -import { FilterLogicalOperator, FilterType, AnyPropertyFilter, PropertyGroupFilter, PropertyOperator } from '~/types' import { useMountedLogic } from 'kea' -import { PropertyGroupFilters } from './PropertyGroupFilters' -import { TaxonomicFilterGroupType } from '../TaxonomicFilter/types' +import { useState } from 'react' + +import { useStorybookMocks } from '~/mocks/browser' import { cohortsModel } from '~/models/cohortsModel' +import { AnyPropertyFilter, FilterLogicalOperator, FilterType, PropertyGroupFilter, PropertyOperator } from '~/types' + +import { TaxonomicFilterGroupType } from '../TaxonomicFilter/types' +import { PropertyGroupFilters } from './PropertyGroupFilters' const meta: Meta = { title: 'Filters/PropertyGroupFilters', @@ -36,6 +39,11 @@ const taxonomicGroupTypes = [ ] export function GroupPropertyFilters(): JSX.Element { + useStorybookMocks({ + get: { + '/api/event/values/': [], + }, + }) useMountedLogic(cohortsModel) const [propertyGroupFilter, setPropertyGroupFilter] = useState({ diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx index 30d7c835ccf9f..6d532b1a948f0 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx @@ -1,17 +1,20 @@ -import { useValues, BindLogic, useActions } from 'kea' -import { PropertyGroupFilter, PropertyGroupFilterValue, FilterType, AnyPropertyFilter } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import './PropertyGroupFilters.scss' -import { propertyGroupFilterLogic } from './propertyGroupFilterLogic' -import { PropertyFilters } from '../PropertyFilters/PropertyFilters' -import { GlobalFiltersTitle } from 'scenes/insights/common' + +import { BindLogic, useActions, useValues } from 'kea' +import { isPropertyGroupFilterLike } from 'lib/components/PropertyFilters/utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { IconCopy, IconDelete, IconPlusMini } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import React from 'react' -import { isPropertyGroupFilterLike } from 'lib/components/PropertyFilters/utils' +import { GlobalFiltersTitle } from 'scenes/insights/common' +import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' + import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect' +import { AnyPropertyFilter, FilterType, PropertyGroupFilter, PropertyGroupFilterValue } from '~/types' + +import { PropertyFilters } from '../PropertyFilters/PropertyFilters' +import { propertyGroupFilterLogic } from './propertyGroupFilterLogic' interface PropertyGroupFilters { value: PropertyGroupFilter diff --git a/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts b/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts index 88d228ea88be8..cabfc14278390 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts +++ b/frontend/src/lib/components/PropertyGroupFilters/propertyGroupFilterLogic.ts @@ -1,11 +1,12 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' - -import { PropertyGroupFilter, FilterLogicalOperator, EmptyPropertyFilter } from '~/types' import { PropertyGroupFilterLogicProps } from 'lib/components/PropertyFilters/types' +import { objectsEqual } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { EmptyPropertyFilter, FilterLogicalOperator, PropertyGroupFilter } from '~/types' + +import { convertPropertiesToPropertyGroup } from '../PropertyFilters/utils' import type { propertyGroupFilterLogicType } from './propertyGroupFilterLogicType' -import { convertPropertiesToPropertyGroup, objectsEqual } from 'lib/utils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export const propertyGroupFilterLogic = kea([ path(['lib', 'components', 'PropertyGroupFilters', 'propertyGroupFilterLogic']), diff --git a/frontend/src/lib/components/PropertyIcon.stories.tsx b/frontend/src/lib/components/PropertyIcon.stories.tsx index 91b2149cb837d..229616979d58f 100644 --- a/frontend/src/lib/components/PropertyIcon.stories.tsx +++ b/frontend/src/lib/components/PropertyIcon.stories.tsx @@ -7,10 +7,7 @@ type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Icons/Property Icon', component: PropertyIcon, - parameters: { - testOptions: { skip: true }, // There are too many icons, the snapshots are huge in table form - }, - tags: ['autodocs'], + tags: ['autodocs', 'test-skip'], // There are too many icons, the snapshots are huge in table form } export default meta diff --git a/frontend/src/lib/components/PropertyIcon.tsx b/frontend/src/lib/components/PropertyIcon.tsx index 91f351b7f1093..d1a5bf8a6848d 100644 --- a/frontend/src/lib/components/PropertyIcon.tsx +++ b/frontend/src/lib/components/PropertyIcon.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx' import { IconAndroidOS, IconAppleIOS, @@ -17,10 +18,9 @@ import { IconWeb, IconWindows, } from 'lib/lemon-ui/icons' -import clsx from 'clsx' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { countryCodeToFlag } from 'scenes/insights/views/WorldMap' import { HTMLAttributes, ReactNode } from 'react' +import { countryCodeToFlag } from 'scenes/insights/views/WorldMap' export const PROPERTIES_ICON_MAP = { $browser: { diff --git a/frontend/src/lib/components/PropertyKeyInfo.scss b/frontend/src/lib/components/PropertyKeyInfo.scss index 23a2e43a5e18c..693cc701c17ea 100644 --- a/frontend/src/lib/components/PropertyKeyInfo.scss +++ b/frontend/src/lib/components/PropertyKeyInfo.scss @@ -31,8 +31,10 @@ .PropertyKeyInfo__overlay { padding: 0.25rem; + p { margin-bottom: 0.25rem; + &:last-child { margin-bottom: 0; } diff --git a/frontend/src/lib/components/PropertyKeyInfo.stories.tsx b/frontend/src/lib/components/PropertyKeyInfo.stories.tsx index 715091e635075..632480f7f0f62 100644 --- a/frontend/src/lib/components/PropertyKeyInfo.stories.tsx +++ b/frontend/src/lib/components/PropertyKeyInfo.stories.tsx @@ -1,4 +1,4 @@ -import { StoryFn, Meta, StoryObj } from '@storybook/react' +import { Meta, StoryFn, StoryObj } from '@storybook/react' import { PropertyKeyInfo } from './PropertyKeyInfo' diff --git a/frontend/src/lib/components/PropertyKeyInfo.tsx b/frontend/src/lib/components/PropertyKeyInfo.tsx index 45aa9117086b7..07d2313b872a8 100644 --- a/frontend/src/lib/components/PropertyKeyInfo.tsx +++ b/frontend/src/lib/components/PropertyKeyInfo.tsx @@ -1,9 +1,10 @@ import './PropertyKeyInfo.scss' + +import { LemonDivider, TooltipProps } from '@posthog/lemon-ui' import clsx from 'clsx' import { Popover } from 'lib/lemon-ui/Popover' import { getKeyMapping, PropertyKey, PropertyType } from 'lib/taxonomy' import React, { useState } from 'react' -import { LemonDivider, TooltipProps } from '@posthog/lemon-ui' interface PropertyKeyInfoProps { value: PropertyKey diff --git a/frontend/src/lib/components/Resizer/Resizer.scss b/frontend/src/lib/components/Resizer/Resizer.scss index 4fa7711683239..52df99380745f 100644 --- a/frontend/src/lib/components/Resizer/Resizer.scss +++ b/frontend/src/lib/components/Resizer/Resizer.scss @@ -1,5 +1,6 @@ .Resizer { --resizer-width: 8px; + position: absolute; top: 0; bottom: 0; @@ -17,8 +18,8 @@ .Resizer__handle { position: absolute; left: calc(var(--resizer-width) / 2); - top: 0px; - bottom: 0px; + top: 0; + bottom: 0; width: 1px; &::before, @@ -35,6 +36,7 @@ transition: 100ms ease transform; background: var(--border); } + &::after { transition: 100ms ease transform; background: var(--text-3000); @@ -43,19 +45,20 @@ } &--left { - left: 0px; + left: 0; transform: translateX(calc(var(--resizer-width) / 2 * -1)); } &--right { - right: 0px; - transform: translateX(calc(var(--resizer-width) / 2 * +1)); + right: 0; + transform: translateX(calc(var(--resizer-width) / 2 * 1)); } &:hover .Resizer__handle::after, &--resizing .Resizer__handle::after { opacity: 0.25; } + &--resizing .Resizer__handle::before, &--resizing .Resizer__handle::after { transform: scaleX(3); diff --git a/frontend/src/lib/components/Resizer/Resizer.tsx b/frontend/src/lib/components/Resizer/Resizer.tsx index 7337be5e2a493..781592aae34bf 100644 --- a/frontend/src/lib/components/Resizer/Resizer.tsx +++ b/frontend/src/lib/components/Resizer/Resizer.tsx @@ -1,9 +1,11 @@ -import { useActions, useValues } from 'kea' import './Resizer.scss' -import { ResizerLogicProps, resizerLogic } from './resizerLogic' + import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { useEffect, useState } from 'react' +import { resizerLogic, ResizerLogicProps } from './resizerLogic' + export type ResizerProps = ResizerLogicProps & { offset?: number | string } diff --git a/frontend/src/lib/components/Resizer/resizerLogic.ts b/frontend/src/lib/components/Resizer/resizerLogic.ts index 4cf16cdf9fb5f..5d4992ac05586 100644 --- a/frontend/src/lib/components/Resizer/resizerLogic.ts +++ b/frontend/src/lib/components/Resizer/resizerLogic.ts @@ -1,7 +1,7 @@ import { actions, beforeUnmount, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import posthog from 'posthog-js' import type { resizerLogicType } from './resizerLogicType' -import posthog from 'posthog-js' export type ResizerEvent = { originX: number diff --git a/frontend/src/lib/components/RestrictedArea.tsx b/frontend/src/lib/components/RestrictedArea.tsx index c334a3a2235e6..636ab88b06d08 100644 --- a/frontend/src/lib/components/RestrictedArea.tsx +++ b/frontend/src/lib/components/RestrictedArea.tsx @@ -1,10 +1,11 @@ import { useValues } from 'kea' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { useMemo } from 'react' + import { organizationLogic } from '../../scenes/organizationLogic' -import { OrganizationMembershipLevel } from '../constants' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { EitherMembershipLevel, membershipLevelToName } from '../utils/permissioning' import { isAuthenticatedTeam, teamLogic } from '../../scenes/teamLogic' +import { EitherMembershipLevel, OrganizationMembershipLevel } from '../constants' +import { membershipLevelToName } from '../utils/permissioning' export interface RestrictedComponentProps { isRestricted: boolean diff --git a/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceModal.tsx b/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceModal.tsx index d74b7dd7abbca..e6e7aa1e9919f 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceModal.tsx +++ b/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceModal.tsx @@ -1,16 +1,18 @@ +import { LemonDivider, LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { dashboardsModel } from '~/models/dashboardsModel' +import { SceneIcon } from 'lib/components/SceneDashboardChoice/SceneIcon' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' + +import { dashboardsModel } from '~/models/dashboardsModel' + import { sceneDashboardChoiceModalLogic, SceneDashboardChoiceModalProps, sceneDescription, } from './sceneDashboardChoiceModalLogic' -import { LemonRow } from 'lib/lemon-ui/LemonRow' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonDivider, LemonInput } from '@posthog/lemon-ui' -import { SceneIcon } from 'lib/components/SceneDashboardChoice/SceneIcon' export function SceneDashboardChoiceModal({ scene }: SceneDashboardChoiceModalProps): JSX.Element { const modalLogic = sceneDashboardChoiceModalLogic({ scene }) diff --git a/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired.tsx b/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired.tsx index efcc813b93cb8..2789d3b4f68b1 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired.tsx +++ b/frontend/src/lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired.tsx @@ -1,10 +1,10 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { DashboardCompatibleScenes, sceneDescription, } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' -import { Scene } from 'scenes/sceneTypes' import { SceneIcon } from 'lib/components/SceneDashboardChoice/SceneIcon' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Scene } from 'scenes/sceneTypes' export function SceneDashboardChoiceRequired(props: { open: () => void diff --git a/frontend/src/lib/components/SceneDashboardChoice/SceneIcon.tsx b/frontend/src/lib/components/SceneDashboardChoice/SceneIcon.tsx index 50283fe1d4239..7cedf11ddd26c 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/SceneIcon.tsx +++ b/frontend/src/lib/components/SceneDashboardChoice/SceneIcon.tsx @@ -1,7 +1,7 @@ +import clsx from 'clsx' import { DashboardCompatibleScenes } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' -import { Scene } from 'scenes/sceneTypes' import { IconCottage, IconPerson } from 'lib/lemon-ui/icons' -import clsx from 'clsx' +import { Scene } from 'scenes/sceneTypes' export function SceneIcon(props: { scene: DashboardCompatibleScenes; size: 'small' | 'large' }): JSX.Element | null { const className = clsx('text-warning', props.size === 'small' ? 'text-lg' : 'text-3xl') diff --git a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts index 88182d3131a78..6e7df822c8b0a 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts +++ b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts @@ -1,11 +1,13 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { sceneDashboardChoiceModalLogic } from './sceneDashboardChoiceModalLogic' import { MOCK_DEFAULT_TEAM, MOCK_DEFAULT_USER } from 'lib/api.mock' +import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' + import { useMocks } from '~/mocks/jest' -import { Scene } from 'scenes/sceneTypes' +import { initKeaTests } from '~/test/init' + +import { sceneDashboardChoiceModalLogic } from './sceneDashboardChoiceModalLogic' describe('sceneDashboardChoiceModalLogic', () => { let logic: ReturnType diff --git a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.ts b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.ts index 14925af1eae5d..171a80749d209 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.ts +++ b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.ts @@ -1,13 +1,14 @@ import Fuse from 'fuse.js' -import { actions, connect, kea, key, listeners, reducers, selectors, path, props } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from 'scenes/teamLogic' -import { dashboardsModel } from '~/models/dashboardsModel' import { posthog } from 'posthog-js' import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + +import { dashboardsModel } from '~/models/dashboardsModel' import type { sceneDashboardChoiceModalLogicType } from './sceneDashboardChoiceModalLogicType' -import { userLogic } from 'scenes/userLogic' export type DashboardCompatibleScenes = Scene.ProjectHomepage | Scene.Person | Scene.Group diff --git a/frontend/src/lib/components/SelectGradientOverflow.scss b/frontend/src/lib/components/SelectGradientOverflow.scss index c1c81e4405e4f..c583519744598 100644 --- a/frontend/src/lib/components/SelectGradientOverflow.scss +++ b/frontend/src/lib/components/SelectGradientOverflow.scss @@ -3,10 +3,12 @@ .ant-select-dropdown { .scrollable-above::after { @extend %mixin-gradient-overlay; - background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + + background: linear-gradient(to bottom, rgb(255 255 255 / 100%), rgb(255 255 255 / 0%)); bottom: unset; top: 0; } + .scrollable-below::after { @extend %mixin-gradient-overlay; } @@ -17,9 +19,7 @@ margin: 1px 4px 1px 0; display: flex; align-items: center; - flex-basis: auto; - flex-grow: 0; - flex-shrink: 0; + flex: 0 0 auto; overflow: hidden; padding: 0 4px 0 8px; font-size: inherit; @@ -27,17 +27,21 @@ background: #f5f5f5; border: 1px solid #f0f0f0; user-select: none; + .label { overflow: hidden; text-overflow: ellipsis; } + .btn-close { font-size: 10px; margin-left: 4px; + .anticon-close { - color: rgba(0, 0, 0, 0.45); + color: rgb(0 0 0 / 45%); + & :hover { - color: rgba(0, 0, 0, 0.75); + color: rgb(0 0 0 / 75%); } } } diff --git a/frontend/src/lib/components/SelectGradientOverflow.tsx b/frontend/src/lib/components/SelectGradientOverflow.tsx index 1623c08976ae3..6a562bb85b59f 100644 --- a/frontend/src/lib/components/SelectGradientOverflow.tsx +++ b/frontend/src/lib/components/SelectGradientOverflow.tsx @@ -1,15 +1,18 @@ +import './SelectGradientOverflow.scss' + // eslint-disable-next-line no-restricted-imports import { LoadingOutlined } from '@ant-design/icons' -import { ReactElement, RefObject, useEffect, useRef, useState } from 'react' import { ConfigProvider, Empty, Select, Tag } from 'antd' import { RefSelectProps, SelectProps } from 'antd/lib/select' -import { CloseButton } from './CloseButton' -import { ANTD_TOOLTIP_PLACEMENTS, toString } from 'lib/utils' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import './SelectGradientOverflow.scss' import { useValues } from 'kea' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { ANTD_TOOLTIP_PLACEMENTS, toString } from 'lib/utils' +import { ReactElement, RefObject, useEffect, useRef, useState } from 'react' + import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { CloseButton } from './CloseButton' + interface DropdownGradientRendererProps { updateScrollGradient: () => void innerRef: RefObject diff --git a/frontend/src/lib/components/SeriesGlyph.tsx b/frontend/src/lib/components/SeriesGlyph.tsx index 156ebcf5f367b..9e0cd0c2f5b6f 100644 --- a/frontend/src/lib/components/SeriesGlyph.tsx +++ b/frontend/src/lib/components/SeriesGlyph.tsx @@ -1,5 +1,8 @@ +import { useValues } from 'kea' import { getSeriesColor } from 'lib/colors' -import { alphabet, hexToRGBA } from 'lib/utils' +import { alphabet, hexToRGBA, lightenDarkenColor, RGBToRGBA } from 'lib/utils' + +import { themeLogic } from '~/layout/navigation-3000/themeLogic' interface SeriesGlyphProps { className?: string @@ -26,6 +29,7 @@ interface SeriesLetterProps { export function SeriesLetter({ className, hasBreakdown, seriesIndex, seriesColor }: SeriesLetterProps): JSX.Element { const color = seriesColor || getSeriesColor(seriesIndex) + const { isDarkModeOn } = useValues(themeLogic) return ( = { id: 123, @@ -62,7 +64,7 @@ const Template = (args: Partial & { licensed?: boolean }): JS created_at: '2022-06-28T12:30:51.459746Z', enabled: true, access_token: '1AEQjQ2xNLGoiyI0UnNlLzOiBZWWMQ', - ...(req.body as any), + ...req.body, }, ] }, diff --git a/frontend/src/lib/components/Sharing/SharingModal.tsx b/frontend/src/lib/components/Sharing/SharingModal.tsx index 446ac5065df42..587492080c504 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.tsx @@ -1,21 +1,24 @@ -import { useEffect, useState } from 'react' -import { InsightModel, InsightShortId, InsightType } from '~/types' -import { useActions, useValues } from 'kea' -import { sharingLogic } from './sharingLogic' -import { LemonButton, LemonSwitch } from '@posthog/lemon-ui' -import { copyToClipboard } from 'lib/utils' -import { IconGlobeLock, IconInfo, IconLink, IconLock, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { DashboardCollaboration } from 'scenes/dashboard/DashboardCollaborators' -import { Field } from 'lib/forms/Field' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import './SharingModal.scss' + +import { LemonButton, LemonSwitch } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { TitleWithIcon } from 'lib/components/TitleWithIcon' +import { Field } from 'lib/forms/Field' +import { IconGlobeLock, IconInfo, IconLink, IconLock, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { useEffect, useState } from 'react' +import { DashboardCollaboration } from 'scenes/dashboard/DashboardCollaborators' + +import { InsightModel, InsightShortId, InsightType } from '~/types' + +import { sharingLogic } from './sharingLogic' export const SHARING_MODAL_WIDTH = 600 @@ -111,7 +114,7 @@ export function SharingModalContent({ await copyToClipboard(shareLink, 'link')} + onClick={() => void copyToClipboard(shareLink, 'link')} icon={} > Copy public link diff --git a/frontend/src/lib/components/Sharing/sharingLogic.ts b/frontend/src/lib/components/Sharing/sharingLogic.ts index d480fabeb8c7a..82898e841f40f 100644 --- a/frontend/src/lib/components/Sharing/sharingLogic.ts +++ b/frontend/src/lib/components/Sharing/sharingLogic.ts @@ -1,18 +1,18 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { AvailableFeature, InsightShortId, SharingConfigurationType } from '~/types' - -import api from 'lib/api' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { getInsightId } from 'scenes/insights/utils' - -import type { sharingLogicType } from './sharingLogicType' -import { ExportOptions } from '~/exporter/types' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { forms } from 'kea-forms' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' + +import { ExportOptions } from '~/exporter/types' import { dashboardsModel } from '~/models/dashboardsModel' +import { AvailableFeature, InsightShortId, SharingConfigurationType } from '~/types' + +import type { sharingLogicType } from './sharingLogicType' export interface SharingLogicProps { dashboardId?: number diff --git a/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx b/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx index d21d6a9bb6808..ad286251e8f20 100644 --- a/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx +++ b/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx @@ -1,11 +1,12 @@ // eslint-disable-next-line no-restricted-imports import { FundOutlined } from '@ant-design/icons' -import { smoothingOptions } from './smoothings' +import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' -import { LemonSelect } from '@posthog/lemon-ui' + +import { smoothingOptions } from './smoothings' export function SmoothingFilter(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx b/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx index 04789b93cbb35..2f90e0ea44f63 100644 --- a/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx +++ b/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx @@ -1,12 +1,15 @@ -import { useValues } from 'kea' import clsx from 'clsx' -import { SocialLoginIcon } from './SocialLoginIcon' -import { SSOProvider } from '~/types' +import { useValues } from 'kea' +import { combineUrl, router } from 'kea-router' import { SSO_PROVIDER_NAMES } from 'lib/constants' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { router, combineUrl } from 'kea-router' +import { useButtonStyle } from 'scenes/authentication/useButtonStyles' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { SSOProvider } from '~/types' + +import { SocialLoginIcon } from './SocialLoginIcon' interface SocialLoginLinkProps { provider: SSOProvider @@ -113,6 +116,7 @@ interface SSOEnforcedLoginButtonProps { } export function SSOEnforcedLoginButton({ provider, email }: SSOEnforcedLoginButtonProps): JSX.Element { + const buttonStyles = useButtonStyle() return ( Log in with {SSO_PROVIDER_NAMES[provider]} diff --git a/frontend/src/lib/components/SocialLoginButton/SocialLoginIcon.tsx b/frontend/src/lib/components/SocialLoginButton/SocialLoginIcon.tsx index 6c1a905c40e35..dd64a8a1d9dce 100644 --- a/frontend/src/lib/components/SocialLoginButton/SocialLoginIcon.tsx +++ b/frontend/src/lib/components/SocialLoginButton/SocialLoginIcon.tsx @@ -1,4 +1,5 @@ import { GithubIcon, GitlabIcon, GoogleIcon, IconKey } from 'lib/lemon-ui/icons' + import { SSOProvider } from '~/types' export const SocialLoginIcon = (provider: SSOProvider): JSX.Element | undefined => { diff --git a/frontend/src/lib/components/StickyView/StickyView.tsx b/frontend/src/lib/components/StickyView/StickyView.tsx index a411236d10852..4aaf4bf2c7fed 100644 --- a/frontend/src/lib/components/StickyView/StickyView.tsx +++ b/frontend/src/lib/components/StickyView/StickyView.tsx @@ -1,6 +1,7 @@ +import './StickyView.scss' + import { useResizeObserver } from 'lib/hooks/useResizeObserver' import React, { useEffect, useRef, useState } from 'react' -import './StickyView.scss' export interface StickyViewProps { children: React.ReactNode diff --git a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx index c83ce034536d1..007b340af7d97 100644 --- a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx +++ b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.stories.tsx @@ -1,13 +1,15 @@ -import { useRef, useState } from 'react' import { Meta } from '@storybook/react' -import { SubscriptionsModal, SubscriptionsModalProps } from './SubscriptionsModal' -import { AvailableFeature, InsightShortId, Realm } from '~/types' -import preflightJson from '~/mocks/fixtures/_preflight.json' -import { useAvailableFeatures } from '~/mocks/features' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { uuid } from 'lib/utils' +import { useRef, useState } from 'react' + import { useStorybookMocks } from '~/mocks/browser' -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useAvailableFeatures } from '~/mocks/features' +import preflightJson from '~/mocks/fixtures/_preflight.json' import { createMockSubscription, mockIntegration, mockSlackChannels } from '~/test/mocks' +import { AvailableFeature, InsightShortId, Realm } from '~/types' + +import { SubscriptionsModal, SubscriptionsModalProps } from './SubscriptionsModal' const meta: Meta = { title: 'Components/Subscriptions', diff --git a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.tsx b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.tsx index 0b9c2af78effa..25ba79553f949 100644 --- a/frontend/src/lib/components/Subscriptions/SubscriptionsModal.tsx +++ b/frontend/src/lib/components/Subscriptions/SubscriptionsModal.tsx @@ -1,14 +1,16 @@ -import { ManageSubscriptions } from './views/ManageSubscriptions' -import { EditSubscription } from './views/EditSubscription' +import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { router } from 'kea-router' -import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' -import { SubscriptionBaseProps, urlForSubscription, urlForSubscriptions } from './utils' -import { PayGatePage } from '../PayGatePage/PayGatePage' -import { AvailableFeature } from '~/types' -import { userLogic } from 'scenes/userLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature } from '~/types' + +import { PayGatePage } from '../PayGatePage/PayGatePage' +import { SubscriptionBaseProps, urlForSubscription, urlForSubscriptions } from './utils' +import { EditSubscription } from './views/EditSubscription' +import { ManageSubscriptions } from './views/ManageSubscriptions' export interface SubscriptionsModalProps extends SubscriptionBaseProps { isOpen: boolean diff --git a/frontend/src/lib/components/Subscriptions/subscriptionLogic.test.ts b/frontend/src/lib/components/Subscriptions/subscriptionLogic.test.ts index a362f3dd7c5ad..820e8eb7d9786 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionLogic.test.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionLogic.test.ts @@ -1,9 +1,11 @@ +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' import { InsightShortId, SubscriptionType } from '~/types' + import { subscriptionLogic } from './subscriptionLogic' -import { router } from 'kea-router' const Insight1 = '1' as InsightShortId diff --git a/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts index 87c84e204c037..d6b65209c1e9b 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts @@ -1,20 +1,19 @@ import { connect, kea, key, listeners, path, props } from 'kea' -import { SubscriptionType } from '~/types' - -import api from 'lib/api' -import { loaders } from 'kea-loaders' import { forms } from 'kea-forms' - -import { isEmail, isURL } from 'lib/utils' +import { loaders } from 'kea-loaders' +import { beforeUnload, router, urlToAction } from 'kea-router' +import api from 'lib/api' import { dayjs } from 'lib/dayjs' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { beforeUnload, router, urlToAction } from 'kea-router' -import { subscriptionsLogic } from './subscriptionsLogic' +import { isEmail, isURL } from 'lib/utils' +import { getInsightId } from 'scenes/insights/utils' +import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' + +import { SubscriptionType } from '~/types' import type { subscriptionLogicType } from './subscriptionLogicType' -import { getInsightId } from 'scenes/insights/utils' +import { subscriptionsLogic } from './subscriptionsLogic' import { SubscriptionBaseProps, urlForSubscription } from './utils' -import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' const NEW_SUBSCRIPTION: Partial = { frequency: 'weekly', diff --git a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts index cfbc33b75fe4f..2cfbe7207bf52 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.test.ts @@ -1,7 +1,7 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' + import { useMocks } from '~/mocks/jest' -import { subscriptionsLogic } from './subscriptionsLogic' +import { initKeaTests } from '~/test/init' import { FilterType, InsightModel, @@ -12,6 +12,8 @@ import { SubscriptionType, } from '~/types' +import { subscriptionsLogic } from './subscriptionsLogic' + const Insight1 = '1' as InsightShortId const Insight2 = '2' as InsightShortId diff --git a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts index 87082bfbbc6d3..6c7ebab34f5d6 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts @@ -1,13 +1,12 @@ import { actions, afterMount, BreakPointFunction, kea, key, listeners, path, props, reducers } from 'kea' -import { SubscriptionType } from '~/types' - -import api from 'lib/api' import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { getInsightId } from 'scenes/insights/utils' -import { deleteWithUndo } from 'lib/utils' +import { SubscriptionType } from '~/types' import type { subscriptionsLogicType } from './subscriptionsLogicType' -import { getInsightId } from 'scenes/insights/utils' import { SubscriptionBaseProps } from './utils' export const subscriptionsLogic = kea([ @@ -48,8 +47,8 @@ export const subscriptionsLogic = kea([ }), listeners(({ actions }) => ({ - deleteSubscription: ({ id }) => { - deleteWithUndo({ + deleteSubscription: async ({ id }) => { + await deleteWithUndo({ endpoint: api.subscriptions.determineDeleteEndpoint(), object: { name: 'Subscription', id }, callback: () => actions.loadSubscriptions(), diff --git a/frontend/src/lib/components/Subscriptions/utils.tsx b/frontend/src/lib/components/Subscriptions/utils.tsx index 9ba2054c86d03..c058d119ff90b 100644 --- a/frontend/src/lib/components/Subscriptions/utils.tsx +++ b/frontend/src/lib/components/Subscriptions/utils.tsx @@ -1,9 +1,10 @@ import { LemonSelectOptions } from '@posthog/lemon-ui' +import { IconMail, IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons' +import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { range } from 'lib/utils' import { urls } from 'scenes/urls' + import { InsightShortId, SlackChannelType } from '~/types' -import { IconMail, IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons' -import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' export interface SubscriptionBaseProps { dashboardId?: number diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index 1926ad2b7fb12..53beeb522951d 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -1,20 +1,33 @@ -import { useEffect, useMemo } from 'react' +import { LemonDivider, LemonInput, LemonTextArea, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +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 { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { + LemonSelectMultiple, + LemonSelectMultipleOptionItem, +} from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { useEffect, useMemo } from 'react' import { membersLogic } from 'scenes/organization/membersLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { Field } from 'lib/forms/Field' -import { dayjs } from 'lib/dayjs' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' +import { urls } from 'scenes/urls' + import { subscriptionLogic } from '../subscriptionLogic' -import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' -import { IconChevronLeft } from 'lib/lemon-ui/icons' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { subscriptionsLogic } from '../subscriptionsLogic' import { bysetposOptions, - frequencyOptionsSingular, frequencyOptionsPlural, + frequencyOptionsSingular, getSlackChannelOptions, intervalOptions, monthlyWeekdayOptions, @@ -23,18 +36,6 @@ import { timeOptions, weekdayOptions, } from '../utils' -import { LemonDivider, LemonInput, LemonTextArea, Link } from '@posthog/lemon-ui' -import { - LemonSelectMultiple, - LemonSelectMultipleOptionItem, -} from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' -import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' -import { urls } from 'scenes/urls' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { Form } from 'kea-forms' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' interface EditSubscriptionProps extends SubscriptionBaseProps { id: number | 'new' @@ -119,11 +120,11 @@ export function EditSubscription({ {!subscription ? ( subscriptionLoading ? (
    - + - + - +
    ) : ( diff --git a/frontend/src/lib/components/Subscriptions/views/ManageSubscriptions.tsx b/frontend/src/lib/components/Subscriptions/views/ManageSubscriptions.tsx index 6707c6a097c55..b38b9f33977c2 100644 --- a/frontend/src/lib/components/Subscriptions/views/ManageSubscriptions.tsx +++ b/frontend/src/lib/components/Subscriptions/views/ManageSubscriptions.tsx @@ -1,13 +1,15 @@ import { useActions, useValues } from 'kea' -import { LemonButton, LemonButtonWithSideAction } from 'lib/lemon-ui/LemonButton' -import { SubscriptionType } from '~/types' -import { capitalizeFirstLetter, pluralize } from 'lib/utils' import { IconEllipsis, IconSlack } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonWithSideAction } from 'lib/lemon-ui/LemonButton' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { ProfileBubbles } from 'lib/lemon-ui/ProfilePicture' +import { capitalizeFirstLetter, pluralize } from 'lib/utils' + +import { SubscriptionType } from '~/types' + import { subscriptionsLogic } from '../subscriptionsLogic' import { SubscriptionBaseProps } from '../utils' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' interface SubscriptionListItemProps { subscription: SubscriptionType @@ -88,7 +90,7 @@ export function ManageSubscriptions({ {subscriptionsLoading && !subscriptions.length ? (
    - +
    ) : subscriptions.length ? ( diff --git a/frontend/src/lib/components/Support/SupportForm.tsx b/frontend/src/lib/components/Support/SupportForm.tsx index fa09fd9f1eaa7..c1ef770c9131f 100644 --- a/frontend/src/lib/components/Support/SupportForm.tsx +++ b/frontend/src/lib/components/Support/SupportForm.tsx @@ -1,13 +1,3 @@ -import { useActions, useValues } from 'kea' -import { SupportTicketKind, TARGET_AREA_TO_NAME, supportLogic } from './supportLogic' -import { Form } from 'kea-forms' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect/LemonSelect' -import { Field } from 'lib/forms/Field' -import { IconBugReport, IconFeedback, IconHelpOutline } from 'lib/lemon-ui/icons' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' -import { useRef } from 'react' import { LemonButton, LemonInput, @@ -15,9 +5,20 @@ import { LemonSegmentedButtonOption, lemonToast, } 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 { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect/LemonSelect' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { useRef } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { userLogic } from 'scenes/userLogic' +import { supportLogic, SupportTicketKind, TARGET_AREA_TO_NAME } from './supportLogic' + const SUPPORT_TICKET_OPTIONS: LemonSegmentedButtonOption[] = [ { value: 'support', diff --git a/frontend/src/lib/components/Support/SupportModal.tsx b/frontend/src/lib/components/Support/SupportModal.tsx index 6a963ae6b04e9..a605c82213cb8 100644 --- a/frontend/src/lib/components/Support/SupportModal.tsx +++ b/frontend/src/lib/components/Support/SupportModal.tsx @@ -1,9 +1,10 @@ import { useActions, useValues } from 'kea' -import { supportLogic } from './supportLogic' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { LemonModal } from 'lib/lemon-ui/LemonModal/LemonModal' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { SupportForm, SupportFormButtons } from './SupportForm' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { supportLogic } from './supportLogic' export function SupportModal({ loggedIn = true }: { loggedIn?: boolean }): JSX.Element | null { const { sendSupportRequest, isSupportFormOpen, sendSupportLoggedOutRequest, title } = useValues(supportLogic) diff --git a/frontend/src/lib/components/Support/supportLogic.ts b/frontend/src/lib/components/Support/supportLogic.ts index af00389dcf7bf..076e44e0ffbd9 100644 --- a/frontend/src/lib/components/Support/supportLogic.ts +++ b/frontend/src/lib/components/Support/supportLogic.ts @@ -1,18 +1,19 @@ +import { captureException } from '@sentry/react' +import * as Sentry from '@sentry/react' import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' -import { userLogic } from 'scenes/userLogic' - -import type { supportLogicType } from './supportLogicType' import { forms } from 'kea-forms' -import { Region, SidePanelTab, TeamType, UserType } from '~/types' +import { actionToUrl, router, urlToAction } from 'kea-router' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { uuid } from 'lib/utils' import posthog from 'posthog-js' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { actionToUrl, router, urlToAction } from 'kea-router' -import { captureException } from '@sentry/react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { teamLogic } from 'scenes/teamLogic' -import * as Sentry from '@sentry/react' +import { userLogic } from 'scenes/userLogic' + import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' +import { Region, SidePanelTab, TeamType, UserType } from '~/types' + +import type { supportLogicType } from './supportLogicType' function getSessionReplayLink(): string { const link = posthog diff --git a/frontend/src/lib/components/TZLabel/index.tsx b/frontend/src/lib/components/TZLabel/index.tsx index ed6c3b4ac5bbb..2e4c1dcb3c3c9 100644 --- a/frontend/src/lib/components/TZLabel/index.tsx +++ b/frontend/src/lib/components/TZLabel/index.tsx @@ -1,17 +1,19 @@ import './index.scss' -import { useActions, useValues } from 'kea' + // eslint-disable-next-line no-restricted-imports -import { ProjectOutlined, LaptopOutlined } from '@ant-design/icons' +import { LaptopOutlined, ProjectOutlined } from '@ant-design/icons' +import { LemonButton, LemonDivider, LemonDropdown, LemonDropdownProps } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { dayjs } from 'lib/dayjs' +import { IconSettings, IconWeb } from 'lib/lemon-ui/icons' import { humanFriendlyDetailedTime, shortTimeZone } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from '../../../scenes/teamLogic' -import { dayjs } from 'lib/dayjs' -import clsx from 'clsx' import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { LemonButton, LemonDivider, LemonDropdown, LemonDropdownProps } from '@posthog/lemon-ui' -import { IconSettings, IconWeb } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' +import { teamLogic } from '../../../scenes/teamLogic' + const BASE_OUTPUT_FORMAT = 'ddd, MMM D, YYYY h:mm A' const BASE_OUTPUT_FORMAT_WITH_SECONDS = 'ddd, MMM D, YYYY h:mm:ss A' diff --git a/frontend/src/lib/components/Table/Table.tsx b/frontend/src/lib/components/Table/Table.tsx index bc8a41e524eec..7cf69790d2763 100644 --- a/frontend/src/lib/components/Table/Table.tsx +++ b/frontend/src/lib/components/Table/Table.tsx @@ -1,10 +1,11 @@ -import { uniqueBy } from 'lib/utils' +import { ColumnType } from 'antd/lib/table' import { useValues } from 'kea' -import { userLogic } from 'scenes/userLogic' -import { TZLabel } from '../TZLabel' import { normalizeColumnTitle } from 'lib/components/Table/utils' -import { ColumnType } from 'antd/lib/table' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { uniqueBy } from 'lib/utils' +import { userLogic } from 'scenes/userLogic' + +import { TZLabel } from '../TZLabel' export function createdAtColumn = Record>(): ColumnType { return { diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss index 227fbe2346059..97c2e739162ac 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.scss @@ -48,7 +48,7 @@ } &.taxonomy-icon-built-in { - color: var(--primary); + color: var(--primary-3000); } } } @@ -68,6 +68,10 @@ &.hover { background-color: var(--primary-bg-hover); border-radius: var(--radius); + + .posthog-3000 & { + background-color: var(--bg-3000); + } } &.selected { @@ -82,7 +86,7 @@ } &.expand-row { - color: var(--primary); + color: var(--primary-3000); } } } diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx index 408f7fc7e926b..643009749e856 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx @@ -1,28 +1,31 @@ import './InfiniteList.scss' import '../../lemon-ui/Popover/Popover.scss' + +import { LemonTag } from '@posthog/lemon-ui' import { Empty } from 'antd' -import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' -import { infiniteListLogic, NO_ITEM_SELECTED } from './infiniteListLogic' +import { ControlledDefinitionPopover } from 'lib/components/DefinitionPopover/DefinitionPopoverContents' +import { definitionPopoverLogic } from 'lib/components/DefinitionPopover/definitionPopoverLogic' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' import { TaxonomicDefinitionTypes, TaxonomicFilterGroup, TaxonomicFilterGroupType, } from 'lib/components/TaxonomicFilter/types' -import { EventDefinition, PropertyDefinition } from '~/types' -import { dayjs } from 'lib/dayjs' import { STALE_EVENT_SECONDS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import clsx from 'clsx' -import { definitionPopoverLogic } from 'lib/components/DefinitionPopover/definitionPopoverLogic' -import { ControlledDefinitionPopover } from 'lib/components/DefinitionPopover/DefinitionPopoverContents' import { pluralize } from 'lib/utils' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { useState } from 'react' -import { LemonTag } from '@posthog/lemon-ui' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List' + +import { EventDefinition, PropertyDefinition } from '~/types' + +import { infiniteListLogic, NO_ITEM_SELECTED } from './infiniteListLogic' export interface InfiniteListProps { popupAnchorElement: HTMLDivElement | null diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx index 777e11cf0c6ff..9aa893e8be895 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx @@ -1,10 +1,11 @@ import { Tag } from 'antd' +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' -import { taxonomicFilterLogic } from './taxonomicFilterLogic' -import { infiniteListLogic } from 'lib/components/TaxonomicFilter/infiniteListLogic' import { InfiniteList } from 'lib/components/TaxonomicFilter/InfiniteList' +import { infiniteListLogic } from 'lib/components/TaxonomicFilter/infiniteListLogic' import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps } from 'lib/components/TaxonomicFilter/types' -import clsx from 'clsx' + +import { taxonomicFilterLogic } from './taxonomicFilterLogic' export interface InfiniteSelectResultsProps { focusInput: () => void diff --git a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx index e7d35c4c18d77..21953476b8a53 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx @@ -1,5 +1,5 @@ -import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' +import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' export interface InlineHogQLEditorProps { value?: TaxonomicFilterValue diff --git a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.scss b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.scss index 11bf7380ce829..108cbcb552950 100644 --- a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.scss +++ b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.scss @@ -5,10 +5,6 @@ display: flex; flex-direction: column; - .posthog-3000 & { - background: var(--bg-3000); - } - &.force-minimum-width { min-width: 300px; } @@ -30,6 +26,7 @@ padding-top: 10px; padding-left: 10px; font-weight: 600; + &.with-border { border-top: 1px solid var(--border-light); } @@ -48,10 +45,25 @@ color: var(--link); background: var(--side); border-color: var(--side); + + .posthog-3000 & { + color: var(--default); + font-weight: 500; + + &:not(.taxonomic-pill-active) { + opacity: 0.7; + } + + &:hover { + opacity: 1; + } + } + &.taxonomic-count-zero { color: var(--muted); cursor: not-allowed; } + &.taxonomic-pill-active { color: #fff; background: var(--link); diff --git a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.stories.tsx b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.stories.tsx index a26af339ab0bc..b6075ccfa8f91 100644 --- a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.stories.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.stories.tsx @@ -1,14 +1,16 @@ -import { TaxonomicFilter } from './TaxonomicFilter' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' +import { Meta, StoryFn } from '@storybook/react' import { useActions, useMountedLogic } from 'kea' -import { actionsModel } from '~/models/actionsModel' +import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { useEffect } from 'react' -import { infiniteListLogic } from './infiniteListLogic' -import { Meta, StoryFn } from '@storybook/react' + import { useAvailableFeatures } from '~/mocks/features' +import { actionsModel } from '~/models/actionsModel' import { AvailableFeature } from '~/types' +import { infiniteListLogic } from './infiniteListLogic' +import { TaxonomicFilter } from './TaxonomicFilter' + const meta: Meta = { title: 'Filters/Taxonomic Filter', component: TaxonomicFilter, diff --git a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx index 9d74f6d1ffb24..0c3b7173e9782 100644 --- a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx @@ -1,17 +1,19 @@ import './TaxonomicFilter.scss' -import { useEffect, useMemo, useRef } from 'react' + +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' -import { InfiniteSelectResults } from './InfiniteSelectResults' -import { taxonomicFilterLogic } from './taxonomicFilterLogic' import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps, TaxonomicFilterProps, } from 'lib/components/TaxonomicFilter/types' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { IconKeyboard } from 'lib/lemon-ui/icons' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import clsx from 'clsx' +import { useEffect, useMemo, useRef } from 'react' + +import { InfiniteSelectResults } from './InfiniteSelectResults' +import { taxonomicFilterLogic } from './taxonomicFilterLogic' let uniqueMemoizedIndex = 0 diff --git a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts index 5ce37452c1e2a..8a524a014d281 100644 --- a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts +++ b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.test.ts @@ -1,11 +1,13 @@ -import { infiniteListLogic } from './infiniteListLogic' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { MOCK_TEAM_ID } from 'lib/api.mock' import { expectLogic, partial } from 'kea-test-utils' +import { MOCK_TEAM_ID } from 'lib/api.mock' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' + +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' import { mockEventDefinitions, mockEventPropertyDefinitions } from '~/test/mocks' import { AppContext, PropertyDefinition } from '~/types' -import { useMocks } from '~/mocks/jest' + +import { infiniteListLogic } from './infiniteListLogic' window.POSTHOG_APP_CONTEXT = { current_team: { id: MOCK_TEAM_ID } } as unknown as AppContext diff --git a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts index 033a3cc08db21..4d0857b0878fc 100644 --- a/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts +++ b/frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts @@ -1,11 +1,9 @@ +import Fuse from 'fuse.js' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' import { combineUrl } from 'kea-router' import api from 'lib/api' -import { RenderedRows } from 'react-virtualized/dist/es/List' -import type { infiniteListLogicType } from './infiniteListLogicType' -import { CohortType, EventDefinition } from '~/types' -import Fuse from 'fuse.js' +import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' import { InfiniteListLogicProps, ListFuse, @@ -15,11 +13,15 @@ import { TaxonomicFilterGroup, TaxonomicFilterGroupType, } from 'lib/components/TaxonomicFilter/types' -import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' -import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { getKeyMapping } from 'lib/taxonomy' +import { RenderedRows } from 'react-virtualized/dist/es/List' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' + +import { CohortType, EventDefinition } from '~/types' + import { teamLogic } from '../../../scenes/teamLogic' import { captureTimeToSeeData } from '../../internalMetrics' +import type { infiniteListLogicType } from './infiniteListLogicType' /* by default the pop-up starts open for the first item in the list diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts index 4237bea8c6453..2c6f0ff84c2db 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.test.ts @@ -1,14 +1,16 @@ -import { infiniteListLogic } from './infiniteListLogic' -import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps } from 'lib/components/TaxonomicFilter/types' -import { MOCK_TEAM_ID } from 'lib/api.mock' import { expectLogic } from 'kea-test-utils' +import { MOCK_TEAM_ID } from 'lib/api.mock' +import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' +import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps } from 'lib/components/TaxonomicFilter/types' + +import { useMocks } from '~/mocks/jest' +import { actionsModel } from '~/models/actionsModel' +import { groupsModel } from '~/models/groupsModel' import { initKeaTests } from '~/test/init' import { mockEventDefinitions } from '~/test/mocks' import { AppContext } from '~/types' -import { taxonomicFilterLogic } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' -import { groupsModel } from '~/models/groupsModel' -import { actionsModel } from '~/models/actionsModel' -import { useMocks } from '~/mocks/jest' + +import { infiniteListLogic } from './infiniteListLogic' window.POSTHOG_APP_CONTEXT = { current_team: { id: MOCK_TEAM_ID } } as unknown as AppContext diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index 7bcd6833e25ab..1cfba27aa602b 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -1,5 +1,7 @@ -import { BuiltLogic, kea, props, key, path, connect, actions, reducers, selectors, listeners } from 'kea' -import type { taxonomicFilterLogicType } from './taxonomicFilterLogicType' +import { actions, BuiltLogic, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { combineUrl } from 'kea-router' +import { infiniteListLogic } from 'lib/components/TaxonomicFilter/infiniteListLogic' +import { infiniteListLogicType } from 'lib/components/TaxonomicFilter/infiniteListLogicType' import { ListStorage, SimpleOption, @@ -8,41 +10,41 @@ import { TaxonomicFilterLogicProps, TaxonomicFilterValue, } from 'lib/components/TaxonomicFilter/types' -import { infiniteListLogic } from 'lib/components/TaxonomicFilter/infiniteListLogic' +import { IconCohort } from 'lib/lemon-ui/icons' +import { KEY_MAPPING } from 'lib/taxonomy' +import { capitalizeFirstLetter, pluralize, toParams } from 'lib/utils' +import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { teamLogic } from 'scenes/teamLogic' + +import { actionsModel } from '~/models/actionsModel' +import { cohortsModel } from '~/models/cohortsModel' +import { dashboardsModel } from '~/models/dashboardsModel' +import { groupPropertiesModel } from '~/models/groupPropertiesModel' +import { groupsModel } from '~/models/groupsModel' +import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' import { ActionType, CohortType, - EventDefinitionType, DashboardType, EventDefinition, + EventDefinitionType, Experiment, FeatureFlagType, Group, InsightModel, + NotebookType, PersonProperty, PersonType, PluginType, PropertyDefinition, - NotebookType, } from '~/types' -import { cohortsModel } from '~/models/cohortsModel' -import { actionsModel } from '~/models/actionsModel' -import { teamLogic } from 'scenes/teamLogic' -import { groupsModel } from '~/models/groupsModel' -import { groupPropertiesModel } from '~/models/groupPropertiesModel' -import { capitalizeFirstLetter, pluralize, toParams } from 'lib/utils' -import { combineUrl } from 'kea-router' -import { IconCohort } from 'lib/lemon-ui/icons' -import { KEY_MAPPING } from 'lib/taxonomy' -import { getEventDefinitionIcon, getPropertyDefinitionIcon } from 'scenes/data-management/events/DefinitionHeader' -import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' -import { experimentsLogic } from 'scenes/experiments/experimentsLogic' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { dashboardsModel } from '~/models/dashboardsModel' -import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { infiniteListLogicType } from 'lib/components/TaxonomicFilter/infiniteListLogicType' -import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' + import { InlineHogQLEditor } from './InlineHogQLEditor' +import type { taxonomicFilterLogicType } from './taxonomicFilterLogicType' export const eventTaxonomicGroupProps: Pick = { getPopoverHeader: (eventDefinition: EventDefinition): string => { diff --git a/frontend/src/lib/components/TaxonomicFilter/types.ts b/frontend/src/lib/components/TaxonomicFilter/types.ts index 3de701a308c3e..3dbb5c0eabc38 100644 --- a/frontend/src/lib/components/TaxonomicFilter/types.ts +++ b/frontend/src/lib/components/TaxonomicFilter/types.ts @@ -1,6 +1,7 @@ +import Fuse from 'fuse.js' import { LogicWrapper } from 'kea' + import { ActionType, CohortType, EventDefinition, PersonProperty, PropertyDefinition } from '~/types' -import Fuse from 'fuse.js' export interface SimpleOption { name: string diff --git a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.stories.tsx b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.stories.tsx index fb8835de130df..881c45b52bd50 100644 --- a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.stories.tsx +++ b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.stories.tsx @@ -1,11 +1,13 @@ -import { useState } from 'react' -import { TaxonomicPopover, TaxonomicStringPopover } from './TaxonomicPopover' -import { cohortsModel } from '~/models/cohortsModel' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { Meta } from '@storybook/react' import { useMountedLogic } from 'kea' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' -import { Meta } from '@storybook/react' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { useState } from 'react' + +import { cohortsModel } from '~/models/cohortsModel' + +import { TaxonomicPopover, TaxonomicStringPopover } from './TaxonomicPopover' const meta: Meta = { title: 'Filters/TaxonomicPopover', diff --git a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx index abf4589005691..2d0e835d7ef85 100644 --- a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx +++ b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx @@ -1,9 +1,9 @@ import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' -import { useEffect, useState } from 'react' -import { LemonButton, LemonButtonProps, LemonButtonWithSideAction } from 'lib/lemon-ui/LemonButton' import { IconClose } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonProps, LemonButtonWithSideAction } from 'lib/lemon-ui/LemonButton' import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' +import { useEffect, useState } from 'react' export interface TaxonomicPopoverProps extends Omit { diff --git a/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.scss b/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.scss index 4774449703e18..80a1630463217 100644 --- a/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.scss +++ b/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.scss @@ -2,6 +2,7 @@ --timeline-seekbar-thickness: 0.125rem; --timeline-seekbar-arrow-width: 0.5rem; --timeline-seekbar-arrow-height: 0.75rem; + margin: 0.75rem 0.5rem; } @@ -22,14 +23,16 @@ width: fit-content; padding: 0 0.25rem; border-radius: var(--radius); - background: var(--primary); + background: var(--primary-3000); color: var(--bg-light); line-height: 1.25rem; font-size: 0.75rem; font-weight: 500; + &::selection { - background: var(--primary-light); // Default selection background is invisible on primary + background: var(--primary-3000-hover); // Default selection background is invisible on primary } + .Spinner { margin-right: 0.25rem; } @@ -48,6 +51,7 @@ top: 0; left: calc(var(--timeline-seekbar-thickness) * 2); width: calc(100% - var(--timeline-seekbar-arrow-width) - var(--timeline-seekbar-thickness) * 3 - 1.25rem); + .LemonBadge:not(.LemonBadge--active) { // Connect each badge to the line rightward to signal the direction in which the badge is applicable border-right-color: transparent; @@ -61,6 +65,7 @@ left: calc(var(--timeline-seekbar-section-progress-current) - var(--timeline-seekbar-thickness)); width: var(--timeline-seekbar-section-progress-next); border-left: var(--timeline-seekbar-thickness) solid var(--bg-light); + &:last-child { width: calc(var(--timeline-seekbar-section-progress-next) + 1.25rem); } @@ -72,7 +77,7 @@ left: 0; height: var(--timeline-seekbar-thickness); width: calc(100% - var(--timeline-seekbar-arrow-width)); - background: var(--primary); + background: var(--primary-3000); } .TimelineSeekbar__line-start, @@ -80,12 +85,13 @@ position: absolute; top: calc(var(--timeline-seekbar-thickness) / 2 - 0.625rem); height: 1.25rem; + &::before { content: ''; display: block; margin: calc(var(--timeline-seekbar-thickness) + 0.125rem) 0; height: var(--timeline-seekbar-arrow-height); - background: var(--primary); + background: var(--primary-3000); } } @@ -98,6 +104,7 @@ left: 100%; width: var(--timeline-seekbar-arrow-width); cursor: pointer; + &::before { clip-path: polygon(0 0, 100% 50%, 0 100%); } diff --git a/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.tsx b/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.tsx index f343686d66a2e..26fc8507bbea4 100644 --- a/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.tsx +++ b/frontend/src/lib/components/TimelineSeekbar/TimelineSeekbar.tsx @@ -1,11 +1,12 @@ +import './TimelineSeekbar.scss' + import { LemonBadge } from '@posthog/lemon-ui' import clsx from 'clsx' import { Dayjs } from 'lib/dayjs' -import { humanFriendlyDetailedTime, pluralize } from 'lib/utils' -import { AlignType } from 'rc-trigger/lib/interface' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import './TimelineSeekbar.scss' +import { humanFriendlyDetailedTime, pluralize } from 'lib/utils' +import { AlignType } from 'rc-trigger/lib/interface' export interface TimelinePoint { timestamp: Dayjs diff --git a/frontend/src/lib/components/UUIDShortener.tsx b/frontend/src/lib/components/UUIDShortener.tsx index c943725ba270b..133c0eedaf6f3 100644 --- a/frontend/src/lib/components/UUIDShortener.tsx +++ b/frontend/src/lib/components/UUIDShortener.tsx @@ -1,5 +1,5 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' export function truncateUuid(uuid: string): string { // Simple function to truncate a UUID. Useful for more simple displaying but should always be made clear it is truncated. diff --git a/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx b/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx index 35f30b91f3241..8491aafb0c61d 100644 --- a/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx +++ b/frontend/src/lib/components/UnitPicker/CustomUnitModal.tsx @@ -1,10 +1,11 @@ -import { RefCallback, useEffect, useState } from 'react' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { HandleUnitChange } from 'lib/components/UnitPicker/UnitPicker' import { PureField } from 'lib/forms/Field' -import { capitalizeFirstLetter } from 'lib/utils' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { HandleUnitChange } from 'lib/components/UnitPicker/UnitPicker' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { capitalizeFirstLetter } from 'lib/utils' +import { RefCallback, useEffect, useState } from 'react' + import { TrendsFilter } from '~/queries/schema' function chooseFormativeElementValue( diff --git a/frontend/src/lib/components/UnitPicker/UnitPicker.tsx b/frontend/src/lib/components/UnitPicker/UnitPicker.tsx index f9c20b279fed5..79a877f9d316f 100644 --- a/frontend/src/lib/components/UnitPicker/UnitPicker.tsx +++ b/frontend/src/lib/components/UnitPicker/UnitPicker.tsx @@ -1,11 +1,11 @@ -import { AggregationAxisFormat, INSIGHT_UNIT_OPTIONS } from 'scenes/insights/aggregationAxisFormat' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { useMemo, useRef, useState } from 'react' import { useActions, useValues } from 'kea' +import { CustomUnitModal } from 'lib/components/UnitPicker/CustomUnitModal' import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { CustomUnitModal } from 'lib/components/UnitPicker/CustomUnitModal' +import { useMemo, useRef, useState } from 'react' +import { AggregationAxisFormat, INSIGHT_UNIT_OPTIONS } from 'scenes/insights/aggregationAxisFormat' import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss index a135232f9a8a8..64f84a90265df 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearch.scss @@ -4,8 +4,8 @@ max-width: 15rem; height: 100%; cursor: pointer; - transition: 200ms ease margin; + .ant-input-affix-wrapper, input { background: var(--bg-bridge); diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx index cbacb2ef845b3..d7f58130d3c7e 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx @@ -1,10 +1,18 @@ import './UniversalSearch.scss' -import { useState } from 'react' + +import clsx from 'clsx' +import { useMountedLogic, useValues } from 'kea' +import { combineUrl, router } from 'kea-router' +import { useEventListener } from 'lib/hooks/useEventListener' import { LemonButtonWithDropdownProps } from 'lib/lemon-ui/LemonButton' -import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps, TaxonomicFilterValue } from '../TaxonomicFilter/types' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { Popover } from 'lib/lemon-ui/Popover' -import { combineUrl, router } from 'kea-router' +import { useState } from 'react' +import { experimentsLogic } from 'scenes/experiments/experimentsLogic' +import { PluginSelectionType, pluginsLogic } from 'scenes/plugins/pluginsLogic' import { urls } from 'scenes/urls' + +import { navigationLogic } from '~/layout/navigation/navigationLogic' import { ActionType, ChartDisplayType, @@ -17,15 +25,10 @@ import { InsightType, PersonType, } from '~/types' -import { PluginSelectionType, pluginsLogic } from 'scenes/plugins/pluginsLogic' -import clsx from 'clsx' -import { navigationLogic } from '~/layout/navigation/navigationLogic' -import { useMountedLogic, useValues } from 'kea' -import { useEventListener } from 'lib/hooks/useEventListener' -import { taxonomicFilterLogic } from '../TaxonomicFilter/taxonomicFilterLogic' + import { TaxonomicFilter } from '../TaxonomicFilter/TaxonomicFilter' -import { experimentsLogic } from 'scenes/experiments/experimentsLogic' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { taxonomicFilterLogic } from '../TaxonomicFilter/taxonomicFilterLogic' +import { TaxonomicFilterGroupType, TaxonomicFilterLogicProps, TaxonomicFilterValue } from '../TaxonomicFilter/types' export interface UniversalSearchPopoverProps extends Omit { diff --git a/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx b/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx index 6030276e756e9..a01ae79e4c7be 100644 --- a/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx +++ b/frontend/src/lib/components/UserActivityIndicator/UserActivityIndicator.tsx @@ -1,8 +1,11 @@ +import './UserActivityIndicator.scss' + import clsx from 'clsx' -import { UserBasicType } from '~/types' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' + +import { UserBasicType } from '~/types' + import { TZLabel } from '../TZLabel' -import './UserActivityIndicator.scss' export interface UserActivityIndicatorProps { prefix?: string diff --git a/frontend/src/lib/components/UserSelectItem.tsx b/frontend/src/lib/components/UserSelectItem.tsx index 9167d87fe3bc5..0732418c35479 100644 --- a/frontend/src/lib/components/UserSelectItem.tsx +++ b/frontend/src/lib/components/UserSelectItem.tsx @@ -1,7 +1,8 @@ -import { UserBasicType, UserType } from '~/types' import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { UserBasicType, UserType } from '~/types' + export interface UserSelectItemProps { user: UserBasicType | UserType } diff --git a/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx b/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx index dcbd1ca4255d4..38094c08dd21f 100644 --- a/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx +++ b/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx @@ -1,12 +1,17 @@ import { useValues } from 'kea' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' + import { versionCheckerLogic } from './versionCheckerLogic' -export function VersionCheckerBanner(): JSX.Element { +export function VersionCheckerBanner({ minVersionAccepted }: { minVersionAccepted?: string }): JSX.Element { const { versionWarning } = useValues(versionCheckerLogic) - // We don't want to show a message if the diff is too small (we might be still deploying the changes out) - if (!versionWarning || versionWarning.diff < 5) { + if ( + !versionWarning || + (minVersionAccepted && versionWarning.currentVersion + ? versionWarning.currentVersion.localeCompare(minVersionAccepted) >= 0 + : versionWarning.diff < 5) + ) { return <> } diff --git a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts index 22431153935d2..9d3ab79d0d3ce 100644 --- a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts +++ b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.test.ts @@ -1,7 +1,9 @@ +import { expectLogic } from 'kea-test-utils' + import { useMocks } from '~/mocks/jest' -import { SDKVersion, versionCheckerLogic } from './versionCheckerLogic' import { initKeaTests } from '~/test/init' -import { expectLogic } from 'kea-test-utils' + +import { SDKVersion, versionCheckerLogic } from './versionCheckerLogic' const useMockedVersions = (githubVersions: SDKVersion[], usedVersions: SDKVersion[]): void => { useMocks({ diff --git a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts index acc1897bc78f2..875c27ae97a8a 100644 --- a/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts +++ b/frontend/src/lib/components/VersionChecker/versionCheckerLogic.ts @@ -1,6 +1,7 @@ import { actions, afterMount, kea, listeners, path, reducers, sharedListeners } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' + import { HogQLQuery, NodeKind } from '~/queries/schema' import type { versionCheckerLogicType } from './versionCheckerLogicType' diff --git a/frontend/src/lib/components/VisibilitySensor/VisibilitySensor.tsx b/frontend/src/lib/components/VisibilitySensor/VisibilitySensor.tsx index 8ff2d988e37ef..97a3fa0fd5820 100644 --- a/frontend/src/lib/components/VisibilitySensor/VisibilitySensor.tsx +++ b/frontend/src/lib/components/VisibilitySensor/VisibilitySensor.tsx @@ -1,5 +1,6 @@ import { useActions } from 'kea' import { useEffect, useRef } from 'react' + import { visibilitySensorLogic } from './visibilitySensorLogic' interface VisibilityProps { diff --git a/frontend/src/lib/components/VisibilitySensor/visibilitySensorLogic.tsx b/frontend/src/lib/components/VisibilitySensor/visibilitySensorLogic.tsx index ce0bdf419953d..835d9a98df30e 100644 --- a/frontend/src/lib/components/VisibilitySensor/visibilitySensorLogic.tsx +++ b/frontend/src/lib/components/VisibilitySensor/visibilitySensorLogic.tsx @@ -1,5 +1,5 @@ +import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { windowValues } from 'kea-window-values' -import { kea, props, key, path, actions, reducers, selectors, listeners } from 'kea' import type { visibilitySensorLogicType } from './visibilitySensorLogicType' export const visibilitySensorLogic = kea([ diff --git a/frontend/src/lib/components/hedgehogs.stories.tsx b/frontend/src/lib/components/hedgehogs.stories.tsx index 3abe96b5fdb9e..0ae1a23b4e39b 100644 --- a/frontend/src/lib/components/hedgehogs.stories.tsx +++ b/frontend/src/lib/components/hedgehogs.stories.tsx @@ -1,5 +1,6 @@ -import { Meta } from '@storybook/react' import { LemonTable } from '@posthog/lemon-ui' +import { Meta } from '@storybook/react' + import * as hedgehogs from './hedgehogs' interface HedgehogDefinition { @@ -14,8 +15,8 @@ const allHedgehogs: HedgehogDefinition[] = Object.entries(hedgehogs).map(([key, const meta: Meta = { title: 'Lemon UI/Hog illustrations', + tags: ['test-skip', 'autodocs'], // Not valuable to take snapshots of these hedgehogs parameters: { - testOptions: { skip: true }, // Not valuable to take snapshots of these hedgehogs docs: { description: { component: ` @@ -37,7 +38,6 @@ she will get to it dependant on work load. }, }, }, - tags: ['autodocs'], } export default meta export function Library(): JSX.Element { diff --git a/frontend/src/lib/components/hedgehogs.tsx b/frontend/src/lib/components/hedgehogs.tsx index f5b8d0fe64097..ddc41d7bf8c4c 100644 --- a/frontend/src/lib/components/hedgehogs.tsx +++ b/frontend/src/lib/components/hedgehogs.tsx @@ -1,36 +1,36 @@ // Loads custom icons (some icons may come from a third-party library) -import { ImgHTMLAttributes } from 'react' -import surprisedHog from 'public/hedgehog/surprised-hog.png' -import xRayHog from 'public/hedgehog/x-ray-hog.png' -import hospitalHog from 'public/hedgehog/hospital-hog.png' import blushingHog from 'public/hedgehog/blushing-hog.png' -import laptopHog1 from 'public/hedgehog/laptop-hog-01.png' -import laptopHog2 from 'public/hedgehog/laptop-hog-02.png' -import explorerHog from 'public/hedgehog/explorer-hog.png' -import runningHog from 'public/hedgehog/running-hog.png' -import spaceHog from 'public/hedgehog/space-hog.png' -import tronHog from 'public/hedgehog/tron-hog.png' -import heartHog from 'public/hedgehog/heart-hog.png' -import starHog from 'public/hedgehog/star-hog.png' -import policeHog from 'public/hedgehog/police-hog.png' -import sleepingHog from 'public/hedgehog/sleeping-hog.png' import builderHog1 from 'public/hedgehog/builder-hog-01.png' import builderHog2 from 'public/hedgehog/builder-hog-02.png' import builderHog3 from 'public/hedgehog/builder-hog-03.png' -import professorHog from 'public/hedgehog/professor-hog.png' -import supportHeroHog from 'public/hedgehog/support-hero-hog.png' -import xRayHog2 from 'public/hedgehog/x-ray-hogs-02.png' +import detectiveHog from 'public/hedgehog/detective-hog.png' +import experimentsHog from 'public/hedgehog/experiments-hog.png' +import explorerHog from 'public/hedgehog/explorer-hog.png' +import featureFlagHog from 'public/hedgehog/feature-flag-hog.png' +import heartHog from 'public/hedgehog/heart-hog.png' +import hospitalHog from 'public/hedgehog/hospital-hog.png' +import laptopHog1 from 'public/hedgehog/laptop-hog-01.png' +import laptopHog2 from 'public/hedgehog/laptop-hog-02.png' import laptopHog3 from 'public/hedgehog/laptop-hog-03.png' import laptopHog4 from 'public/hedgehog/laptop-hog-04.png' import laptopHogEU from 'public/hedgehog/laptop-hog-eu.png' -import detectiveHog from 'public/hedgehog/detective-hog.png' -import mailHog from 'public/hedgehog/mail-hog.png' -import featureFlagHog from 'public/hedgehog/feature-flag-hog.png' -import experimentsHog from 'public/hedgehog/experiments-hog.png' import listHog from 'public/hedgehog/list-hog.png' -import warningHog from 'public/hedgehog/warning-hog.png' -import readingHog from 'public/hedgehog/reading-hog.png' +import mailHog from 'public/hedgehog/mail-hog.png' import microphoneHog from 'public/hedgehog/microphone-hog.png' +import policeHog from 'public/hedgehog/police-hog.png' +import professorHog from 'public/hedgehog/professor-hog.png' +import readingHog from 'public/hedgehog/reading-hog.png' +import runningHog from 'public/hedgehog/running-hog.png' +import sleepingHog from 'public/hedgehog/sleeping-hog.png' +import spaceHog from 'public/hedgehog/space-hog.png' +import starHog from 'public/hedgehog/star-hog.png' +import supportHeroHog from 'public/hedgehog/support-hero-hog.png' +import surprisedHog from 'public/hedgehog/surprised-hog.png' +import tronHog from 'public/hedgehog/tron-hog.png' +import warningHog from 'public/hedgehog/warning-hog.png' +import xRayHog from 'public/hedgehog/x-ray-hog.png' +import xRayHog2 from 'public/hedgehog/x-ray-hogs-02.png' +import { ImgHTMLAttributes } from 'react' type HedgehogProps = Omit, 'src'> diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 4e12e485db46e..69c5a65aaa2f8 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -1,4 +1,3 @@ -import { urls } from 'scenes/urls' import { AvailableFeature, ChartDisplayType, LicensePlan, Region, SSOProvider } from '../types' /** Display types which don't allow grouping by unit of time. Sync with backend NON_TIME_SERIES_DISPLAY_TYPES. */ @@ -39,6 +38,8 @@ export enum TeamMembershipLevel { Admin = 8, } +export type EitherMembershipLevel = OrganizationMembershipLevel | TeamMembershipLevel + /** See posthog/api/organization.py for details. */ export enum PluginsAccessLevel { None = 0, @@ -136,6 +137,7 @@ export const FEATURE_FLAGS = { ROLE_BASED_ACCESS: 'role-based-access', // owner: #team-experiments, @liyiy QUERY_RUNNING_TIME: 'query_running_time', // owner: @mariusandra QUERY_TIMINGS: 'query-timings', // owner: @mariusandra + QUERY_ASYNC: 'query-async', // owner: @webjunkie POSTHOG_3000: 'posthog-3000', // owner: @Twixes POSTHOG_3000_NAV: 'posthog-3000-nav', // owner: @Twixes ENABLE_PROMPTS: 'enable-prompts', // owner: @lharries @@ -143,7 +145,6 @@ export const FEATURE_FLAGS = { NOTEBOOKS: 'notebooks', // owner: #team-monitoring EARLY_ACCESS_FEATURE_SITE_BUTTON: 'early-access-feature-site-button', // owner: @neilkakkar HEDGEHOG_MODE_DEBUG: 'hedgehog-mode-debug', // owner: @benjackwhite - AUTO_REDIRECT: 'auto-redirect', // owner: @lharries GENERIC_SIGNUP_BENEFITS: 'generic-signup-benefits', // experiment, owner: @raquelmsmith WEB_ANALYTICS: 'web-analytics', // owner @robbie-c #team-web-analytics HIGH_FREQUENCY_BATCH_EXPORTS: 'high-frequency-batch-exports', // owner: @tomasfarias @@ -173,9 +174,10 @@ export const FEATURE_FLAGS = { NOTEBOOK_CANVASES: 'notebook-canvases', // owner: #team-monitoring SESSION_RECORDING_SAMPLING: 'session-recording-sampling', // owner: #team-monitoring PERSON_FEED_CANVAS: 'person-feed-canvas', // owner: #project-canvas - SIDE_PANEL_DOCS: 'side-panel-docs', // owner: #noteforce-3000 MULTI_PROJECT_FEATURE_FLAGS: 'multi-project-feature-flags', // owner: @jurajmajerik #team-feature-success NETWORK_PAYLOAD_CAPTURE: 'network-payload-capture', // owner: #team-monitoring + FEATURE_FLAG_COHORT_CREATION: 'feature-flag-cohort-creation', // owner: @neilkakkar #team-feature-success + INSIGHT_HORIZONTAL_CONTROLS: 'insight-horizontal-controls', // owner: @benjackwhite } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] @@ -239,10 +241,6 @@ export const SSO_PROVIDER_NAMES: Record = { saml: 'Single sign-on (SAML)', } -// TODO: Remove UPGRADE_LINK, as the billing page is now universal -export const UPGRADE_LINK = (cloud?: boolean): { url: string; target?: '_blank' } => - cloud ? { url: urls.organizationBilling() } : { url: 'https://posthog.com/pricing', target: '_blank' } - export const DOMAIN_REGEX = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/ export const SECURE_URL_REGEX = /^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=]+$/gi @@ -257,3 +255,9 @@ export const SESSION_RECORDINGS_PLAYLIST_FREE_COUNT = 5 export const AUTO_REFRESH_DASHBOARD_THRESHOLD_HOURS = 20 export const GENERATED_DASHBOARD_PREFIX = 'Generated Dashboard' + +export const ACTIVITY_PAGE_SIZE = 20 +export const EVENT_DEFINITIONS_PER_PAGE = 50 +export const PROPERTY_DEFINITIONS_PER_EVENT = 5 +export const EVENT_PROPERTY_DEFINITIONS_PER_PAGE = 50 +export const LOGS_PORTION_LIMIT = 50 diff --git a/frontend/src/lib/dayjs.ts b/frontend/src/lib/dayjs.ts index e4f4881dfd390..6dac103d72ff2 100644 --- a/frontend/src/lib/dayjs.ts +++ b/frontend/src/lib/dayjs.ts @@ -1,13 +1,13 @@ // eslint-disable-next-line no-restricted-imports import dayjs, { Dayjs as DayjsOriginal, isDayjs } from 'dayjs' -import LocalizedFormat from 'dayjs/plugin/localizedFormat' -import relativeTime from 'dayjs/plugin/relativeTime' +import duration from 'dayjs/plugin/duration' import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import LocalizedFormat from 'dayjs/plugin/localizedFormat' +import quarterOfYear from 'dayjs/plugin/quarterOfYear' +import relativeTime from 'dayjs/plugin/relativeTime' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import duration from 'dayjs/plugin/duration' -import quarterOfYear from 'dayjs/plugin/quarterOfYear' // necessary for any localized date formatting to work dayjs.extend(LocalizedFormat) @@ -21,7 +21,7 @@ dayjs.extend(quarterOfYear) const now = (): Dayjs => dayjs() -export { dayjs, now, isDayjs } +export { dayjs, isDayjs, now } /** Parse UTC datetime string using Day.js, taking into account time zone conversion edge cases. */ export function dayjsUtcToTimezone( @@ -61,7 +61,6 @@ export function dayjsLocalToTimezone( // We could only use types like "dayjs.OpUnitType", causing errors such as: // error TS2312: An interface can only extend an object type or intersection of object types with statically known members. -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Dayjs extends DayjsOriginal {} export type UnitTypeShort = 'd' | 'D' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms' diff --git a/frontend/src/lib/forms/Field.stories.tsx b/frontend/src/lib/forms/Field.stories.tsx index d5d071ba89d9d..34c229681c114 100644 --- a/frontend/src/lib/forms/Field.stories.tsx +++ b/frontend/src/lib/forms/Field.stories.tsx @@ -1,9 +1,9 @@ -import { Meta } from '@storybook/react' -import { Field, PureField } from './Field' import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, LemonTextArea } from '@posthog/lemon-ui' +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' const meta: Meta = { diff --git a/frontend/src/lib/forms/Field.tsx b/frontend/src/lib/forms/Field.tsx index 6fdc536be363f..b0924ae43ae5b 100644 --- a/frontend/src/lib/forms/Field.tsx +++ b/frontend/src/lib/forms/Field.tsx @@ -1,7 +1,7 @@ +import clsx from 'clsx' +import { Field as KeaField, FieldProps as KeaFieldProps } from 'kea-forms/lib/components' import { IconErrorOutline } from 'lib/lemon-ui/icons' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { Field as KeaField, FieldProps as KeaFieldProps } from 'kea-forms/lib/components' -import clsx from 'clsx' export type PureFieldProps = { /** The label name to be displayed */ diff --git a/frontend/src/lib/hooks/useAsyncHandler.ts b/frontend/src/lib/hooks/useAsyncHandler.ts index 3f0f4717ea7d0..af962ad35a322 100644 --- a/frontend/src/lib/hooks/useAsyncHandler.ts +++ b/frontend/src/lib/hooks/useAsyncHandler.ts @@ -9,7 +9,7 @@ import { useState } from 'react' * return Click me */ export function useAsyncHandler( - onEvent: ((e: E) => any | Promise) | undefined + onEvent: ((e: E) => any) | undefined ): { loading: boolean; onEvent: ((e: E) => void) | undefined } { const [loading, setLoading] = useState(false) @@ -19,7 +19,7 @@ export function useAsyncHandler( const result = onEvent(e) if (result instanceof Promise) { setLoading(true) - result.finally(() => setLoading(false)) + void result.finally(() => setLoading(false)) } } } diff --git a/frontend/src/lib/hooks/useBreakpoint.ts b/frontend/src/lib/hooks/useBreakpoint.ts index 1592c09b6781e..cf01e7d80e925 100644 --- a/frontend/src/lib/hooks/useBreakpoint.ts +++ b/frontend/src/lib/hooks/useBreakpoint.ts @@ -1,6 +1,7 @@ +import { getActiveBreakpointValue } from 'lib/utils/responsiveUtils' import { useEffect, useState } from 'react' + import { useWindowSize } from './useWindowSize' -import { getActiveBreakpointValue } from 'lib/utils/responsiveUtils' export const useBreakpoint = (): number => { const { width } = useWindowSize() diff --git a/frontend/src/lib/hooks/useD3.ts b/frontend/src/lib/hooks/useD3.ts index 19de5fab0171c..8f1dcae4dbc02 100644 --- a/frontend/src/lib/hooks/useD3.ts +++ b/frontend/src/lib/hooks/useD3.ts @@ -1,5 +1,5 @@ -import { MutableRefObject, useEffect, useRef } from 'react' import * as d3 from 'd3' +import { MutableRefObject, useEffect, useRef } from 'react' export type D3Selector = d3.Selection export type D3Transition = d3.Transition diff --git a/frontend/src/lib/hooks/useKeyboardHotkeys.tsx b/frontend/src/lib/hooks/useKeyboardHotkeys.tsx index eb6bf3694132e..d08afd46441b2 100644 --- a/frontend/src/lib/hooks/useKeyboardHotkeys.tsx +++ b/frontend/src/lib/hooks/useKeyboardHotkeys.tsx @@ -1,5 +1,6 @@ import { useEventListener } from 'lib/hooks/useEventListener' import { DependencyList } from 'react' + import { HotKey } from '~/types' export interface HotkeyInterface { diff --git a/frontend/src/lib/hooks/useScrollable.ts b/frontend/src/lib/hooks/useScrollable.ts index 08fe12247725d..2d69b0df6905e 100644 --- a/frontend/src/lib/hooks/useScrollable.ts +++ b/frontend/src/lib/hooks/useScrollable.ts @@ -1,4 +1,5 @@ import { useLayoutEffect, useRef, useState } from 'react' + import { useResizeObserver } from './useResizeObserver' /** Determine whether an element is horizontally scrollable, on the left and on the right respectively. */ diff --git a/frontend/src/lib/hooks/useUploadFiles.ts b/frontend/src/lib/hooks/useUploadFiles.ts index 1b21c2613e3a5..ab4df041ba180 100644 --- a/frontend/src/lib/hooks/useUploadFiles.ts +++ b/frontend/src/lib/hooks/useUploadFiles.ts @@ -1,7 +1,8 @@ -import { MediaUploadResponse } from '~/types' import api from 'lib/api' import { useEffect, useState } from 'react' +import { MediaUploadResponse } from '~/types' + export const lazyImageBlobReducer = async (blob: Blob): Promise => { const blobReducer = (await import('image-blob-reduce')).default() return blobReducer.toBlob(blob, { max: 2000 }) diff --git a/frontend/src/lib/hooks/useWhyDidIRender.ts b/frontend/src/lib/hooks/useWhyDidIRender.ts index 2650dd3d7882d..eb8af54e66c63 100644 --- a/frontend/src/lib/hooks/useWhyDidIRender.ts +++ b/frontend/src/lib/hooks/useWhyDidIRender.ts @@ -1,4 +1,5 @@ import { useMemo, useRef } from 'react' + import { useFeatureFlag } from './useFeatureFlag' export function useWhyDidIRender(name: string, props: Record): void { diff --git a/frontend/src/lib/hooks/useWindowSize.js b/frontend/src/lib/hooks/useWindowSize.js index dc0615e5d9196..d9e137bc1b684 100644 --- a/frontend/src/lib/hooks/useWindowSize.js +++ b/frontend/src/lib/hooks/useWindowSize.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' export function useWindowSize() { const isClient = typeof window === 'object' diff --git a/frontend/src/lib/internalMetrics.ts b/frontend/src/lib/internalMetrics.ts index 417fa56e82a2e..9ae89f02c235d 100644 --- a/frontend/src/lib/internalMetrics.ts +++ b/frontend/src/lib/internalMetrics.ts @@ -1,5 +1,5 @@ -import posthog from 'posthog-js' import api, { getJSONOrThrow } from 'lib/api' +import posthog from 'posthog-js' import { getResponseBytes } from 'scenes/insights/utils' export interface TimeToSeeDataPayload { @@ -58,7 +58,7 @@ export async function apiGetWithTimeToSeeDataTracking( error = e } const requestDurationMs = performance.now() - requestStartMs - captureTimeToSeeData(teamId, { + void captureTimeToSeeData(teamId, { ...timeToSeeDataPayload, api_url: url, status: error ? 'failure' : 'success', diff --git a/frontend/src/lib/introductions/GroupsIntroductionOption.tsx b/frontend/src/lib/introductions/GroupsIntroductionOption.tsx index c2b863cef3154..6050f89941867 100644 --- a/frontend/src/lib/introductions/GroupsIntroductionOption.tsx +++ b/frontend/src/lib/introductions/GroupsIntroductionOption.tsx @@ -1,8 +1,8 @@ import { useValues } from 'kea' -import Select from 'rc-select' -import { Link } from 'lib/lemon-ui/Link' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' import { IconLock } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import Select from 'rc-select' // TODO: Remove, but de-ant FeatureFlagReleaseConditions first export function GroupsIntroductionOption({ value }: { value: any }): JSX.Element | null { diff --git a/frontend/src/lib/introductions/NewFeatureBanner.tsx b/frontend/src/lib/introductions/NewFeatureBanner.tsx index 6b8232e1d0000..da8b63c94028d 100644 --- a/frontend/src/lib/introductions/NewFeatureBanner.tsx +++ b/frontend/src/lib/introductions/NewFeatureBanner.tsx @@ -1,6 +1,6 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Link } from 'lib/lemon-ui/Link' -import { LemonButton } from '@posthog/lemon-ui' import { billingLogic } from 'scenes/billing/billingLogic' export function NewFeatureBanner(): JSX.Element | null { diff --git a/frontend/src/lib/introductions/groupsAccessLogic.ts b/frontend/src/lib/introductions/groupsAccessLogic.ts index 7e6b45edf201f..37bcb2e97972c 100644 --- a/frontend/src/lib/introductions/groupsAccessLogic.ts +++ b/frontend/src/lib/introductions/groupsAccessLogic.ts @@ -1,9 +1,10 @@ -import { kea, path, connect, selectors } from 'kea' -import { AvailableFeature } from '~/types' -import { teamLogic } from 'scenes/teamLogic' +import { connect, kea, path, selectors } from 'kea' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' +import { AvailableFeature } from '~/types' + import type { groupsAccessLogicType } from './groupsAccessLogicType' export enum GroupsAccessStatus { AlreadyUsing, diff --git a/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.scss b/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.scss index 46fc8856b62d6..b01a0c24d3f18 100644 --- a/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.scss +++ b/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.scss @@ -1,58 +1,71 @@ .LemonActionableTooltip { max-width: var(--in-app-prompts-width); padding: 0.5rem; + > * + * { margin-top: 0.5rem; } + .LemonActionableTooltip__header { display: flex; justify-content: space-between; + > * + * { margin-left: 0.5rem; } } + .LemonActionableTooltip__title { font-size: 1.125rem; font-weight: 500; line-height: 1.75rem; } + .LemonActionableTooltip__icon { - color: var(--primary); + color: var(--primary-3000); display: flex; align-items: center; width: 1.5rem; height: 1.5rem; + > svg { width: 100%; height: 100%; } } + .LemonActionableTooltip__body { > * + * { margin-top: 0.5rem; } } + .LemonActionableTooltip__footer { display: flex; justify-content: space-between; margin-top: 1rem; } + .LemonActionableTooltip__url-buttons { display: flex; width: 100%; flex-direction: column; + > * + * { margin-top: 0.25rem; } } + .LemonActionableTooltip__action-buttons { display: flex; width: 100%; flex-direction: column; + > * + * { margin-top: 0.25rem; } } + .LemonActionableTooltip__navigation { color: var(--muted); display: flex; diff --git a/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.tsx b/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.tsx index c9b5f8db5d554..394e9424ddd4a 100644 --- a/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.tsx +++ b/frontend/src/lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip.tsx @@ -1,9 +1,10 @@ +import './LemonActionableTooltip.scss' + import { Placement } from '@floating-ui/react' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { IconOpenInNew } from 'lib/lemon-ui/icons' -import { IconClose, IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' import { LemonButton } from '@posthog/lemon-ui' -import './LemonActionableTooltip.scss' +import { IconOpenInNew } from 'lib/lemon-ui/icons' +import { IconChevronLeft, IconChevronRight, IconClose } from 'lib/lemon-ui/icons' +import { Popover } from 'lib/lemon-ui/Popover/Popover' export type LemonActionableTooltipProps = { title?: string diff --git a/frontend/src/lib/lemon-ui/LemonActionableTooltip/index.ts b/frontend/src/lib/lemon-ui/LemonActionableTooltip/index.ts index 516fcb923abd2..4c3069d89979f 100644 --- a/frontend/src/lib/lemon-ui/LemonActionableTooltip/index.ts +++ b/frontend/src/lib/lemon-ui/LemonActionableTooltip/index.ts @@ -1,2 +1,2 @@ -export { LemonActionableTooltip } from './LemonActionableTooltip' export type { LemonActionableTooltipProps } from './LemonActionableTooltip' +export { LemonActionableTooltip } from './LemonActionableTooltip' diff --git a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.scss b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.scss index f3737f96455cb..adc91cce935f8 100644 --- a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.scss +++ b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.scss @@ -1,5 +1,5 @@ .LemonBadge { - --lemon-badge-color: var(--primary); + --lemon-badge-color: var(--primary-3000); --lemon-badge-size: 1.5rem; --lemon-badge-font-size: 0.75rem; --lemon-badge-position-offset: 0.5rem; @@ -17,6 +17,7 @@ height: var(--lemon-badge-size); font-size: var(--lemon-badge-font-size); line-height: var(--lemon-badge-size); + // Just enough so the overall size is unaffected with a single digit (i.e. badge stays round) padding: calc(var(--lemon-badge-size) / 8); border-radius: calc(var(--lemon-badge-size) / 2); diff --git a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.stories.tsx b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.stories.tsx index 20e40eae0b97c..04eb497c9e940 100644 --- a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonBadge } from './LemonBadge' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconPlusMini } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' + +import { LemonBadge } from './LemonBadge' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.tsx b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.tsx index 75f785f3429b8..cb5af2947a797 100644 --- a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.tsx +++ b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadge.tsx @@ -1,7 +1,8 @@ +import './LemonBadge.scss' + import clsx from 'clsx' import { compactNumber, humanFriendlyNumber } from 'lib/utils' import { CSSTransition } from 'react-transition-group' -import './LemonBadge.scss' interface LemonBadgePropsBase { size?: 'small' | 'medium' | 'large' diff --git a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadgeNumber.stories.tsx b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadgeNumber.stories.tsx index 09b2003aedbf2..92d2884d11a70 100644 --- a/frontend/src/lib/lemon-ui/LemonBadge/LemonBadgeNumber.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonBadge/LemonBadgeNumber.stories.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonBadge, LemonBadgeNumberProps } from './LemonBadge' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useState } from 'react' + +import { LemonBadge, LemonBadgeNumberProps } from './LemonBadge' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonBadge/index.ts b/frontend/src/lib/lemon-ui/LemonBadge/index.ts index 1c064e83a1404..26d5ac39abf75 100644 --- a/frontend/src/lib/lemon-ui/LemonBadge/index.ts +++ b/frontend/src/lib/lemon-ui/LemonBadge/index.ts @@ -1,2 +1,2 @@ -export { LemonBadge } from './LemonBadge' export type { LemonBadgeProps } from './LemonBadge' +export { LemonBadge } from './LemonBadge' diff --git a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss index e165aaa435d4f..9a948c4f24dd3 100644 --- a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss +++ b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss @@ -1,13 +1,14 @@ .LemonBanner { + align-items: center; border-radius: var(--radius); - padding: 0.5rem 0.75rem; + border: solid 1px var(--border-3000); color: var(--primary-alt); - font-weight: 500; display: flex; - align-items: center; - text-align: left; + font-weight: 500; gap: 0.5rem; min-height: 3rem; + padding: 0.5rem 0.75rem; + text-align: left; &.LemonBanner--info { background-color: var(--primary-alt-highlight); diff --git a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.stories.tsx b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.stories.tsx index c4ad6115a0433..4276607b0dc72 100644 --- a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { LemonBanner, LemonBannerProps } from './LemonBanner' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx index 2f7f95cb06409..69e0da9f467af 100644 --- a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx +++ b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.tsx @@ -1,9 +1,11 @@ import './LemonBanner.scss' -import { IconClose, IconInfo, IconWarning } from 'lib/lemon-ui/icons' + import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { IconClose, IconInfo, IconWarning } from 'lib/lemon-ui/icons' import { LemonButton, SideAction } from 'lib/lemon-ui/LemonButton' import { LemonButtonPropsBase } from 'lib/lemon-ui/LemonButton/LemonButton' -import { useActions, useValues } from 'kea' + import { lemonBannerLogic } from './lemonBannerLogic' export type LemonBannerAction = SideAction & Pick diff --git a/frontend/src/lib/lemon-ui/LemonBanner/index.ts b/frontend/src/lib/lemon-ui/LemonBanner/index.ts index a8b9f586f8000..f06472f32fe40 100644 --- a/frontend/src/lib/lemon-ui/LemonBanner/index.ts +++ b/frontend/src/lib/lemon-ui/LemonBanner/index.ts @@ -1,2 +1,2 @@ -export { LemonBanner } from './LemonBanner' export type { LemonBannerProps } from './LemonBanner' +export { LemonBanner } from './LemonBanner' diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss index 6c1b7ea66fb3e..2a23964cfc0f9 100644 --- a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss +++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss @@ -1,26 +1,41 @@ -.LemonButton { - --lemon-button-height: 2.5rem; - position: relative; - transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease, - transform 100ms ease; - display: flex; - flex-direction: row; - flex-shrink: 0; +.LemonButton, +.Link.LemonButton { + // Make sure we override .Link's styles where needed, e.g. padding align-items: center; - justify-content: flex-start; - min-height: var(--lemon-button-height); - padding: 0.25rem 0.75rem; - gap: 0.5rem; + appearance: none !important; // Important as this gets overridden by Ant styles... background: none; border-radius: var(--radius); border: none; + cursor: pointer; + display: flex; + flex-direction: row; + + .posthog-3000 & { + font-family: var(--font-title); + } + + flex-shrink: 0; font-size: 0.875rem; - text-align: left; - line-height: 1.5rem; font-weight: 500; - cursor: pointer; + gap: 0.5rem; + justify-content: flex-start; + line-height: 1.5rem; + padding: 0.25rem 0.75rem; + position: relative; + text-align: left; + transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease, + transform 100ms ease; user-select: none; - -webkit-appearance: none !important; // Important as this gets overridden by Ant styles... + + .font-normal { + font-family: var(--font-sans); + } + + > span { + display: flex; + flex: 1; + gap: 0.5rem; + } .LemonButton__content { flex: 1; @@ -31,6 +46,11 @@ &[aria-disabled='true']:not(.LemonButton--loading) { cursor: not-allowed; + + > span { + cursor: not-allowed; + } + opacity: var(--opacity-disabled); } @@ -42,27 +62,27 @@ width: 100%; padding-left: 0.5rem; padding-right: 0.5rem; - overflow: hidden; + > span, .LemonButton__content { overflow: hidden; } } &.LemonButton--centered { - justify-content: center; + > span { + justify-content: center !important; + } + .LemonButton__content { - flex: initial; - text-align: center; + flex: initial !important; + text-align: center !important; } } &.LemonButton--has-icon { padding-left: 0.5rem; } - &.LemonButton--has-side-icon { - padding-right: 0.5rem; - } &.LemonButton--no-content { padding-left: 0.5rem; @@ -70,65 +90,39 @@ } &.LemonButton--xsmall { - --lemon-button-height: 1.5rem; - padding: 0.125rem 0.375rem; gap: 0.25rem; font-size: 0.75rem; - .LemonButton__icon { - font-size: 0.875rem; - } - - &.LemonButton--has-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-left: 0.25rem; + > span { + gap: 0.25rem; } - &.LemonButton--has-side-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-right: 0.25rem; + .LemonButton__icon { + font-size: 0.875rem; } } - &.LemonButton--small, - .Breadcrumbs3000 & { - --lemon-button-height: 2rem; - padding: 0.125rem 0.5rem; + &.LemonButton--small { gap: 0.25rem; - .LemonButton__icon { - font-size: 1.25rem; - } - - &.LemonButton--has-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-left: 0.375rem; + > span { + gap: 0.25rem; } - &.LemonButton--has-side-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-right: 0.375rem; + .LemonButton__icon { + font-size: 1.25rem; } } &.LemonButton--large { - --lemon-button-height: 3.5rem; - padding: 0.5rem 1rem; - gap: 0.75rem; font-size: 1rem; - .LemonButton__icon { - font-size: 1.75rem; + > span { + gap: 0.75rem; } - &.LemonButton--has-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-left: 0.75rem; - } - - &.LemonButton--has-side-icon:not(.LemonButton--no-padding), - &.LemonButton--no-content:not(.LemonButton--no-padding) { - padding-right: 0.75rem; + .LemonButton__icon { + font-size: 1.75rem; } } @@ -137,8 +131,6 @@ height: auto; width: auto; padding: 0; - padding-left: 0; - padding-right: 0; &.LemonButton--full-width { width: 100%; @@ -150,109 +142,23 @@ font-size: 1.5rem; flex-shrink: 0; transition: color 200ms ease; - justify-items: center; - } - - // LemonStealth has some specific styles - &.LemonButton--status-stealth { - font-weight: 400; - color: var(--default); - - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--primary-highlight); - color: inherit; // Avoid links being colored on hover - } - - &.LemonButton--active { - font-weight: 500; - - // These buttons keep their font-weight when actve - &.LemonButtonWithSideAction, - &[role='menuitem'], - &[aria-haspopup='true'] { - font-weight: 400; - } - } - - .LemonButton__icon { - color: var(--muted-alt); - } - - // Secondary - outlined color style - &.LemonButton--secondary { - background: var(--bg-light); - border: 1px solid var(--border); - - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--primary-highlight); - border-color: var(--primary); - } - - &:not([aria-disabled='true']):active { - border-color: var(--primary-dark); - } - } + place-items: center center; } @each $status in ('primary', 'danger', 'primary-alt', 'muted') { &.LemonButton--status-#{$status} { - color: var(--#{$status}, var(--primary)); - - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--#{$status}-highlight, var(--primary-highlight)); - } - &:not([aria-disabled='true']):active { - color: var(--#{$status}-dark, var(--primary-dark)); - .LemonButton__icon { - color: var(--#{$status}-dark, var(--primary-dark)); - } - } - - .LemonButton__icon { - color: var(--#{$status}); - } - // Primary - blocked color style &.LemonButton--primary { color: #fff; - background: var(--#{$status}); - .LemonButton__icon { - color: #fff; - } + background: var(--#{$status}-3000, var(--#{$status})); &:not([aria-disabled='true']):hover, &.LemonButton--active { color: #fff; - background: var(--#{$status}-light, var(--#{$status})); - .LemonButton__icon { - color: #fff; - } - } - &:not([aria-disabled='true']):active { - background: var(--#{$status}-dark, var(--#{$status})); - color: #fff; - .LemonButton__icon { - color: #fff; - } - } - } - - // Secondary - outlined color style - &.LemonButton--secondary { - background: var(--bg-light); - border: 1px solid var(--border); - - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--#{$status}-highlight, var(--primary-highlight)); - border-color: var(--#{$status}); } &:not([aria-disabled='true']):active { - border-color: var(--#{$status}-dark, var(--status)); + color: #fff; } } } @@ -261,69 +167,15 @@ .ant-tooltip & { // Buttons have an overriden style in tooltips, as they are always dark &:hover { - background: rgba(255, 255, 255, 0.15) !important; + background: rgb(255 255 255 / 15%) !important; } + &:active { - background: rgba(255, 255, 255, 0.2) !important; - } - .LemonButton__icon { - color: #fff !important; + background: rgb(255 255 255 / 20%) !important; } - } - - .posthog-3000 & { - font-size: 0.8125rem; - border: none !important; // 3000 buttons never have borders .LemonButton__icon { - color: var(--muted); - } - - &.LemonButton--status-primary { - color: var(--muted); - } - - &.LemonButton--status-stealth { - color: var(--default); - } - - &.LemonButton--primary { - color: #fff; - background: var(--primary-3000); - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--primary-3000-hover); - color: #fff; - } - .LemonButton__icon { - color: #fff; - } - } - - &.LemonButton--secondary { - color: var(--default); - background: var(--secondary-3000); - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - background: var(--secondary-3000-hover); - color: var(--default); - } - .LemonButton__icon { - color: var(--default); - } - } - - &:not([aria-disabled='true']):hover, - &.LemonButton--active { - color: var(--default); - background: var(--border); - .LemonButton__icon { - color: var(--default); - } - } - - &:not([aria-disabled='true']):active { - transform: scale(calc(35 / 36)); + color: #fff !important; } } } @@ -333,19 +185,11 @@ } .LemonButtonWithSideAction__spacer { - height: 1.5rem; - width: 1.5rem; box-sizing: content-box; &.LemonButtonWithSideAction__spacer--divider { - opacity: 0.17; - padding-left: 0.375rem; border-left: 1px solid currentColor; } - - .LemonButton--small & { - margin-left: 0.25rem; - } } .LemonButtonWithSideAction__side-button { diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.stories.tsx b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.stories.tsx index c9c9c9c6251cc..03e34921b04b5 100644 --- a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.stories.tsx @@ -1,4 +1,13 @@ +import { Link } from '@posthog/lemon-ui' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import clsx from 'clsx' +import { useAsyncHandler } from 'lib/hooks/useAsyncHandler' +import { IconCalculate, IconInfo, IconPlus } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { capitalizeFirstLetter, delay, range } from 'lib/utils' +import { urls } from 'scenes/urls' + import { LemonButton, LemonButtonProps, @@ -6,15 +15,7 @@ import { LemonButtonWithDropdownProps, LemonButtonWithSideAction, } from './LemonButton' -import { IconCalculate, IconInfo, IconPlus } from 'lib/lemon-ui/icons' import { More } from './More' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { capitalizeFirstLetter, delay, range } from 'lib/utils' -import { urls } from 'scenes/urls' -import { Link } from '@posthog/lemon-ui' -import { useAsyncHandler } from 'lib/hooks/useAsyncHandler' -import clsx from 'clsx' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' const statuses: LemonButtonProps['status'][] = ['primary', 'danger', 'primary-alt', 'muted', 'stealth'] const types: LemonButtonProps['type'][] = ['primary', 'secondary', 'tertiary'] diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx index 1d0f1cd90b8a9..087f10ea8558f 100644 --- a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx +++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx @@ -1,12 +1,16 @@ +import './LemonButton.scss' +import './LemonButtonLegacy.scss' +import './LemonButton3000.scss' + import clsx from 'clsx' -import React, { useContext } from 'react' import { IconArrowDropDown, IconChevronRight } from 'lib/lemon-ui/icons' +import React, { useContext } from 'react' + +import { LemonDropdown, LemonDropdownProps } from '../LemonDropdown' import { Link } from '../Link' +import { PopoverReferenceContext } from '../Popover' import { Spinner } from '../Spinner/Spinner' import { Tooltip, TooltipProps } from '../Tooltip' -import './LemonButton.scss' -import { LemonDropdown, LemonDropdownProps } from '../LemonDropdown' -import { PopoverReferenceContext } from '../Popover' export type LemonButtonDropdown = Omit @@ -63,6 +67,8 @@ export interface LemonButtonPropsBase /** Like plain `disabled`, except we enforce a reason to be shown in the tooltip. */ disabledReason?: string | null | false noPadding?: boolean + /** Hides the button chrome until hover. */ + stealth?: boolean size?: 'xsmall' | 'small' | 'medium' | 'large' 'data-attr'?: string 'aria-label'?: string @@ -92,6 +98,7 @@ export const LemonButton: React.FunctionComponent { const [popoverVisibility, popoverPlacement] = useContext(PopoverReferenceContext) || [false, null] + const within3000PageHeader = useContext(Within3000PageHeaderContext) if (!active && popoverVisibility) { active = true @@ -122,6 +130,9 @@ export const LemonButton: React.FunctionComponent disabled = true // Cannot interact with a loading button } + if (within3000PageHeader) { + size = 'small' + } let tooltipContent: TooltipProps['title'] if (disabledReason) { @@ -169,6 +180,7 @@ export const LemonButton: React.FunctionComponent - {icon ? {icon} : null} - {children ? {children} : null} - {sideIcon ? {sideIcon} : null} + + {icon ? {icon} : null} + {children ? {children} : null} + {sideIcon ? {sideIcon} : null} + ) @@ -202,6 +216,8 @@ export const LemonButton: React.FunctionComponent(false) + export type SideAction = Pick< LemonButtonProps, 'onClick' | 'to' | 'disabled' | 'icon' | 'type' | 'tooltip' | 'data-attr' | 'aria-label' | 'status' | 'targetBlank' diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton3000.scss b/frontend/src/lib/lemon-ui/LemonButton/LemonButton3000.scss new file mode 100644 index 0000000000000..70fa6ad667194 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton3000.scss @@ -0,0 +1,268 @@ +.posthog-3000.posthog-3000.posthog-3000 { + // The repetition is a specificity hack, so that we override .LemonButton + --transition: opacity 200ms ease, transform 200ms ease; + + .LemonButton { + border-width: 0; + border-style: solid; + border-color: transparent; + min-height: 2.125rem; + padding: 0; + position: relative; + outline: none; + transition: var(--transition); + border-radius: 6px; + cursor: pointer; + + .LemonButton__chrome { + border-radius: 6px; + font-size: 0.875rem; + display: flex; + flex-direction: row; + flex-shrink: 0; + align-items: center; + justify-content: flex-start; + background: none; + border-style: solid; + border-color: transparent; + font-weight: 500; + gap: 0.5rem; + line-height: 1.5rem; + min-height: 2.125rem; + position: relative; + text-align: left; + transition: var(--transition); + padding: 0.25rem 0.75rem; + width: 100%; + + .LemonButton__icon { + opacity: 0.5; + } + } + + &.LemonButton--full-width { + .LemonButton__chrome { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + } + + &.LemonButton--xsmall { + min-height: 1.5rem; + padding-left: 0; + font-size: 0.8125rem; + + .LemonButton__chrome { + min-height: 1.5rem; + padding: 0.125rem 0.375rem; + } + + &.LemonButton--has-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + .LemonButton__chrome { + padding-left: 0.25rem; + } + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + .LemonButton__chrome { + padding-right: 0.25rem; + } + } + } + + &.LemonButton--small { + min-height: 1.75rem; + font-size: 0.8125rem; + + .LemonButton__chrome { + min-height: 1.75rem; + padding: 0.25rem 0.5rem; + } + + &.LemonButton--has-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + .LemonButton__chrome { + padding-left: 0.375rem; + } + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + .LemonButton__chrome { + padding-right: 0.375rem; + } + } + } + + &.LemonButton--large { + min-height: 2.5rem; + + .LemonButton__chrome { + gap: 0.75rem; + min-height: 2.5rem; + padding: 0.5rem 1rem; + } + + &.LemonButton--has-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + padding-left: 0; + + .LemonButton__chrome { + padding-left: 0.75rem; + } + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-content, .LemonButton--no-padding) { + padding-right: 0; + + .LemonButton__chrome { + padding-right: 0.75rem; + } + } + } + + &.LemonButton--no-padding { + padding: 0; + min-height: 0; + + .LemonButton__chrome { + padding: 0; + min-height: 0; + } + } + + &:not(.LemonButton--tertiary) { + border-width: 1px; + padding-bottom: 1px; + + .LemonButton__chrome { + border-width: 1px; + margin: 0 -1px; + top: -1px; + } + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + .LemonButton__chrome { + top: -1.5px; + } + } + + &:not([aria-disabled='true']):active { + .LemonButton__chrome { + top: -0.5px; + } + } + } + + &.LemonButton--primary { + background: var(--primary-3000-frame-bg); + border-color: var(--primary-3000-frame-border); + + .LemonButton__chrome { + background: var(--primary-3000-button-bg); + border-color: var(--primary-3000-button-border); + color: #111; + font-weight: 600; + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + border-color: var(--primary-3000-button-border-hover); + } + } + } + + &.LemonButton--secondary { + background: var(--secondary-3000-frame-bg); + border-color: var(--secondary-3000-frame-border); + + &:not([aria-disabled='true']):hover .LemonButton__chrome { + border-color: var(--secondary-3000-button-border-hover); + } + + .LemonButton__chrome { + color: var(--default); + background: var(--accent-3000); + border-color: var(--secondary-3000-button-border); + } + + &.LemonButton--active { + .LemonButton__chrome { + color: var(--default); + background: var(--bg-light); + border-color: var(--secondary-3000-button-border-hover); + } + } + } + + &.LemonButton--is-stealth:not(.LemonButton--active) { + &:hover { + .LemonButton__chrome { + border-color: var(--secondary-3000-button-border); + } + } + + &:not(:hover) { + background-color: transparent; + border-color: transparent; + + .LemonButton__chrome { + background-color: transparent; + border-color: transparent; + color: var(--muted); + } + } + } + + &.LemonButton--tertiary { + color: var(--default); + + &.LemonButton--status-danger { + color: var(--danger); + } + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + background-color: var(--bg-3000); + } + } + } + + .LemonButtonWithSideAction__spacer { + color: var(--muted); + height: 1.25rem; + width: 1.25rem; + + &.LemonButtonWithSideAction__spacer--divider { + margin-left: 0.25rem; + padding: 0; + } + } + + // SideAction buttons are buttons next to other buttons in the DOM but layered on top. since they're on another button, we don't want them to look like buttons. + .LemonButtonWithSideAction__side-button { + top: 1px; + right: 1px; + bottom: 4px; + width: 1.625rem; + transform: none; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + + .LemonButton { + background: none !important; + border: none !important; + padding-bottom: 0 !important; + margin: 0 auto !important; + height: 100%; + } + + .LemonButton__chrome { + margin: auto !important; + top: 0 !important; + background: none !important; + border: none !important; + } + + &:not([aria-disabled='true']):hover { + background: rgb(0 0 0 / 10%); + } + } +} diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButtonLegacy.scss b/frontend/src/lib/lemon-ui/LemonButton/LemonButtonLegacy.scss new file mode 100644 index 0000000000000..abe0e6cc3e802 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButtonLegacy.scss @@ -0,0 +1,201 @@ +body:not(.posthog-3000) { + .LemonButton { + --lemon-button-height: 2.5rem; + + min-height: var(--lemon-button-height); + + &.LemonButton--has-side-icon { + padding-right: 0.5rem; + } + + &.LemonButton--xsmall { + padding: 0.125rem 0.375rem; + + --lemon-button-height: 1.5rem; + + &.LemonButton--has-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-left: 0.25rem; + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-right: 0.25rem; + } + } + + &.LemonButton--small, + .Breadcrumbs3000 & { + --lemon-button-height: 2rem; + + padding: 0.125rem 0.5rem; + + &.LemonButton--has-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-left: 0.375rem; + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-right: 0.375rem; + } + } + + &.LemonButton--full-width { + overflow: hidden; + } + + &.LemonButton--large { + --lemon-button-height: 3.5rem; + + padding: 0.5rem 1rem; + + &.LemonButton--has-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-left: 0.75rem; + } + + &.LemonButton--has-side-icon:not(.LemonButton--no-padding), + &.LemonButton--no-content:not(.LemonButton--no-padding) { + padding-right: 0.75rem; + } + } + + &.LemonButton--no-padding { + min-height: 0; + height: auto; + width: auto; + padding: 0; + padding-left: 0; + padding-right: 0; + + &.LemonButton--full-width { + width: 100%; + } + } + + // LemonStealth has some specific styles + &.LemonButton--status-stealth { + font-weight: 400; + color: var(--default); + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + background: var(--primary-highlight); + color: inherit; // Avoid links being colored on hover + } + + &.LemonButton--active { + font-weight: 500; + + // These buttons keep their font-weight when actve + &.LemonButtonWithSideAction, + &[role='menuitem'], + &[aria-haspopup='true'] { + font-weight: 400; + } + } + + .LemonButton__icon { + color: var(--muted-alt); + } + + // Secondary - outlined color style + &.LemonButton--secondary { + background: var(--bg-light); + border: 1px solid var(--border); + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + background: var(--primary-highlight); + border-color: var(--primary); + } + + &:not([aria-disabled='true']):active { + border-color: var(--primary-dark); + } + } + } + + @each $status in ('primary', 'danger', 'primary-alt', 'muted') { + &.LemonButton--status-#{$status} { + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + background: var(--#{$status}-highlight, var(--primary-highlight)); + } + + color: var(--#{$status}-3000, var(--#{$status}, var(--primary))); + + .LemonButton__icon { + color: var(--#{$status}-3000, var(--#{$status})); + } + + &:not([aria-disabled='true']):active { + color: var(--#{$status}-dark, var(--primary-dark)); + + .LemonButton__icon { + color: var(--#{$status}-dark, var(--primary-dark)); + } + } + + // Primary - blocked color style + &.LemonButton--primary { + color: #fff; + + .LemonButton__icon { + color: #fff; + } + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + color: #fff; + background: var(--#{$status}-light, var(--#{$status})); + + .LemonButton__icon { + color: #fff; + } + } + + &:not([aria-disabled='true']):active { + color: #fff; + background: var(--#{$status}-dark, var(--#{$status})); + + .LemonButton__icon { + color: #fff; + } + } + } + + // Secondary - outlined color style + &.LemonButton--secondary { + background: var(--bg-light); + border: 1px solid var(--border); + + &:not([aria-disabled='true']):hover, + &.LemonButton--active { + background: var(--#{$status}-highlight, var(--primary-highlight)); + border-color: var(--#{$status}); + } + + &:not([aria-disabled='true']):active { + border-color: var(--#{$status}-dark, var(--status)); + } + } + } + } + } + + .LemonButtonWithSideAction__spacer { + height: 1.5rem; + width: 1.5rem; + + &.LemonButtonWithSideAction__spacer--divider { + opacity: 0.17; + padding-left: 0.375rem; + } + + .LemonButton--small & { + margin-left: 0.25rem; + } + } +} diff --git a/frontend/src/lib/lemon-ui/LemonButton/More.tsx b/frontend/src/lib/lemon-ui/LemonButton/More.tsx index 00f03c2879ec3..71f6943ef93ee 100644 --- a/frontend/src/lib/lemon-ui/LemonButton/More.tsx +++ b/frontend/src/lib/lemon-ui/LemonButton/More.tsx @@ -1,6 +1,7 @@ -import { LemonButtonWithDropdown } from '.' import { IconEllipsis } from 'lib/lemon-ui/icons' + import { PopoverProps } from '../Popover/Popover' +import { LemonButtonWithDropdown } from '.' import { LemonButtonProps, LemonButtonWithDropdownProps } from './LemonButton' export type MoreProps = Partial> & diff --git a/frontend/src/lib/lemon-ui/LemonButton/index.ts b/frontend/src/lib/lemon-ui/LemonButton/index.ts index 944bbdc1a3c75..1f679448b3949 100644 --- a/frontend/src/lib/lemon-ui/LemonButton/index.ts +++ b/frontend/src/lib/lemon-ui/LemonButton/index.ts @@ -1,8 +1,8 @@ -export { LemonButton, LemonButtonWithSideAction, LemonButtonWithDropdown } from './LemonButton' export type { - LemonButtonPropsBase, LemonButtonProps, - LemonButtonWithSideActionProps, + LemonButtonPropsBase, LemonButtonWithDropdownProps, + LemonButtonWithSideActionProps, SideAction, } from './LemonButton' +export { LemonButton, LemonButtonWithDropdown, LemonButtonWithSideAction } from './LemonButton' diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss index 8d115c78884c6..8df6f4fc1166c 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss @@ -17,17 +17,20 @@ .LemonCalendar__month tr { .LemonButton { &.rounded-none { - border-radius: 0px; + border-radius: 0; } + &.rounded-r-none { - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + &.rounded-l-none { - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } } + .LemonCalendar__today { position: relative; @@ -39,7 +42,7 @@ border-radius: 100%; width: calc(var(--lemon-calendar-today-radius) * 2); height: calc(var(--lemon-calendar-today-radius) * 2); - background: currentcolor; + background: currentColor; } } } diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.stories.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.stories.tsx index 4d11003196266..7de56eab1a06b 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonCalendar, LemonCalendarProps } from './LemonCalendar' import { dayjs } from 'lib/dayjs' +import { LemonCalendar, LemonCalendarProps } from './LemonCalendar' + type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Lemon Calendar/Lemon Calendar', diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.test.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.test.tsx index fb3ca56a7852f..ea52b8c681552 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.test.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.test.tsx @@ -1,9 +1,11 @@ -import { LemonCalendar } from './LemonCalendar' import { render, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { getAllByDataAttr, getByDataAttr } from '~/test/byDataAttr' import { dayjs } from 'lib/dayjs' +import { getAllByDataAttr, getByDataAttr } from '~/test/byDataAttr' + +import { LemonCalendar } from './LemonCalendar' + describe('LemonCalendar', () => { test('click and move between months with one month showing', async () => { const onLeftmostMonthChanged = jest.fn() diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsx index 25fcc5c4cfecc..b9ca6dd658ab5 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsx @@ -1,11 +1,12 @@ import './LemonCalendar.scss' -import { useEffect, useState } from 'react' -import { dayjs } from 'lib/dayjs' -import { range } from 'lib/utils' -import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' -import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' + import clsx from 'clsx' import { useValues } from 'kea' +import { dayjs } from 'lib/dayjs' +import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import { range } from 'lib/utils' +import { useEffect, useState } from 'react' import { teamLogic } from 'scenes/teamLogic' export interface LemonCalendarProps { diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.stories.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.stories.tsx index a27e3f21ff22d..1c6bff250dd2b 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.stories.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { dayjs } from 'lib/dayjs' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCalendarSelect, LemonCalendarSelectProps } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { dayjs } from 'lib/dayjs' import { formatDate } from 'lib/utils' +import { useState } from 'react' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.test.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.test.tsx index 6d2850a258ced..dd44efa6634cc 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.test.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.test.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react' import { render, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { getByDataAttr } from '~/test/byDataAttr' -import { LemonCalendarSelect } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' import { dayjs } from 'lib/dayjs' +import { LemonCalendarSelect } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' +import { useState } from 'react' + +import { getByDataAttr } from '~/test/byDataAttr' describe('LemonCalendarSelect', () => { test('select various dates', async () => { diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx index d06f263ab8dc0..754b42c8ffc4c 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx @@ -1,8 +1,9 @@ -import { LemonCalendar } from 'lib/lemon-ui/LemonCalendar/LemonCalendar' -import { useState } from 'react' import { dayjs } from 'lib/dayjs' -import { LemonButton, LemonButtonProps, LemonButtonWithSideAction, SideAction } from 'lib/lemon-ui/LemonButton' import { IconClose } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonProps, LemonButtonWithSideAction, SideAction } from 'lib/lemon-ui/LemonButton' +import { LemonCalendar } from 'lib/lemon-ui/LemonCalendar/LemonCalendar' +import { useState } from 'react' + import { Popover } from '../Popover' export interface LemonCalendarSelectProps { diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.stories.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.stories.tsx index ec59718296c29..d11471a3db00c 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.stories.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { dayjs } from 'lib/dayjs' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCalendarRange, LemonCalendarRangeProps } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { dayjs } from 'lib/dayjs' import { formatDateRange } from 'lib/utils' +import { useState } from 'react' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.test.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.test.tsx index 9dff31017de88..82546dfb305f5 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.test.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.test.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react' import { render, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { getByDataAttr } from '~/test/byDataAttr' -import { LemonCalendarRange } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' import { dayjs } from 'lib/dayjs' +import { LemonCalendarRange } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' +import { useState } from 'react' + +import { getByDataAttr } from '~/test/byDataAttr' describe('LemonCalendarRange', () => { test('select various ranges', async () => { diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsx index 10f968ea2ba28..92492cc2bfe50 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' import { dayjs } from 'lib/dayjs' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconClose } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { formatDate, formatDateRange } from 'lib/utils' +import { useState } from 'react' + import { LemonCalendarRangeInline } from './LemonCalendarRangeInline' export interface LemonCalendarRangeProps { diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.stories.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.stories.tsx index 1cc8d6dec12a0..fbdbc8a174470 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.stories.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonCalendarRangeProps } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' import { dayjs } from 'lib/dayjs' +import { LemonCalendarRangeProps } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' import { formatDateRange } from 'lib/utils' +import { useState } from 'react' + import { LemonCalendarRangeInline } from './LemonCalendarRangeInline' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx index 898f77728dd26..3fb703a5b4081 100644 --- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx +++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx @@ -1,7 +1,8 @@ +import clsx from 'clsx' +import { dayjs } from 'lib/dayjs' import { LemonCalendar } from 'lib/lemon-ui/LemonCalendar/LemonCalendar' import { useEffect, useState } from 'react' -import { dayjs } from 'lib/dayjs' -import clsx from 'clsx' + import { LemonCalendarRangeProps } from './LemonCalendarRange' /** Used to calculate how many calendars fit on the screen */ diff --git a/frontend/src/lib/lemon-ui/LemonCard/LemonCard.scss b/frontend/src/lib/lemon-ui/LemonCard/LemonCard.scss index 044e7b9f25603..f5fd7cf2493a4 100644 --- a/frontend/src/lib/lemon-ui/LemonCard/LemonCard.scss +++ b/frontend/src/lib/lemon-ui/LemonCard/LemonCard.scss @@ -1,5 +1,6 @@ .LemonCard { transition: 200ms ease; + &.LemonCard--hoverEffect { &:hover { transform: scale(1.01); diff --git a/frontend/src/lib/lemon-ui/LemonCard/index.ts b/frontend/src/lib/lemon-ui/LemonCard/index.ts index b3d760635a508..1bad4093b23d8 100644 --- a/frontend/src/lib/lemon-ui/LemonCard/index.ts +++ b/frontend/src/lib/lemon-ui/LemonCard/index.ts @@ -1,2 +1,2 @@ -export { LemonCard } from './LemonCard' export type { LemonCardProps } from './LemonCard' +export { LemonCard } from './LemonCard' diff --git a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss index 560b2ce2c48a3..135c3c2a43e24 100644 --- a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss +++ b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.scss @@ -14,6 +14,11 @@ label { --tick-length: 12.73; // Approximation of tick length, which is (3 + 6) * sqrt(2) --box-color: var(--primary); + + .posthog-3000 & { + --box-color: var(--primary-3000); + } + display: flex; align-items: center; cursor: pointer; @@ -45,6 +50,7 @@ &.LemonCheckbox--full-width { width: 100%; + label { width: 100%; } @@ -57,21 +63,16 @@ &.LemonCheckbox:not(.LemonCheckbox--disabled):hover, &.LemonCheckbox:not(.LemonCheckbox--disabled):active { label { - --box-color: var(--primary-light); + --box-color: var(--primary-3000-hover); .LemonCheckbox__box { border-color: var(--box-color); - border-color: var(--box-color); } } } &.LemonCheckbox:not(.LemonCheckbox--disabled):active label { - --box-color: var(--primary-dark); - } - - &.LemonCheckbox--full-width label { - width: 100%; + --box-color: var(--primary-3000-active); } &.LemonCheckbox--checked { diff --git a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.stories.tsx b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.stories.tsx index 370c0e17ed1d1..c26bc8a52f2a2 100644 --- a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { LemonCheckbox, LemonCheckboxProps } from './LemonCheckbox' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.tsx b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.tsx index 810d5969826f6..29fc08835695d 100644 --- a/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.tsx +++ b/frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.tsx @@ -1,6 +1,8 @@ +import './LemonCheckbox.scss' + import clsx from 'clsx' import { useEffect, useMemo, useState } from 'react' -import './LemonCheckbox.scss' + import { Tooltip } from '../Tooltip' export interface LemonCheckboxProps { diff --git a/frontend/src/lib/lemon-ui/LemonCheckbox/index.ts b/frontend/src/lib/lemon-ui/LemonCheckbox/index.ts index 0859c338959ff..d9e9746415f55 100644 --- a/frontend/src/lib/lemon-ui/LemonCheckbox/index.ts +++ b/frontend/src/lib/lemon-ui/LemonCheckbox/index.ts @@ -1,2 +1,2 @@ -export { LemonCheckbox } from './LemonCheckbox' export type { LemonCheckboxProps } from './LemonCheckbox' +export { LemonCheckbox } from './LemonCheckbox' diff --git a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss index 08c6f340c44bf..6dd509e146f97 100644 --- a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss +++ b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.scss @@ -12,14 +12,15 @@ display: flex; flex-direction: column; align-items: stretch; + &:not(:last-child) { border-bottom-width: 1px; } } .LemonCollapsePanel__header { - min-height: 2.875rem; - border-radius: 0; + min-height: 2.875rem !important; + border-radius: 0 !important; padding: 0.5rem 0.75rem !important; // Override reduced side padding font-weight: 500 !important; // Override status="stealth"'s font-weight @@ -29,11 +30,15 @@ } .LemonCollapsePanel__body { - transition: height 200ms ease; - height: 0; - overflow: hidden; border-top-width: 1px; box-sizing: content-box; + height: 0; + overflow: hidden; + transition: height 200ms ease; + + .posthog-3000 & { + background: var(--bg-light); + } } .LemonCollapsePanel__content { diff --git a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.stories.tsx b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.stories.tsx index 253d295bf135c..4eaefe6b2a8cc 100644 --- a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { LemonCollapse as LemonCollapseComponent } from './LemonCollapse' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.tsx b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.tsx index 07a04ab3b0751..378ad687033c9 100644 --- a/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.tsx +++ b/frontend/src/lib/lemon-ui/LemonCollapse/LemonCollapse.tsx @@ -1,11 +1,13 @@ +import './LemonCollapse.scss' + import clsx from 'clsx' import React, { ReactNode, useState } from 'react' import { Transition } from 'react-transition-group' import { ENTERED, ENTERING } from 'react-transition-group/Transition' import useResizeObserver from 'use-resize-observer' + import { IconUnfoldLess, IconUnfoldMore } from '../icons' import { LemonButton } from '../LemonButton' -import './LemonCollapse.scss' export interface LemonCollapsePanel { key: K diff --git a/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.stories.tsx b/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.stories.tsx index 3d1ac6ae829de..83b75dbacee26 100644 --- a/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.stories.tsx @@ -1,7 +1,8 @@ +import { Link } from '@posthog/lemon-ui' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonDialog, LemonDialogProps } from './LemonDialog' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Link } from '@posthog/lemon-ui' + +import { LemonDialog, LemonDialogProps } from './LemonDialog' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.tsx b/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.tsx index 8ef22e6628dd6..f7ddd96fbc40a 100644 --- a/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.tsx +++ b/frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.tsx @@ -1,9 +1,9 @@ -import { ReactNode, useEffect, useRef, useState } from 'react' +import { useValues } from 'kea' +import { router } from 'kea-router' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { LemonModal, LemonModalProps } from 'lib/lemon-ui/LemonModal' +import { ReactNode, useEffect, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' -import { useValues } from 'kea' -import { router } from 'kea-router' export type LemonDialogProps = Pick & { primaryButton?: LemonButtonProps | null diff --git a/frontend/src/lib/lemon-ui/LemonDialog/index.ts b/frontend/src/lib/lemon-ui/LemonDialog/index.ts index e091a9cc0f90f..92240b21e97f8 100644 --- a/frontend/src/lib/lemon-ui/LemonDialog/index.ts +++ b/frontend/src/lib/lemon-ui/LemonDialog/index.ts @@ -1,2 +1,2 @@ -export { LemonDialog } from './LemonDialog' export type { LemonDialogProps } from './LemonDialog' +export { LemonDialog } from './LemonDialog' diff --git a/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.stories.tsx b/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.stories.tsx index 5680846242126..54bb7be44dbf8 100644 --- a/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.stories.tsx @@ -1,9 +1,10 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonDivider, LemonDividerProps } from './LemonDivider' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonRow } from 'lib/lemon-ui/LemonRow' + import { Lettermark, LettermarkColor } from '../Lettermark/Lettermark' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { ProfileBubbles } from '../ProfilePicture' +import { LemonDivider, LemonDividerProps } from './LemonDivider' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.tsx b/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.tsx index 557d4ed70156e..c6051406f9016 100644 --- a/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.tsx +++ b/frontend/src/lib/lemon-ui/LemonDivider/LemonDivider.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx' import './LemonDivider.scss' +import clsx from 'clsx' + export interface LemonDividerProps { /** 3x the thickness of the line. */ thick?: boolean diff --git a/frontend/src/lib/lemon-ui/LemonDivider/index.ts b/frontend/src/lib/lemon-ui/LemonDivider/index.ts index dc8fe94a055f7..9370254049c75 100644 --- a/frontend/src/lib/lemon-ui/LemonDivider/index.ts +++ b/frontend/src/lib/lemon-ui/LemonDivider/index.ts @@ -1,2 +1,2 @@ -export { LemonDivider } from './LemonDivider' export type { LemonDividerProps } from './LemonDivider' +export { LemonDivider } from './LemonDivider' diff --git a/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx b/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx index 35588606fcdd9..c1fe28245c5c6 100644 --- a/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx +++ b/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx @@ -1,4 +1,5 @@ import React, { MouseEventHandler, useContext, useEffect, useRef, useState } from 'react' + import { Popover, PopoverOverlayContext, PopoverProps } from '../Popover' export interface LemonDropdownProps extends Omit { diff --git a/frontend/src/lib/lemon-ui/LemonDropdown/index.ts b/frontend/src/lib/lemon-ui/LemonDropdown/index.ts index c60bc106452cc..9a0e2a2dca3b5 100644 --- a/frontend/src/lib/lemon-ui/LemonDropdown/index.ts +++ b/frontend/src/lib/lemon-ui/LemonDropdown/index.ts @@ -1,2 +1,2 @@ -export { LemonDropdown } from './LemonDropdown' export type { LemonDropdownProps } from './LemonDropdown' +export { LemonDropdown } from './LemonDropdown' diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.scss b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.scss index bba49b00e27b7..731f84ed06b90 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.scss +++ b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.scss @@ -4,12 +4,13 @@ .FileDropTarget--active::after { --file-drop-target-padding: 0.5rem; + content: ''; position: absolute; top: calc(-1 * var(--file-drop-target-padding)); left: calc(-1 * var(--file-drop-target-padding)); height: calc(100% + var(--file-drop-target-padding) * 2); width: calc(100% + var(--file-drop-target-padding) * 2); - border: 3px dashed var(--primary); + border: 3px dashed var(--primary-3000); border-radius: var(--radius); } diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx index 25cf4daf6e04d..f12cc67d0eb1f 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx @@ -1,9 +1,10 @@ -import { ChangeEvent, createRef, RefObject, useEffect, useState } from 'react' +import './LemonFileInput.scss' + +import clsx from 'clsx' import { IconUploadFile } from 'lib/lemon-ui/icons' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import clsx from 'clsx' -import './LemonFileInput.scss' +import { ChangeEvent, createRef, RefObject, useEffect, useState } from 'react' export interface LemonFileInputProps extends Pick { value?: File[] diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/index.ts b/frontend/src/lib/lemon-ui/LemonFileInput/index.ts index a6b3cbff76d30..606370c2b67d4 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/index.ts +++ b/frontend/src/lib/lemon-ui/LemonFileInput/index.ts @@ -1,2 +1,2 @@ -export { LemonFileInput } from './LemonFileInput' export type { LemonFileInputProps } from './LemonFileInput' +export { LemonFileInput } from './LemonFileInput' diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss index 30eeee4083802..a5f81f7376b58 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss @@ -1,26 +1,34 @@ .LemonInput { - transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease, opacity 200ms ease; - display: flex; - min-height: 2.5rem; - padding: 0.25rem 0.5rem; + align-items: center; background: none; border-radius: var(--radius); + border: 1px solid var(--border); + color: var(--default); + cursor: text; + display: flex; font-size: 0.875rem; - text-align: left; + gap: 0.25rem; + justify-content: center; line-height: 1.25rem; - cursor: text; - color: var(--default); - border: 1px solid var(--border); + min-height: 2.5rem; + padding: 0.25rem 0.5rem; + text-align: left; background-color: var(--bg-light); - align-items: center; - justify-content: center; - gap: 0.25rem; &:hover:not([aria-disabled='true']) { - border-color: var(--primary-light); + border-color: var(--primary-3000-hover); + + .posthog-3000 & { + border-color: var(--border-bold); + } } + &.LemonInput--focused:not([aria-disabled='true']) { - border-color: var(--primary); + border-color: var(--primary-3000); + + .posthog-3000 & { + border-color: var(--border-bold); + } } &.LemonInput--transparent-background { @@ -66,7 +74,7 @@ &.LemonInput--has-content { > .LemonIcon { - color: var(--primary); + color: var(--primary-3000); } } @@ -74,6 +82,7 @@ // NOTE Design: Search inputs are given a specific small width max-width: 240px; } + &.LemonInput--full-width { width: 100%; max-width: 100%; diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.stories.tsx b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.stories.tsx index 53e20b45848de..e254c18262fcb 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.stories.tsx @@ -1,9 +1,9 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' - -import { LemonInput } from './LemonInput' import { IconArrowDropDown, IconCalendar } from 'lib/lemon-ui/icons' import { LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { useState } from 'react' + +import { LemonInput } from './LemonInput' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx index 650d621528ea2..5149610d9cc28 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx @@ -1,8 +1,9 @@ import './LemonInput.scss' -import React, { useRef, useState } from 'react' + import clsx from 'clsx' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconClose, IconEyeHidden, IconEyeVisible, IconMagnifier } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import React, { useRef, useState } from 'react' interface LemonInputPropsBase extends Pick< diff --git a/frontend/src/lib/lemon-ui/LemonInput/index.ts b/frontend/src/lib/lemon-ui/LemonInput/index.ts index 06f396399f111..43895c91420c9 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/index.ts +++ b/frontend/src/lib/lemon-ui/LemonInput/index.ts @@ -1,2 +1,2 @@ -export { LemonInput } from './LemonInput' export type { LemonInputProps, LemonInputPropsNumber, LemonInputPropsText } from './LemonInput' +export { LemonInput } from './LemonInput' diff --git a/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.stories.tsx b/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.stories.tsx index cb81dd236aeed..0a3a1ecf512da 100644 --- a/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.stories.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react' +import { LemonModal } from '@posthog/lemon-ui' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { useState } from 'react' + import { LemonLabel, LemonLabelProps } from './LemonLabel' -import { LemonModal } from '@posthog/lemon-ui' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.tsx b/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.tsx index 7568d9b838eed..8aeb58c93d545 100644 --- a/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.tsx +++ b/frontend/src/lib/lemon-ui/LemonLabel/LemonLabel.tsx @@ -1,8 +1,10 @@ import './LemonLabel.scss' -import { Tooltip } from '../Tooltip' -import { IconInfo } from 'lib/lemon-ui/icons' + import clsx from 'clsx' +import { IconInfo } from 'lib/lemon-ui/icons' + import { Link, LinkProps } from '../Link' +import { Tooltip } from '../Tooltip' export interface LemonLabelProps extends Pick, 'id' | 'htmlFor' | 'form' | 'children' | 'className'> { diff --git a/frontend/src/lib/lemon-ui/LemonLabel/index.ts b/frontend/src/lib/lemon-ui/LemonLabel/index.ts index 508b15abac24d..eb65b794dc459 100644 --- a/frontend/src/lib/lemon-ui/LemonLabel/index.ts +++ b/frontend/src/lib/lemon-ui/LemonLabel/index.ts @@ -1,2 +1,2 @@ -export { LemonLabel } from './LemonLabel' export type { LemonLabelProps } from './LemonLabel' +export { LemonLabel } from './LemonLabel' diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss index 1858e38e78e92..f8ae353f277ac 100644 --- a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss @@ -1,28 +1,35 @@ .LemonMarkdown { > * { - margin: 0 0 0.5em 0; + margin: 0 0 0.5em; + &:last-child { margin-bottom: 0; } } + ol, ul, dl { padding-left: 1.5em; } + ol { list-style-type: decimal; } + ul { list-style-type: disc; } + strong[level] { // Low-key headings display: block; } + hr { margin: 1em 0; } + h1 { margin-bottom: 0.25em; font-weight: 600; diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx index 89c4b786360e6..30065bcc45fc6 100644 --- a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { LemonMarkdown as LemonMarkdownComponent, LemonMarkdownProps } from './LemonMarkdown' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx index 90d8258c1cf30..118f182c52b5c 100644 --- a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx @@ -1,8 +1,10 @@ -import ReactMarkdown from 'react-markdown' import './LemonMarkdown.scss' -import { Link } from '../Link' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import clsx from 'clsx' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import ReactMarkdown from 'react-markdown' + +import { Link } from '../Link' export interface LemonMarkdownProps { children: string diff --git a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.stories.tsx b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.stories.tsx index 05ed74c6eea29..90ed1a63e0d5a 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.stories.tsx @@ -1,11 +1,12 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + +import { Splotch, SplotchColor } from '../Splotch' import { + LemonMenuItems, LemonMenuOverlay as LemonMenuOverlayComponent, LemonMenuOverlayProps, - LemonMenuItems, LemonMenuSection, } from './LemonMenu' -import { Splotch, SplotchColor } from '../Splotch' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx index b061b90d56c35..227a2c3d6a342 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx +++ b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx @@ -1,13 +1,15 @@ +import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import React, { FunctionComponent, ReactNode, useCallback, useMemo } from 'react' + +import { KeyboardShortcut, KeyboardShortcutProps } from '~/layout/navigation-3000/components/KeyboardShortcut' + import { LemonButton, LemonButtonProps } from '../LemonButton' -import { TooltipProps } from '../Tooltip' import { LemonDivider } from '../LemonDivider' import { LemonDropdown, LemonDropdownProps } from '../LemonDropdown' +import { TooltipProps } from '../Tooltip' import { useKeyboardNavigation } from './useKeyboardNavigation' -import { useValues } from 'kea' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { KeyboardShortcut, KeyboardShortcutProps } from '~/layout/navigation-3000/components/KeyboardShortcut' type KeyboardShortcut = Array diff --git a/frontend/src/lib/lemon-ui/LemonMenu/index.ts b/frontend/src/lib/lemon-ui/LemonMenu/index.ts index 38661b24bc2fb..c12d3404d74e6 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/index.ts +++ b/frontend/src/lib/lemon-ui/LemonMenu/index.ts @@ -1,2 +1,2 @@ +export type { LemonMenuItem, LemonMenuItems, LemonMenuSection } from './LemonMenu' export { LemonMenu } from './LemonMenu' -export type { LemonMenuItem, LemonMenuSection, LemonMenuItems } from './LemonMenu' diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss index a3f0cae016830..50506eac65e68 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.scss @@ -1,14 +1,9 @@ .LemonModal__overlay { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - + inset: 0; transition: background-color var(--modal-transition-time) ease-out, backdrop-filter var(--modal-transition-time) ease-out; z-index: var(--z-modal); - display: flex; align-items: center; justify-content: center; @@ -24,7 +19,7 @@ &.ReactModal__Overlay--before-close { background-color: transparent; - backdrop-filter: blur(0px); + backdrop-filter: blur(0); } } @@ -37,7 +32,7 @@ margin: 1rem auto; border-radius: var(--radius); background-color: var(--bg-light); - border: 1px solid var(--border); + border: 1px solid var(--border-3000); box-shadow: var(--shadow-elevation); transition: opacity var(--modal-transition-time) ease-out, transform var(--modal-transition-time) ease-out; display: flex; @@ -62,13 +57,15 @@ transform: scale(1); opacity: 1; } + .LemonModal__close { position: absolute; top: 1.25rem; right: 1.25rem; z-index: 1; + &.LemonModal__close--highlighted { - animation: tilt-shake 400ms; + animation: LemonModal__tilt-shake 400ms; } } @@ -132,19 +129,23 @@ border-top: none; } -@keyframes tilt-shake { +@keyframes LemonModal__tilt-shake { 0% { transform: rotate(0deg); } + 16.666% { transform: rotate(12deg); } + 50% { transform: rotate(-10deg); } + 83.333% { transform: rotate(8deg); } + 100% { transform: rotate(0deg); } diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.stories.tsx b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.stories.tsx index 55ca9533948b9..50de36a8d50bd 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.stories.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react' import { Meta, StoryFn } from '@storybook/react' -import { LemonModal, LemonModalProps } from './LemonModal' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useState } from 'react' + +import { LemonModal, LemonModalProps } from './LemonModal' const meta: Meta = { title: 'Lemon UI/Lemon Modal', diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx index 76dde577a77f9..b791f1b4b9826 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx @@ -1,14 +1,15 @@ -import { useEffect, useRef, useState } from 'react' -import clsx from 'clsx' -import Modal from 'react-modal' +import './LemonModal.scss' +import clsx from 'clsx' import { IconClose } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useEffect, useRef, useState } from 'react' +import Modal from 'react-modal' -import './LemonModal.scss' -import { Tooltip } from '../Tooltip' import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' +import { Tooltip } from '../Tooltip' + interface LemonModalInnerProps { children?: React.ReactNode className?: string diff --git a/frontend/src/lib/lemon-ui/LemonModal/index.ts b/frontend/src/lib/lemon-ui/LemonModal/index.ts index 02ed8923ad50a..3f8910e2634e7 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/index.ts +++ b/frontend/src/lib/lemon-ui/LemonModal/index.ts @@ -1,2 +1,2 @@ -export { LemonModal } from './LemonModal' export type { LemonModalProps } from './LemonModal' +export { LemonModal } from './LemonModal' diff --git a/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.stories.tsx b/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.stories.tsx index 7fc77ead26486..7215a14b0ea11 100644 --- a/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.stories.tsx @@ -1,9 +1,10 @@ +import { IconGear } from '@posthog/icons' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonProgressCircle, LemonProgressCircleProps } from './LemonProgressCircle' import { useEffect, useState } from 'react' + import { LemonButton } from '../LemonButton' -import { IconGear } from '@posthog/icons' import { LemonCheckbox } from '../LemonCheckbox' +import { LemonProgressCircle, LemonProgressCircleProps } from './LemonProgressCircle' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.tsx b/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.tsx index 6d8e48419aa19..87eb0959a1450 100644 --- a/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.tsx +++ b/frontend/src/lib/lemon-ui/LemonProgressCircle/LemonProgressCircle.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx' import './LemonProgressCircle.scss' +import clsx from 'clsx' + export type LemonProgressCircleProps = { strokePercentage?: number backgroundStrokeOpacity?: number diff --git a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.scss b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.scss index a1b6d8c5c03c8..cdec3b871b333 100644 --- a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.scss +++ b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.scss @@ -20,7 +20,7 @@ font-weight: 600; .LemonRow__icon { - color: var(--primary); + color: var(--primary-3000); } } @@ -117,6 +117,7 @@ .LemonRow--large { @extend .LemonRow--tall; + font-size: 1rem; .LemonRow__icon { diff --git a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.stories.tsx b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.stories.tsx index c67cb1759c0a8..bb58867af0373 100644 --- a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { IconInfo, IconPremium } from 'lib/lemon-ui/icons' + import { LemonRow, LemonRowProps } from './LemonRow' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.tsx b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.tsx index b41c2397c3550..c109e2d818a63 100644 --- a/frontend/src/lib/lemon-ui/LemonRow/LemonRow.tsx +++ b/frontend/src/lib/lemon-ui/LemonRow/LemonRow.tsx @@ -1,9 +1,11 @@ -import clsx from 'clsx' import './LemonRow.scss' -import { Tooltip } from '../Tooltip' -import { Spinner } from '../Spinner/Spinner' + +import clsx from 'clsx' import React from 'react' +import { Spinner } from '../Spinner/Spinner' +import { Tooltip } from '../Tooltip' + // Fix for function type inference in forwardRef, so that function components wrapped with forwardRef can be generic. // For some reason the @types/react definitons as React 16 and TS 4.9 don't work, because `P` (the props) is wrapped in // `Pick` (inside `React.PropsWithoutRef`), which breaks TypeScript's ability to reason about it as a generic type. diff --git a/frontend/src/lib/lemon-ui/LemonRow/index.ts b/frontend/src/lib/lemon-ui/LemonRow/index.ts index 9e5c1b25d9e48..b2b3718d558b4 100644 --- a/frontend/src/lib/lemon-ui/LemonRow/index.ts +++ b/frontend/src/lib/lemon-ui/LemonRow/index.ts @@ -1,2 +1,2 @@ -export { LemonRow } from './LemonRow' export type { LemonRowProps, LemonRowPropsBase } from './LemonRow' +export { LemonRow } from './LemonRow' diff --git a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss index 44f4c12cd5cab..fd66747c15565 100644 --- a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss +++ b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss @@ -2,9 +2,8 @@ position: relative; flex-shrink: 0; width: fit-content; - background: var(--bg-light); border-radius: var(--radius); - border: 1px solid var(--border); + > ul { z-index: 1; // Place above slider list-style: none; @@ -15,72 +14,154 @@ &.LemonSegmentedButton--full-width { width: 100%; } -} -.LemonSegmentedButton__slider { - // This is a real element and not ::after to avoid initial transition from 0 width - position: absolute; - top: -1px; // 1px of border - left: -1px; // 1px of border - height: calc(100% + 2px); // 1px of border (top + bottom) - width: calc(var(--lemon-segmented-button-slider-width) + 2px); // 1px of border (left + right) - transform: translateX(var(--lemon-segmented-button-slider-offset)); - background: var(--primary); - &.LemonSegmentedButton__slider--first { - border-top-left-radius: var(--radius); - border-bottom-left-radius: var(--radius); + .LemonSegmentedButton__option { + display: flex; + flex: 1; + + .LemonButton__content { + text-wrap: nowrap; + } } - &.LemonSegmentedButton__slider--last { - border-top-right-radius: var(--radius); - border-bottom-right-radius: var(--radius); +} + +body:not(.posthog-3000) { + .LemonSegmentedButton { + background: var(--bg-light); + border: 1px solid var(--border); } - .LemonSegmentedButton--transitioning & { + + .LemonSegmentedButton__slider { + // This is a real element and not ::after to avoid initial transition from 0 width transition: width 200ms ease, transform 200ms ease, border-radius 200ms ease; will-change: width, transform, border-radius; - } -} + position: absolute; + top: -1px; // 1px of border + left: -1px; // 1px of border + height: calc(100% + 2px); // 1px of border (top + bottom) + width: calc(var(--lemon-segmented-button-slider-width) + 2px); // 1px of border (left + right) + transform: translateX(var(--lemon-segmented-button-slider-offset)); + background: var(--primary); -.LemonSegmentedButton__option { - display: flex; - flex: 1; - .LemonButton { - // Original transition with outline added - transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease, - outline 200ms ease; - outline: 1px solid transparent; - border-radius: 0; - min-height: calc(var(--lemon-button-height) - 2px); - } - &:first-child, - &:first-child .LemonButton { - border-top-left-radius: var(--radius); - border-bottom-left-radius: var(--radius); - } - &:last-child, - &:last-child .LemonButton { - border-top-right-radius: var(--radius); - border-bottom-right-radius: var(--radius); - } - &:not(:last-child) { - border-right: 1px solid var(--border); + &.LemonSegmentedButton__slider--first { + border-top-left-radius: var(--radius); + border-bottom-left-radius: var(--radius); + } + + &.LemonSegmentedButton__slider--last { + border-top-right-radius: var(--radius); + border-bottom-right-radius: var(--radius); + } } - &:not(.LemonSegmentedButton__option--disabled):not(.LemonSegmentedButton__option--selected) { - &:hover .LemonButton { - outline-color: var(--primary); + + .LemonSegmentedButton__option { + .LemonButton { + // Original transition with outline added + transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease, + outline 200ms ease; + outline: 1px solid transparent; + border-radius: 0; + min-height: calc(var(--lemon-button-height) - 2px); + + &:hover { + > span { + border-color: none !important; + } + } + } + + &:first-child, + &:first-child .LemonButton { + border-top-left-radius: var(--radius); + border-bottom-left-radius: var(--radius); + } + + &:last-child, + &:last-child .LemonButton { + border-top-right-radius: var(--radius); + border-bottom-right-radius: var(--radius); + } + + &:not(:last-child) { + border-right: 1px solid var(--border); + } + + &:not(.LemonSegmentedButton__option--disabled, .LemonSegmentedButton__option--selected) { + &:hover .LemonButton { + outline-color: var(--primary); + } + + &:active .LemonButton { + outline-color: var(--primary-dark); + } } - &:active .LemonButton { - outline-color: var(--primary-dark); + + &.LemonSegmentedButton__option--selected { + .LemonButton, + .LemonButton__icon { + color: #fff; + } + + .LemonButton { + &:hover, + &:active { + background: none; // Disable LemonButton's hover styles for the selected option + } + } } } - &.LemonSegmentedButton__option--selected { - .LemonButton, - .LemonButton__icon { - color: #fff; +} + +.posthog-3000 { + .LemonSegmentedButton__option { + & .LemonButton, + & .LemonButton > span { + border-radius: 0; } - .LemonButton { - &:hover, - &:active { - background: none; // Disable LemonButton's hover styles for the selected option + + .LemonButton > span { + background: var(--bg-3000); + + .LemonButton__icon, + .LemonButton__content { + opacity: 0.4; + } + } + + .LemonButton.LemonButton--secondary:not([aria-disabled='true']):hover { + > span { + border-color: var(--secondary-3000-button-border); + } + } + + &:first-child, + &:first-child .LemonButton, + &:first-child .LemonButton > span { + border-top-left-radius: var(--radius); + border-bottom-left-radius: var(--radius); + } + + &:last-child, + &:last-child .LemonButton, + &:last-child .LemonButton > span { + border-top-right-radius: var(--radius); + border-bottom-right-radius: var(--radius); + } + + &:not(:last-child) { + .LemonButton { + border-right: none; + } + } + + &.LemonSegmentedButton__option--selected { + .LemonButton > span { + background-color: var(--bg-light); + + .LemonButton__icon, + .LemonButton__content { + opacity: 1; + } } } } diff --git a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.stories.tsx b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.stories.tsx index 34d31b37b84c7..0b0090ecbfde9 100644 --- a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useState } from 'react' + import { IconCalculate, IconCalendar, IconLightBulb, IconSettings } from '../icons' import { LemonSegmentedButton, LemonSegmentedButtonOption, LemonSegmentedButtonProps } from './LemonSegmentedButton' diff --git a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx index a925394115938..2e8e6ef391975 100644 --- a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx +++ b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx @@ -1,8 +1,13 @@ +import './LemonSegmentedButton.scss' + import clsx from 'clsx' +import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import React from 'react' -import { LemonButton } from '../LemonButton' + import { useSliderPositioning } from '../hooks' -import './LemonSegmentedButton.scss' +import { LemonButton, LemonButtonProps } from '../LemonButton' export interface LemonSegmentedButtonOption { value: T @@ -18,7 +23,7 @@ export interface LemonSegmentedButtonProps { value?: T onChange?: (newValue: T) => void options: LemonSegmentedButtonOption[] - size?: 'small' | 'medium' + size?: LemonButtonProps['size'] className?: string fullWidth?: boolean } @@ -41,6 +46,15 @@ export function LemonSegmentedButton({ HTMLDivElement, HTMLLIElement >(value, 200) + const { featureFlags } = useValues(featureFlagLogic) + + const has3000 = featureFlags[FEATURE_FLAGS.POSTHOG_3000] + + let buttonProps = {} + + if (has3000) { + buttonProps = { status: 'stealth', type: 'secondary', motion: false } + } return (
    ({ icon={option.icon} data-attr={option['data-attr']} center + {...buttonProps} > {option.label} diff --git a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.scss b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.scss index 1e9684ab3cc16..fdcd0a1577f6c 100644 --- a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.scss +++ b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.scss @@ -1,8 +1,15 @@ -.LemonSelect--button--clearable { - padding-left: 0.5rem !important; - margin-left: auto; -} - .LemonSelect--clearable { padding-right: 0 !important; + + > span { + padding-right: 0 !important; + } + + .LemonButton__content { + gap: 0.5rem; + + .LemonSelect--button--clearable { + margin-left: auto; + } + } } diff --git a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.stories.tsx b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.stories.tsx index f86c3b9fed7b0..6b78f42d26663 100644 --- a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonSelect, LemonSelectOptions, LemonSelectProps } from './LemonSelect' import { capitalizeFirstLetter } from 'lib/utils' +import { LemonSelect, LemonSelectOptions, LemonSelectProps } from './LemonSelect' + type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Lemon Select', diff --git a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx index 5686b6af19412..d57dc883eb34f 100644 --- a/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelect/LemonSelect.tsx @@ -1,11 +1,12 @@ +import './LemonSelect.scss' + +import clsx from 'clsx' import React, { useMemo } from 'react' + import { IconClose } from '../icons' import { LemonButton, LemonButtonProps } from '../LemonButton' -import { PopoverProps } from '../Popover' -import './LemonSelect.scss' -import clsx from 'clsx' -import { TooltipProps } from '../Tooltip' import { + isLemonMenuSection, LemonMenu, LemonMenuItem, LemonMenuItemBase, @@ -13,8 +14,9 @@ import { LemonMenuItemNode, LemonMenuProps, LemonMenuSection, - isLemonMenuSection, } from '../LemonMenu/LemonMenu' +import { PopoverProps } from '../Popover' +import { TooltipProps } from '../Tooltip' // Select options are basically menu items that handle onClick and active state internally interface LemonSelectOptionBase extends Omit { @@ -140,7 +142,7 @@ export function LemonSelect({ className={clsx(className, isClearButtonShown && 'LemonSelect--clearable')} icon={activeLeaf?.icon} // so that the pop-up isn't shown along with the close button - sideIcon={isClearButtonShown ?
    : undefined} + sideIcon={isClearButtonShown ? <> : undefined} type="secondary" status="stealth" {...buttonProps} diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss index 81162b59129b2..59b8e3bd4d44a 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss @@ -1,6 +1,7 @@ .LemonSelectMultiple { .ant-select { width: 100%; + .ant-select-selector, &.ant-select-single .ant-select-selector { min-height: 40px; @@ -9,7 +10,6 @@ font-size: 0.875rem; text-align: left; line-height: 1.25rem; - border: 1px solid var(--border); background: var(--bg-light); @@ -23,10 +23,11 @@ &:not(.ant-select-disabled):active { .ant-select-selector { background: var(--bg-light); - border-color: var(--primary); + border-color: var(--primary-3000); box-shadow: none; } } + &:not(.ant-select-disabled):active { .ant-select-selector { color: var(--primary-active); @@ -64,15 +65,20 @@ .LemonSelectMultipleDropdown { background: var(--bg-light); - padding: 0.5rem; border-radius: var(--radius); border: 1px solid var(--primary); - margin: -4px 0px; // Counteract antd wrapper + margin: -4px 0; // Counteract antd wrapper + padding: 0.5rem; + + .posthog-3000 & { + border: 1px solid var(--primary-3000); + } .ant-select-item { - padding: 0px; + padding: 0; background: none; padding-bottom: 0.2rem; + .ant-select-item-option-content { height: 40px; cursor: pointer; @@ -91,13 +97,14 @@ background: var(--primary-bg-active); } } + .ant-select-item-option-state { display: none; } } .ant-select-item-empty { - padding: 0px; + padding: 0; } .ant-select-item-option-content { @@ -110,6 +117,6 @@ align-items: center; gap: 0.5rem; height: 40px; - padding: 0px 0.25rem; + padding: 0 0.25rem; } } diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx index 049a85f9bfe0e..c360b9f86e796 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonSelectMultiple, LemonSelectMultipleProps } from './LemonSelectMultiple' -import { ProfilePicture } from '../ProfilePicture' import { capitalizeFirstLetter } from 'lib/utils' +import { useState } from 'react' + +import { ProfilePicture } from '../ProfilePicture' +import { LemonSelectMultiple, LemonSelectMultipleProps } from './LemonSelectMultiple' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx index 49bee6c0f3589..9c085799bb855 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx @@ -1,8 +1,9 @@ +import './LemonSelectMultiple.scss' + import { Select } from 'antd' -import { range } from 'lib/utils' -import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import './LemonSelectMultiple.scss' +import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' +import { range } from 'lib/utils' import { ReactNode } from 'react' export interface LemonSelectMultipleOption { @@ -88,7 +89,7 @@ export function LemonSelectMultiple({ const typedOnChange = onChange as (newValue: string | null) => void typedOnChange(typedValues) } else { - const typedValues = v.map((token) => token.toString().trim()) as string[] + const typedValues = v.map((token) => token.toString().trim()) const typedOnChange = onChange as (newValue: string[]) => void typedOnChange(typedValues) } diff --git a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.scss b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.scss index bbd07691933a2..29095c2aac936 100644 --- a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.scss +++ b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.scss @@ -4,12 +4,12 @@ background: linear-gradient( 90deg, - rgba(190, 190, 190, 0.2) 25%, - rgba(129, 129, 129, 0.24) 45%, - rgba(190, 190, 190, 0.2) 65% + rgb(190 190 190 / 20%) 25%, + rgb(129 129 129 / 24%) 45%, + rgb(190 190 190 / 20%) 65% ); background-size: 400% 100%; - animation: lemon-skeleton-loading 2s ease infinite; + animation: LemonSkeleton__shimmer 2s ease infinite; @media (prefers-reduced-motion) { animation: none; @@ -30,7 +30,7 @@ } } -@keyframes lemon-skeleton-loading { +@keyframes LemonSkeleton__shimmer { 0% { background-position: 100% 50%; } diff --git a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.stories.tsx b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.stories.tsx index 7bb37f0f5165b..26cd5a09660c8 100644 --- a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.stories.tsx @@ -1,9 +1,9 @@ import { Meta } from '@storybook/react' - -import { LemonSkeleton } from './LemonSkeleton' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonSkeleton } from './LemonSkeleton' + const meta: Meta = { title: 'Lemon UI/Lemon Skeleton', component: LemonSkeleton, @@ -54,7 +54,7 @@ export function Presets(): JSX.Element { } >
    - +
    diff --git a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.tsx b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.tsx index 2cac518029426..321e9c4071769 100644 --- a/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.tsx +++ b/frontend/src/lib/lemon-ui/LemonSkeleton/LemonSkeleton.tsx @@ -1,33 +1,20 @@ +import './LemonSkeleton.scss' + import clsx from 'clsx' -import { range } from 'lib/utils' import { LemonButtonProps } from 'lib/lemon-ui/LemonButton' -import './LemonSkeleton.scss' +import { range } from 'lib/utils' export interface LemonSkeletonProps { className?: string - /** Height of the skeleton bar */ - height?: number /** Repeat this component this many of times */ repeat?: number /** Used in combination with repeat to progressively fade out the repeated skeletons */ fade?: boolean active?: boolean } -export function LemonSkeleton({ - className, - repeat, - height = 4, - active = true, - fade = false, -}: LemonSkeletonProps): JSX.Element { +export function LemonSkeleton({ className, repeat, active = true, fade = false }: LemonSkeletonProps): JSX.Element { const content = ( -
    +
    {/* The span is for accessibility, but also because @storybook/test-runner smoke tests require content */} Loading…
    diff --git a/frontend/src/lib/lemon-ui/LemonSkeleton/index.ts b/frontend/src/lib/lemon-ui/LemonSkeleton/index.ts index c29e265fe801a..206f9202dc085 100644 --- a/frontend/src/lib/lemon-ui/LemonSkeleton/index.ts +++ b/frontend/src/lib/lemon-ui/LemonSkeleton/index.ts @@ -1,2 +1,2 @@ -export { LemonSkeleton } from './LemonSkeleton' export type { LemonSkeletonProps } from './LemonSkeleton' +export { LemonSkeleton } from './LemonSkeleton' diff --git a/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx b/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx index 67bd0ed1a1787..e7e2c9528687d 100644 --- a/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonSnack, LemonSnackProps } from './LemonSnack' + import { ProfilePicture } from '../ProfilePicture' +import { LemonSnack, LemonSnackProps } from './LemonSnack' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonSnack/index.ts b/frontend/src/lib/lemon-ui/LemonSnack/index.ts index 7e0c7cf81f3c3..b228c4eb995d1 100644 --- a/frontend/src/lib/lemon-ui/LemonSnack/index.ts +++ b/frontend/src/lib/lemon-ui/LemonSnack/index.ts @@ -1,2 +1,2 @@ -export { LemonSnack } from './LemonSnack' export type { LemonSnackProps } from './LemonSnack' +export { LemonSnack } from './LemonSnack' diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss index e3bc0a676f39e..c785fbc3c53b6 100644 --- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss +++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss @@ -1,4 +1,7 @@ .LemonSwitch { + --lemon-switch-height: 1.25rem; + --lemon-switch-width: 2.25rem; + width: fit-content; font-weight: 500; line-height: 1.5rem; @@ -46,6 +49,11 @@ 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 { @@ -53,8 +61,8 @@ display: inline-block; flex-shrink: 0; padding: 0; - width: 2.25rem; - height: 1.25rem; + width: var(--lemon-switch-width); + height: var(--lemon-switch-height); background: none; border: none; cursor: pointer; @@ -74,8 +82,22 @@ border-radius: 0.625rem; background-color: var(--border); transition: background-color 100ms ease; + + .posthog-3000 & { + border-radius: var(--lemon-switch-height); + height: 100%; + width: 100%; + top: 0; + pointer-events: none; + background-color: var(--border-bold); + } + .LemonSwitch--checked & { background-color: var(--primary-highlight); + + .posthog-3000 & { + background-color: var(--primary-3000); + } } } @@ -88,21 +110,55 @@ border-radius: 0.625rem; background-color: #fff; border: 2px solid var(--border); - transition: background-color 100ms ease, transform 100ms ease, border-color 100ms ease; + transition: background-color 100ms ease, transform 100ms ease, width 100ms ease, border-color 100ms ease; cursor: inherit; display: flex; align-items: center; justify-content: center; + .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)); + border: none; + pointer-events: none; + background-color: #fff; + } + .LemonSwitch--checked & { - transform: translateX(1rem); background-color: var(--primary); border-color: var(--primary); + transform: translateX(1rem); + + .posthog-3000 & { + transform: var(--lemon-switch-active-translate); + background-color: #fff; + } } + .LemonSwitch--active & { transform: scale(1.1); + + .posthog-3000 & { + --lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio) * 1.2); + + transform: none; + } } + .LemonSwitch--active.LemonSwitch--checked & { transform: translateX(1rem) scale(1.1); + + .posthog-3000 & { + transform: var(--lemon-switch-active-translate); + } } } diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.stories.tsx b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.stories.tsx index 8775d1f549972..29c7a81df6181 100644 --- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.stories.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { IconGlobeLock } from 'lib/lemon-ui/icons' +import { useState } from 'react' import { LemonSwitch as RawLemonSwitch, LemonSwitchProps } from './LemonSwitch' -import { IconGlobeLock } from 'lib/lemon-ui/icons' const LemonSwitch = ({ checked, ...props }: Partial): JSX.Element => { const [isChecked, setIsChecked] = useState(checked || false) diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx index 945a59523535b..b47ce51773ae1 100644 --- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx +++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx @@ -1,7 +1,8 @@ -import clsx from 'clsx' -import { useMemo, useState } from 'react' import './LemonSwitch.scss' + +import clsx from 'clsx' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useMemo, useState } from 'react' export interface LemonSwitchProps { className?: string @@ -79,7 +80,7 @@ export function LemonSwitch({ buttonComponent = ( {/* wrap it in a div so that the tooltip works even when disabled */} -
    {buttonComponent}
    +
    {buttonComponent}
    ) } diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss index fe0a5e5cda011..4bffbac72b20e 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss @@ -1,17 +1,28 @@ .LemonTable { position: relative; width: 100%; - background: var(--bg-light); + background: var(--bg-table); border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; flex: 1; + --row-base-height: 3rem; + + .posthog-3000 & { + --row-base-height: auto; + + font-size: 13px; + } + --row-horizontal-padding: 1rem; + &.LemonTable--with-ribbon { --row-ribbon-width: 0.25rem; + .LemonTable__content > table > tbody > tr > :first-child { position: relative; + &::after { content: ''; position: absolute; @@ -23,63 +34,86 @@ } } } + &--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 { border: none; border-radius: 0; background: none; } + &--borderless-rows { tr { border: none !important; } } + &--stealth { border: none; border-radius: 0; background: none; + .LemonTable__content > table > thead { background: none; border: none; } } + &.LemonTable--inset { --row-horizontal-padding: 0.5rem; } + .PaginationControl { height: var(--row-base-height); padding: 0.5rem; border-top: 1px solid var(--border); } + .row-name { display: flex; align-items: center; font-size: 0.875rem; font-weight: 600; + &:not(:last-child) { margin-bottom: 0.125rem; } } + .row-description { display: inline-block; max-width: 30rem; font-size: 0.75rem; } + + a.Link { + .posthog-3000 & { + color: var(--default); + + &:not(:disabled):hover { + color: var(--primary-3000-hover); + } + } + } } .LemonTable__content > table { width: 100%; border-collapse: collapse; border-spacing: 0; + > thead { position: relative; border-bottom: 1px solid var(--border); @@ -87,64 +121,107 @@ font-size: 0.75rem; letter-spacing: 0.03125rem; text-transform: uppercase; + + .posthog-3000 & { + background: none; + } + > tr { > th { font-weight: 700; text-align: left; + + .posthog-3000 & { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .LemonButton { + .posthog-3000 & { + margin: -0.5rem 0; + } + } } + &.LemonTable__row--grouping { --row-base-height: 2.5rem; // Make group headers smaller for better hierarchy } } } + > tbody { > tr { &.LemonTable__expansion { position: relative; background: var(--side); + > td { // Disable padding inside the expansion for better tailored placement of contents padding: 0 !important; } } + &.LemonTable__row--status-highlighted { background: var(--primary-bg-hover); color: var(--default); font-weight: 600; } + > td { + color: var(--text-secondary); padding-top: 0.5rem; padding-bottom: 0.5rem; + + .posthog-3000 & { + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + } + + .LemonButton { + .posthog-3000 & { + margin-top: -0.25rem; + margin-bottom: -0.25rem; + } + } } } } + > thead, > tbody { > tr { height: var(--row-base-height); + &:not(:first-child) { border-top: 1px solid var(--border); } + > th, > td { padding-right: var(--row-horizontal-padding); overflow: hidden; text-overflow: ellipsis; + &:first-child { padding-left: calc(var(--row-horizontal-padding) + var(--row-ribbon-width, 0px)); } + &.LemonTable__boundary:not(:first-child) { padding-left: var(--row-horizontal-padding); } + &.LemonTable__boundary:not(:first-of-type) { border-left: 1px solid var(--border); } + &.LemonTable__toggle { padding-right: 0; } + &.LemonTable__toggle + * { border-left: none !important; } + &[colspan='0'] { // Hidden cells should not affect the width of the table padding-left: 0 !important; @@ -170,6 +247,7 @@ opacity: 0; pointer-events: none; z-index: 2; + .LemonTable--loading & { opacity: 0.5; pointer-events: auto; @@ -178,8 +256,23 @@ .LemonTable__header { cursor: default; + + .posthog-3000 & { + opacity: 0.4; + } + &.LemonTable__header--actionable { cursor: pointer; + + .posthog-3000 & { + &:hover { + opacity: 0.7; + } + + &:active { + opacity: 0.9; + } + } } } @@ -188,6 +281,12 @@ align-items: center; justify-content: space-between; line-height: 1.5; + + div { + .posthog-3000 & { + white-space: nowrap; + } + } } .LemonTable__footer { @@ -203,6 +302,7 @@ body:not(.storybook-test-runner) { left: 0; z-index: 1; overflow: visible !important; + // Replicate .scrollable style for sticky cells &::before { transition: box-shadow 200ms ease; @@ -217,19 +317,23 @@ body:not(.storybook-test-runner) { box-shadow: -16px 0 16px 16px transparent; } } + .LemonTable__cell--sticky::before { background: var(--bg-light); } + tr.LemonTable__row--status-highlighted .LemonTable__cell--sticky::before { background: #e8ecff; // TRICKY: This is a one-off opaque form of --primary-bg-hover, keep in sync with source } + .LemonTable__header--sticky::before { background: var(--mid); } + .scrollable--left { .LemonTable__cell--sticky::before, .LemonTable__header--sticky::before { - box-shadow: -16px 0 16px 16px rgba(0, 0, 0, 0.25); + box-shadow: -16px 0 16px 16px rgb(0 0 0 / 25%); } } } diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx index 95a7903db1374..751114946a32f 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonTable, LemonTableProps } from './LemonTable' -import { LemonButton } from '../LemonButton' import { useEffect } from 'react' +import { LemonButton } from '../LemonButton' +import { LemonTable, LemonTableProps } from './LemonTable' + type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Lemon Table', diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx index b3c5deae98491..d39b9ba9e3896 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx @@ -1,18 +1,20 @@ +import './LemonTable.scss' + import clsx from 'clsx' import { useActions, useValues } from 'kea' import { router } from 'kea-router' +import { useScrollable } from 'lib/hooks/useScrollable' +import { IconInfo } from 'lib/lemon-ui/icons' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import React, { HTMLProps, useCallback, useEffect, useMemo, useState } from 'react' + +import { PaginationAuto, PaginationControl, PaginationManual, usePagination } from '../PaginationControl' import { Tooltip } from '../Tooltip' +import { LemonTableLoader } from './LemonTableLoader' +import { getNextSorting, Sorting, SortingIndicator } from './sorting' import { TableRow } from './TableRow' -import './LemonTable.scss' -import { Sorting, SortingIndicator, getNextSorting } from './sorting' import { ExpandableConfig, LemonTableColumn, LemonTableColumnGroup, LemonTableColumns } from './types' -import { PaginationAuto, PaginationControl, PaginationManual, usePagination } from '../PaginationControl' -import { useScrollable } from 'lib/hooks/useScrollable' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonTableLoader } from './LemonTableLoader' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { IconInfo } from 'lib/lemon-ui/icons' /** * Determine the column's key, using `dataIndex` as fallback. diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss index 30844284a9cd8..2f0d22e9fd1e8 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.scss @@ -1,15 +1,16 @@ .LemonTableLoader { - transition: height 200ms ease, top 200ms ease; - z-index: 10; - position: absolute; - left: 0; - padding: 0; - bottom: -1px; - width: 100%; - height: 0; background: var(--primary-bg-active); border: none !important; + bottom: -1px; + height: 0; + left: 0; overflow: hidden; + padding: 0.05rem !important; + position: absolute; + transition: height 200ms ease, top 200ms ease; + width: 100%; + z-index: 10; + &::after { content: ''; position: absolute; @@ -17,24 +18,31 @@ top: 0; width: 50%; height: 100%; - animation: loading-bar 1.5s linear infinite; + animation: LemonTableLoader__swooping 1.5s linear infinite; background: var(--primary); + + .posthog-3000 & { + background: var(--primary-3000); + } } + &.LemonTableLoader--enter-active, &.LemonTableLoader--enter-done { - height: 0.25rem; + height: 0.125rem; } } -@keyframes loading-bar { +@keyframes LemonTableLoader__swooping { 0% { left: 0; width: 33.333%; transform: translateX(-100%); } + 50% { width: 50%; } + 100% { left: 100%; width: 33.333%; diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.tsx index 2b50878fb1e07..3b79a8651a47a 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTableLoader.tsx @@ -1,6 +1,7 @@ -import { CSSTransition } from 'react-transition-group' import './LemonTableLoader.scss' + import React from 'react' +import { CSSTransition } from 'react-transition-group' export function LemonTableLoader({ loading = false, diff --git a/frontend/src/lib/lemon-ui/LemonTable/TableCellSparkline.tsx b/frontend/src/lib/lemon-ui/LemonTable/TableCellSparkline.tsx index 226809d3c8b43..8a77bc49345f5 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/TableCellSparkline.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/TableCellSparkline.tsx @@ -1,10 +1,10 @@ -import { useEffect, useRef, useState } from 'react' -import { Chart, ChartItem, TooltipModel } from 'lib/Chart' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { offset } from '@floating-ui/react' - import './TableCellSparkline.scss' + +import { offset } from '@floating-ui/react' +import { Chart, ChartItem, TooltipModel } from 'lib/Chart' import { getColorVar } from 'lib/colors' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { useEffect, useRef, useState } from 'react' export function TableCellSparkline({ labels, data }: { labels?: string[]; data: number[] }): JSX.Element { const canvasRef = useRef(null) diff --git a/frontend/src/lib/lemon-ui/LemonTable/TableRow.tsx b/frontend/src/lib/lemon-ui/LemonTable/TableRow.tsx index 615c7c6afbb44..281a73ff3519a 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/TableRow.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/TableRow.tsx @@ -1,8 +1,9 @@ -import React, { HTMLProps, useState } from 'react' +import clsx from 'clsx' import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import React, { HTMLProps, useState } from 'react' + import { ExpandableConfig, LemonTableColumnGroup, TableCellRepresentation } from './types' -import clsx from 'clsx' export interface TableRowProps> { record: T diff --git a/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx b/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx index 608c0b0b70657..d91e1ff31b3d7 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/columnUtils.tsx @@ -1,8 +1,10 @@ import { TZLabel } from 'lib/components/TZLabel' +import { Dayjs, dayjs } from 'lib/dayjs' + +import { UserBasicType } from '~/types' + import { ProfilePicture } from '../ProfilePicture' import { LemonTableColumn } from './types' -import { UserBasicType } from '~/types' -import { Dayjs, dayjs } from 'lib/dayjs' export function createdAtColumn(): LemonTableColumn { return { diff --git a/frontend/src/lib/lemon-ui/LemonTable/index.ts b/frontend/src/lib/lemon-ui/LemonTable/index.ts index ce12c3ad40fd3..f375acd2dffa5 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/index.ts +++ b/frontend/src/lib/lemon-ui/LemonTable/index.ts @@ -1,4 +1,4 @@ -export { LemonTable } from './LemonTable' export type { LemonTableProps } from './LemonTable' +export { LemonTable } from './LemonTable' export type { Sorting } from './sorting' export type { ExpandableConfig, LemonTableColumn, LemonTableColumnGroup, LemonTableColumns } from './types' diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss index a5b58dcd27935..967a0add69417 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.scss @@ -3,6 +3,7 @@ display: flex; flex-direction: column; align-self: stretch; + .Navigation3000__scene > &:first-child, .Navigation3000__scene > :first-child > &:first-child { margin-top: -0.75rem; @@ -18,6 +19,7 @@ flex-direction: row; align-items: stretch; overflow-x: auto; + &::before { // The bottom border content: ''; @@ -28,6 +30,7 @@ width: 100%; background: var(--border); } + &::after { // The active tab slider content: ''; @@ -38,6 +41,7 @@ width: var(--lemon-tabs-slider-width); transform: translateX(var(--lemon-tabs-slider-offset)); background: var(--link); + .LemonTabs--transitioning & { transition: width 200ms ease, transform 200ms ease; } @@ -48,19 +52,24 @@ .LemonTabs--transitioning & { transition: color 200ms ease; } + &:not(:last-child) { margin-right: 2rem; } + &:hover { color: var(--link); } + &:active { - color: var(--primary-dark); + color: var(--primary-3000-active); } + &.LemonTabs__tab--active { color: var(--link); text-shadow: 0 0 0.25px currentColor; // Simulate increased weight without affecting width } + a { // Make tab labels that are links the same colors as regular tab labels text-decoration: none; @@ -70,8 +79,9 @@ } .LemonTabs__tab-content { - display: flex; align-items: center; - padding: 0.75rem 0; cursor: pointer; + display: flex; + padding: 0.75rem 0; + white-space: nowrap; } diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx index 9d1e6532cecfc..9618ab6215934 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useState } from 'react' + import { LemonTab, LemonTabs as LemonTabsComponent } from './LemonTabs' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx index 873c47327ddbb..d94f9732623cd 100644 --- a/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx +++ b/frontend/src/lib/lemon-ui/LemonTabs/LemonTabs.tsx @@ -1,10 +1,12 @@ +import './LemonTabs.scss' + import clsx from 'clsx' import { AlignType } from 'rc-trigger/lib/interface' + import { useSliderPositioning } from '../hooks' import { IconInfo } from '../icons' -import { Tooltip } from '../Tooltip' -import './LemonTabs.scss' import { Link } from '../Link' +import { Tooltip } from '../Tooltip' /** A tab that represents one of the options, but doesn't have any content. Render tab-dependent UI yourself. */ export interface AbstractLemonTab { diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss index 055db35a70146..348d596a1cec3 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss @@ -1,48 +1,99 @@ .LemonTag { - font-size: 0.75rem; - font-weight: var(--font-medium); + align-items: center; background: var(--border); - padding: 0.125rem 0.25rem; border-radius: var(--radius); - display: inline-flex; - align-items: center; color: var(--default); + display: inline-flex; + font-size: 0.75rem; + font-weight: var(--font-medium); line-height: 1rem; + padding: 0.125rem 0.25rem; white-space: nowrap; + .posthog-3000 & { + background: none; + border-radius: calc(var(--radius) * 0.75); + border-style: solid; + border-width: 1px; + font-size: 0.688rem; + padding: 0.075rem 0.25rem; + } + &.primary { - background-color: var(--primary); + background-color: var(--primary-3000); color: #fff; + + .posthog-3000 & { + background: none; + border-color: var(--primary-3000); + color: var(--primary-3000); + } } &.highlight { background-color: var(--mark); color: var(--bg-charcoal); + + .posthog-3000 & { + background: none; + border-color: var(--mark); + color: var(--mark); + } } &.warning { background-color: var(--warning); color: var(--bg-charcoal); + + .posthog-3000 & { + background: none; + border-color: var(--warning); + color: var(--warning); + } } &.danger { background-color: var(--danger); color: #fff; + + .posthog-3000 & { + background: none; + border-color: var(--danger); + color: var(--danger); + } } &.success { background-color: var(--success); color: #fff; + + .posthog-3000 & { + background: none; + border-color: var(--success); + color: var(--success); + } } &.completion { background-color: var(--purple-light); color: var(--bg-charcoal); + + .posthog-3000 & { + background: none; + border-color: var(--purple-light); + color: var(--purple-light); + } } &.caution { background-color: var(--danger-lighter); color: var(--bg-charcoal); + + .posthog-3000 & { + background: none; + border-color: var(--danger-lighter); + color: var(--danger-lighter); + } } &.none { @@ -62,7 +113,7 @@ .LemonTag__right-button { margin-left: 0.25rem; - min-height: 1.5rem; - padding: 0.125rem 0.125rem !important; + min-height: 1.5rem !important; + padding: 0.125rem !important; } } diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx index 1c5403b05f0c8..bf68c9db7d5a0 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { LemonTag as LemonTagComponent, LemonTagType } from './LemonTag' type Story = StoryObj diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx index 3d7c36494ab4a..564509e905c65 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx @@ -1,8 +1,9 @@ +import './LemonTag.scss' + import clsx from 'clsx' import { IconClose, IconEllipsis } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { LemonButtonDropdown } from 'lib/lemon-ui/LemonButton/LemonButton' -import './LemonTag.scss' export type LemonTagType = | 'primary' diff --git a/frontend/src/lib/lemon-ui/LemonTag/index.ts b/frontend/src/lib/lemon-ui/LemonTag/index.ts index 217aec8ebd633..540d69cfadf7b 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/index.ts +++ b/frontend/src/lib/lemon-ui/LemonTag/index.ts @@ -1,2 +1,2 @@ -export { LemonTag } from './LemonTag' export type { LemonTagProps, LemonTagType } from './LemonTag' +export { LemonTag } from './LemonTag' diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss index 389975e57915a..ccc45dc5f36a2 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss @@ -15,7 +15,7 @@ display: block; &:not(:disabled):hover { - border: 1px solid var(--primary-light); + border: 1px solid var(--primary-3000-hover); } width: 100%; @@ -27,7 +27,7 @@ } &:focus:not(:disabled) { - border: 1px solid var(--primary); + border: 1px solid var(--primary-3000); } .Field--error & { diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx index ea0f7ee62571a..c0e55962e477c 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { useState } from 'react' -import { LemonTextArea, LemonTextAreaProps, LemonTextAreaMarkdown as _LemonTextMarkdown } from './LemonTextArea' +import { LemonTextArea, LemonTextAreaMarkdown as _LemonTextMarkdown, LemonTextAreaProps } from './LemonTextArea' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx index 77c94c64aad0f..dbc2181d76b05 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx @@ -1,18 +1,20 @@ import './LemonTextArea.scss' -import React, { createRef, useRef, useState } from 'react' + import clsx from 'clsx' -import TextareaAutosize from 'react-textarea-autosize' -import { IconMarkdown, IconTools } from 'lib/lemon-ui/icons' +import { useValues } from 'kea' import { TextContent } from 'lib/components/Cards/TextCard/TextCard' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import posthog from 'posthog-js' +import { useUploadFiles } from 'lib/hooks/useUploadFiles' +import { IconMarkdown, IconTools } from 'lib/lemon-ui/icons' import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' -import { useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import posthog from 'posthog-js' +import React, { createRef, useRef, useState } from 'react' +import TextareaAutosize from 'react-textarea-autosize' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { LemonTabs } from '../LemonTabs' -import { useUploadFiles } from 'lib/hooks/useUploadFiles' export interface LemonTextAreaProps extends Pick< diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/index.ts b/frontend/src/lib/lemon-ui/LemonTextArea/index.ts index 9276620a4db25..138d5df39dcf0 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/index.ts +++ b/frontend/src/lib/lemon-ui/LemonTextArea/index.ts @@ -1,2 +1,2 @@ -export { LemonTextArea, LemonTextAreaMarkdown } from './LemonTextArea' export type { LemonTextAreaProps } from './LemonTextArea' +export { LemonTextArea, LemonTextAreaMarkdown } from './LemonTextArea' diff --git a/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx b/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx index 232a68b23fd12..9747b5c249a11 100644 --- a/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx +++ b/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx @@ -1,8 +1,10 @@ -import { LemonButton } from '../LemonButton' -import { IconClose } from '../icons' import './LemonWidget.scss' + import clsx from 'clsx' +import { IconClose } from '../icons' +import { LemonButton } from '../LemonButton' + export interface LemonWidgetProps { title: string onClose?: () => void diff --git a/frontend/src/lib/lemon-ui/LemonWidget/index.ts b/frontend/src/lib/lemon-ui/LemonWidget/index.ts index 3577ddd8cb037..eee1745c27a29 100644 --- a/frontend/src/lib/lemon-ui/LemonWidget/index.ts +++ b/frontend/src/lib/lemon-ui/LemonWidget/index.ts @@ -1,2 +1,2 @@ -export { LemonWidget } from './LemonWidget' export type { LemonWidgetProps } from './LemonWidget' +export { LemonWidget } from './LemonWidget' diff --git a/frontend/src/lib/lemon-ui/Lettermark/Lettermark.stories.tsx b/frontend/src/lib/lemon-ui/Lettermark/Lettermark.stories.tsx index 2a1eb5aa8a757..292fc93b2c71b 100644 --- a/frontend/src/lib/lemon-ui/Lettermark/Lettermark.stories.tsx +++ b/frontend/src/lib/lemon-ui/Lettermark/Lettermark.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { Lettermark, LettermarkColor, LettermarkProps } from './Lettermark' import { range } from 'lib/utils' +import { Lettermark, LettermarkColor, LettermarkProps } from './Lettermark' + type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Lettermark', diff --git a/frontend/src/lib/lemon-ui/Lettermark/Lettermark.tsx b/frontend/src/lib/lemon-ui/Lettermark/Lettermark.tsx index a60be3adaa15f..e0d88847ec6f6 100644 --- a/frontend/src/lib/lemon-ui/Lettermark/Lettermark.tsx +++ b/frontend/src/lib/lemon-ui/Lettermark/Lettermark.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx' import './Lettermark.scss' +import clsx from 'clsx' + // This is the number of known --lettermark-* variables in `globals.scss` const NUM_LETTERMARK_STYLES = 8 diff --git a/frontend/src/lib/lemon-ui/Lettermark/index.ts b/frontend/src/lib/lemon-ui/Lettermark/index.ts index 3f8c05eaa269e..ef89108074f7e 100644 --- a/frontend/src/lib/lemon-ui/Lettermark/index.ts +++ b/frontend/src/lib/lemon-ui/Lettermark/index.ts @@ -1,2 +1,2 @@ -export { Lettermark, LettermarkColor } from './Lettermark' export type { LettermarkProps } from './Lettermark' +export { Lettermark, LettermarkColor } from './Lettermark' diff --git a/frontend/src/lib/lemon-ui/Link/Link.scss b/frontend/src/lib/lemon-ui/Link/Link.scss index a07b371b26d90..2500c9b62debe 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.scss +++ b/frontend/src/lib/lemon-ui/Link/Link.scss @@ -1,21 +1,23 @@ .Link { - transition: color 200ms ease, opacity 200ms ease; background: none; - color: var(--link); border: none; + color: var(--link); + cursor: pointer; + line-height: inherit; outline: none; padding: 0; - line-height: inherit; - cursor: pointer; + transition: none; &:not(:disabled) { &:hover { - color: var(--primary-light); + color: var(--primary-3000-hover); } + &:active { - color: var(--primary-dark); + color: var(--primary-3000-active); } } + &:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; diff --git a/frontend/src/lib/lemon-ui/Link/Link.stories.tsx b/frontend/src/lib/lemon-ui/Link/Link.stories.tsx index 38142d11ac702..638030e19d0e7 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.stories.tsx +++ b/frontend/src/lib/lemon-ui/Link/Link.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { Link, LinkProps } from './Link' import { urls } from 'scenes/urls' +import { Link, LinkProps } from './Link' + type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Link', diff --git a/frontend/src/lib/lemon-ui/Link/Link.tsx b/frontend/src/lib/lemon-ui/Link/Link.tsx index 12c1c9e966a47..4677818b2b529 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.tsx +++ b/frontend/src/lib/lemon-ui/Link/Link.tsx @@ -1,16 +1,18 @@ -import React from 'react' -import { router } from 'kea-router' -import { isExternalLink } from 'lib/utils' -import clsx from 'clsx' import './Link.scss' -import { IconOpenInNew } from '../icons' -import { Tooltip } from '../Tooltip' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' -import { useActions } from 'kea' +import clsx from 'clsx' +import { useActions } from 'kea' +import { router } from 'kea-router' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { isExternalLink } from 'lib/utils' +import React from 'react' import { useNotebookDrag } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' + import { sidePanelDocsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic' +import { IconOpenInNew } from '../icons' +import { Tooltip } from '../Tooltip' + type RoutePart = string | Record export type LinkProps = Pick, 'target' | 'className' | 'children' | 'title'> & { @@ -82,7 +84,6 @@ export const Link: React.FC> = Reac href: typeof to === 'string' ? to : undefined, }) - const docsPanelEnabled = useFeatureFlag('SIDE_PANEL_DOCS') const is3000 = useFeatureFlag('POSTHOG_3000') const { openDocsPage } = useActions(sidePanelDocsLogic) @@ -99,7 +100,7 @@ export const Link: React.FC> = Reac return } - if (typeof to === 'string' && is3000 && docsPanelEnabled && isPostHogComDomain(to)) { + if (typeof to === 'string' && is3000 && isPostHogComDomain(to)) { event.preventDefault() openDocsPage(to) return diff --git a/frontend/src/lib/lemon-ui/Link/index.ts b/frontend/src/lib/lemon-ui/Link/index.ts index ca0be19dee84a..0442a73e1dbca 100644 --- a/frontend/src/lib/lemon-ui/Link/index.ts +++ b/frontend/src/lib/lemon-ui/Link/index.ts @@ -1,2 +1,2 @@ -export { Link } from './Link' export type { LinkProps } from './Link' +export { Link } from './Link' diff --git a/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.stories.tsx b/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.stories.tsx index b2522a0b08c7a..8953d363d7daf 100644 --- a/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.stories.tsx +++ b/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' + import { PaginationControl, PaginationControlProps } from './PaginationControl' import { usePagination } from './usePagination' diff --git a/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.tsx b/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.tsx index b26cfdb485f5c..578c0c5f648b7 100644 --- a/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.tsx +++ b/frontend/src/lib/lemon-ui/PaginationControl/PaginationControl.tsx @@ -1,8 +1,10 @@ +import './PaginationControl.scss' + +import clsx from 'clsx' import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './PaginationControl.scss' + import { PaginationState } from './types' -import clsx from 'clsx' export interface PaginationControlProps extends PaginationState { nouns?: [string, string] diff --git a/frontend/src/lib/lemon-ui/PaginationControl/index.ts b/frontend/src/lib/lemon-ui/PaginationControl/index.ts index 4328e3614fc19..7ec775564dc7e 100644 --- a/frontend/src/lib/lemon-ui/PaginationControl/index.ts +++ b/frontend/src/lib/lemon-ui/PaginationControl/index.ts @@ -1,4 +1,4 @@ -export { PaginationControl } from './PaginationControl' -export { usePagination } from './usePagination' export type { PaginationControlProps } from './PaginationControl' +export { PaginationControl } from './PaginationControl' export type { PaginationAuto, PaginationManual, PaginationState } from './types' +export { usePagination } from './usePagination' diff --git a/frontend/src/lib/lemon-ui/PaginationControl/usePagination.ts b/frontend/src/lib/lemon-ui/PaginationControl/usePagination.ts index 24beda803877b..8b8e74350ecd4 100644 --- a/frontend/src/lib/lemon-ui/PaginationControl/usePagination.ts +++ b/frontend/src/lib/lemon-ui/PaginationControl/usePagination.ts @@ -1,6 +1,7 @@ import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { useCallback, useMemo } from 'react' + import { PaginationAuto, PaginationManual, PaginationState } from './types' export function usePagination( diff --git a/frontend/src/lib/lemon-ui/Popover/Popover.scss b/frontend/src/lib/lemon-ui/Popover/Popover.scss index 3e53c125cfb19..5dbb894d99043 100644 --- a/frontend/src/lib/lemon-ui/Popover/Popover.scss +++ b/frontend/src/lib/lemon-ui/Popover/Popover.scss @@ -8,9 +8,11 @@ h5 { margin: 0.25rem 0.5rem; } + p:last-child { margin-bottom: 0; } + &[data-floating-placement^='top'] { perspective-origin: bottom; } @@ -36,7 +38,7 @@ opacity: 0; .Popover--actionable & { - border-color: var(--primary); + border-color: var(--primary-3000); } // We set the offset below instead of using floating-ui's offset(), because we need there to be no gap between @@ -45,7 +47,9 @@ .Popover[data-placement^='bottom'] & { transform-origin: top; margin-top: 0.25rem; + transform: rotateX(-6deg); } + .Popover[data-placement^='bottom'].Popover--with-arrow & { margin-top: 0.5rem; } @@ -53,7 +57,9 @@ .Popover[data-placement^='top'] & { transform-origin: bottom; margin-bottom: 0.25rem; + transform: rotateX(6deg); } + .Popover[data-placement^='top'].Popover--with-arrow & { margin-bottom: 0.5rem; } @@ -61,7 +67,9 @@ .Popover[data-placement^='left'] & { transform-origin: right; margin-right: 0.25rem; + transform: rotateY(-6deg); } + .Popover[data-placement^='left'].Popover--with-arrow & { margin-right: 0.5rem; } @@ -69,27 +77,13 @@ .Popover[data-placement^='right'] & { transform-origin: left; margin-left: 0.25rem; + transform: rotateY(6deg); } + .Popover[data-placement^='right'].Popover--with-arrow & { margin-left: 0.5rem; } - .Popover[data-placement^='bottom'] & { - transform: rotateX(-6deg); - } - - .Popover[data-placement^='top'] & { - transform: rotateX(6deg); - } - - .Popover[data-placement^='left'] & { - transform: rotateY(-6deg); - } - - .Popover[data-placement^='right'] & { - transform: rotateY(6deg); - } - .Popover.Popover--enter-active &, .Popover.Popover--enter-done & { opacity: 1; @@ -101,9 +95,10 @@ } .posthog-3000 & { - background: var(--bg-3000); + background: var(--bg-light); padding: 0.25rem; } + .posthog-3000 .Popover--actionable & { border-color: var(--border); } @@ -141,7 +136,7 @@ } .Popover--actionable & { - border-color: var(--primary); + border-color: var(--primary-3000); } } diff --git a/frontend/src/lib/lemon-ui/Popover/Popover.stories.tsx b/frontend/src/lib/lemon-ui/Popover/Popover.stories.tsx index 477f44fd52d9f..dba48ee486eb8 100644 --- a/frontend/src/lib/lemon-ui/Popover/Popover.stories.tsx +++ b/frontend/src/lib/lemon-ui/Popover/Popover.stories.tsx @@ -1,18 +1,13 @@ -import { StoryFn, Meta, StoryObj } from '@storybook/react' +import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { IconArrowDropDown } from 'lib/lemon-ui/icons' import { Popover } from './Popover' -import { IconArrowDropDown } from 'lib/lemon-ui/icons' type Story = StoryObj const meta: Meta = { title: 'Lemon UI/Popover', component: Popover, - parameters: { - testOptions: { - skip: true, // FIXME: This story needs a play test for the popup to show up in snapshots - }, - }, - tags: ['autodocs'], + tags: ['autodocs', 'test-skip'], // FIXME: This story needs a play test for the popup to show up in snapshots } export default meta diff --git a/frontend/src/lib/lemon-ui/Popover/Popover.tsx b/frontend/src/lib/lemon-ui/Popover/Popover.tsx index 2081fac8edb85..f77c7c6e49a42 100644 --- a/frontend/src/lib/lemon-ui/Popover/Popover.tsx +++ b/frontend/src/lib/lemon-ui/Popover/Popover.tsx @@ -1,22 +1,23 @@ import './Popover.scss' -import React, { MouseEventHandler, ReactElement, useContext, useEffect, useLayoutEffect, useRef } from 'react' -import { CLICK_OUTSIDE_BLOCK_CLASS, useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler' -import clsx from 'clsx' + import { - useFloating, + arrow, autoUpdate, + flip, + FloatingPortal, Middleware, Placement, shift, - flip, size, - arrow, - FloatingPortal, + useFloating, UseFloatingReturn, useMergeRefs, } from '@floating-ui/react' -import { CSSTransition } from 'react-transition-group' +import clsx from 'clsx' import { useEventListener } from 'lib/hooks/useEventListener' +import { CLICK_OUTSIDE_BLOCK_CLASS, useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler' +import React, { MouseEventHandler, ReactElement, useContext, useEffect, useLayoutEffect, useRef } from 'react' +import { CSSTransition } from 'react-transition-group' export interface PopoverProps { ref?: React.MutableRefObject | React.Ref | null diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.stories.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.stories.tsx index 821b2abfc42e5..96c6d6fa7f1f4 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.stories.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.stories.tsx @@ -1,7 +1,8 @@ -import { ProfileBubbles as ProfileBubblesComponent, ProfileBubblesProps } from './ProfileBubbles' import { Meta } from '@storybook/react' import { alphabet, range } from 'lib/utils' +import { ProfileBubbles as ProfileBubblesComponent, ProfileBubblesProps } from './ProfileBubbles' + const DUMMIES: ProfileBubblesProps['people'] = [ { email: 'michael@posthog.com', name: 'Michael' }, { email: 'lottie@posthog.com', name: 'Lottie' }, diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx index f9cbd5ff25255..23cd6b167efeb 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfileBubbles.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx' -import { ProfilePicture } from '.' + import { Tooltip } from '../Tooltip' +import { ProfilePicture } from '.' export interface ProfileBubblesProps extends React.HTMLProps { people: { email: string; name?: string; title?: string }[] diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.scss b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.scss index 9185ae635f9a8..d3f5896baebca 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.scss +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.scss @@ -1,6 +1,7 @@ .profile-package { display: inline-flex; align-items: center; + &:not(:first-child) { margin-left: 0.375rem; } @@ -75,9 +76,11 @@ .ProfileBubbles { display: flex; align-items: center; + > * { outline: 0.125rem solid var(--bg-light); } + > :not(:first-child) { margin-left: -0.125rem; } @@ -90,7 +93,7 @@ height: 1.5rem; width: 1.5rem; border-radius: 50%; - background: var(--primary); + background: var(--primary-3000); color: #fff; font-size: 0.625rem; font-weight: 600; diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx index 4e855d4ba00b7..07fb82adffecd 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx @@ -1,12 +1,14 @@ +import './ProfilePicture.scss' + import clsx from 'clsx' import { useValues } from 'kea' +import { inStorybookTestRunner } from 'lib/utils' import md5 from 'md5' import { useEffect, useState } from 'react' import { userLogic } from 'scenes/userLogic' + import { IconRobot } from '../icons' import { Lettermark, LettermarkColor } from '../Lettermark/Lettermark' -import './ProfilePicture.scss' -import { inStorybookTestRunner } from 'lib/utils' export interface ProfilePictureProps { name?: string @@ -47,7 +49,7 @@ export function ProfilePicture({ const emailHash = md5(emailOrNameWithEmail.trim().toLowerCase()) const tentativeUrl = `https://www.gravatar.com/avatar/${emailHash}?s=96&d=404` // The image will be cached, so it's best to do GET request check before trying to render it - fetch(tentativeUrl).then((response) => { + void fetch(tentativeUrl).then((response) => { if (response.status === 200) { setGravatarUrl(tentativeUrl) } diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/index.ts b/frontend/src/lib/lemon-ui/ProfilePicture/index.ts index 00937f134c370..2cd1f985d3e99 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/index.ts +++ b/frontend/src/lib/lemon-ui/ProfilePicture/index.ts @@ -1,4 +1,4 @@ -export { ProfilePicture } from './ProfilePicture' -export type { ProfilePictureProps } from './ProfilePicture' -export { ProfileBubbles } from './ProfileBubbles' export type { ProfileBubblesProps } from './ProfileBubbles' +export { ProfileBubbles } from './ProfileBubbles' +export type { ProfilePictureProps } from './ProfilePicture' +export { ProfilePicture } from './ProfilePicture' diff --git a/frontend/src/lib/lemon-ui/Spinner/Spinner.scss b/frontend/src/lib/lemon-ui/Spinner/Spinner.scss index 2e817a93317e0..9504dd20eb1c4 100644 --- a/frontend/src/lib/lemon-ui/Spinner/Spinner.scss +++ b/frontend/src/lib/lemon-ui/Spinner/Spinner.scss @@ -4,7 +4,9 @@ width: 1em; height: 1em; flex-shrink: 0; + --spinner-color: var(--brand-blue); + &.Spinner--textColored { --spinner-color: currentColor; } @@ -12,6 +14,7 @@ .Spinner__layer { transform-origin: center; + > circle { display: block; fill: transparent; @@ -20,23 +23,27 @@ stroke: var(--spinner-color); stroke-linecap: round; } + &:nth-child(1) { opacity: 0.333; } + &:nth-child(2) { animation: spin 1s infinite linear; + > circle { - animation: writhe 1.5s infinite ease both; + animation: Spinner__writhe 1.5s infinite ease both; } } } -@keyframes writhe { +@keyframes Spinner__writhe { 0%, 100% { stroke-dashoffset: -60; stroke-dasharray: 70; } + 50% { stroke-dashoffset: -30; stroke-dasharray: 70; @@ -46,33 +53,31 @@ .SpinnerOverlay { transition: opacity 0.2s ease; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; + inset: 0; text-align: center; min-height: 6rem; z-index: var(--z-content-overlay); display: flex; align-items: center; justify-content: center; + &[aria-hidden='true'] { opacity: 0; pointer-events: none; } + &::before { content: ''; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; + inset: 0; background: var(--bg-light); opacity: 0.5; } + .Spinner { position: relative; } + .posthog-3000 &.SpinnerOverlay--scene-level::before { background: var(--bg-3000); } diff --git a/frontend/src/lib/lemon-ui/Spinner/Spinner.stories.tsx b/frontend/src/lib/lemon-ui/Spinner/Spinner.stories.tsx index 1ebec0c0a5213..e1625461109a8 100644 --- a/frontend/src/lib/lemon-ui/Spinner/Spinner.stories.tsx +++ b/frontend/src/lib/lemon-ui/Spinner/Spinner.stories.tsx @@ -1,7 +1,7 @@ +import { LemonButton } from '@posthog/lemon-ui' import { Meta } from '@storybook/react' import { Spinner as Spinner, SpinnerOverlay } from './Spinner' -import { LemonButton } from '@posthog/lemon-ui' const meta: Meta = { title: 'Lemon UI/Spinner', diff --git a/frontend/src/lib/lemon-ui/Spinner/Spinner.tsx b/frontend/src/lib/lemon-ui/Spinner/Spinner.tsx index 3c613721602bb..5939bc14ec114 100644 --- a/frontend/src/lib/lemon-ui/Spinner/Spinner.tsx +++ b/frontend/src/lib/lemon-ui/Spinner/Spinner.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx' import './Spinner.scss' +import clsx from 'clsx' + export interface SpinnerProps { textColored?: boolean className?: string diff --git a/frontend/src/lib/lemon-ui/Splotch/Splotch.scss b/frontend/src/lib/lemon-ui/Splotch/Splotch.scss index 5f1a03bd642e2..f183e17b5a6d8 100644 --- a/frontend/src/lib/lemon-ui/Splotch/Splotch.scss +++ b/frontend/src/lib/lemon-ui/Splotch/Splotch.scss @@ -8,18 +8,23 @@ width: 1rem; height: 1rem; border-radius: var(--radius); + .Splotch--blue & { background: var(--blue); } + .Splotch--purple & { background: var(--purple); } + .Splotch--green & { background: var(--green); } + .Splotch--black & { background: var(--black); } + .Splotch--white & { background: #fff; border: 1px solid var(--border); diff --git a/frontend/src/lib/lemon-ui/Splotch/Splotch.stories.tsx b/frontend/src/lib/lemon-ui/Splotch/Splotch.stories.tsx index 821f6d29141a0..a61e437ed1ae3 100644 --- a/frontend/src/lib/lemon-ui/Splotch/Splotch.stories.tsx +++ b/frontend/src/lib/lemon-ui/Splotch/Splotch.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn } from '@storybook/react' + import { Splotch, SplotchColor, SplotchProps } from './Splotch' const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/Splotch/Splotch.tsx b/frontend/src/lib/lemon-ui/Splotch/Splotch.tsx index e8a91a09f7b21..e3f9a2bbac587 100644 --- a/frontend/src/lib/lemon-ui/Splotch/Splotch.tsx +++ b/frontend/src/lib/lemon-ui/Splotch/Splotch.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx' import './Splotch.scss' +import clsx from 'clsx' + export enum SplotchColor { Purple = 'purple', Blue = 'blue', diff --git a/frontend/src/lib/lemon-ui/Tooltip/Tooltip.tsx b/frontend/src/lib/lemon-ui/Tooltip/Tooltip.tsx index 04ebbd7e00412..87b6182f61d81 100644 --- a/frontend/src/lib/lemon-ui/Tooltip/Tooltip.tsx +++ b/frontend/src/lib/lemon-ui/Tooltip/Tooltip.tsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react' -// eslint-disable-next-line no-restricted-imports import { Tooltip as AntdTooltip } from 'antd' import { TooltipProps as AntdTooltipProps } from 'antd/lib/tooltip' +import React, { useState } from 'react' import { useDebounce } from 'use-debounce' const DEFAULT_DELAY_MS = 500 diff --git a/frontend/src/lib/lemon-ui/Tooltip/index.ts b/frontend/src/lib/lemon-ui/Tooltip/index.ts index eaca424fb1663..31867601a614d 100644 --- a/frontend/src/lib/lemon-ui/Tooltip/index.ts +++ b/frontend/src/lib/lemon-ui/Tooltip/index.ts @@ -1,2 +1,2 @@ -export { Tooltip } from './Tooltip' export type { TooltipProps } from './Tooltip' +export { Tooltip } from './Tooltip' diff --git a/frontend/src/lib/lemon-ui/colors.stories.tsx b/frontend/src/lib/lemon-ui/colors.stories.tsx index d8d9573ac68b0..304661610cb2e 100644 --- a/frontend/src/lib/lemon-ui/colors.stories.tsx +++ b/frontend/src/lib/lemon-ui/colors.stories.tsx @@ -1,7 +1,8 @@ import { Meta } from '@storybook/react' -import { Popover } from './Popover/Popover' import { useState } from 'react' + import { LemonTable } from './LemonTable' +import { Popover } from './Popover/Popover' const meta: Meta = { title: 'Lemon UI/Colors', diff --git a/frontend/src/lib/lemon-ui/icons/icons.stories.tsx b/frontend/src/lib/lemon-ui/icons/icons.stories.tsx index cb09face4657f..2c7c61429d41f 100644 --- a/frontend/src/lib/lemon-ui/icons/icons.stories.tsx +++ b/frontend/src/lib/lemon-ui/icons/icons.stories.tsx @@ -1,9 +1,10 @@ -import * as React from 'react' -import * as icons from './icons' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonTable } from 'lib/lemon-ui/LemonTable' -import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonTable } from 'lib/lemon-ui/LemonTable' +import * as React from 'react' + +import * as icons from './icons' const { IconGauge, IconWithCount } = icons @@ -104,7 +105,7 @@ const LibraryTemplate: StoryFn<{ letter?: string | null }> = ({ letter }) => { // This is for actual Storybook users export const Library: LibraryType = LibraryTemplate.bind({}) -Library.parameters = { testOptions: { skip: true } } +Library.tags = ['autodocs', 'test-skip'] // These are just for snapshots. As opposed to the full library, the stories below are segmented by the first letter // of the icon name, which greatly optimizes both the UX and storage aspects of diffing snapshots. diff --git a/frontend/src/lib/lemon-ui/icons/icons.tsx b/frontend/src/lib/lemon-ui/icons/icons.tsx index 429bd17c3c6eb..aa04067a78ba7 100644 --- a/frontend/src/lib/lemon-ui/icons/icons.tsx +++ b/frontend/src/lib/lemon-ui/icons/icons.tsx @@ -1,8 +1,9 @@ // Loads custom icons (some icons may come from a third-party library) -import clsx from 'clsx' -import { CSSProperties, PropsWithChildren, SVGAttributes } from 'react' import './icons.scss' + +import clsx from 'clsx' import { LemonBadge, LemonBadgeProps } from 'lib/lemon-ui/LemonBadge' +import { CSSProperties, PropsWithChildren, SVGAttributes } from 'react' interface IconWithCountProps { count: number diff --git a/frontend/src/lib/lemon-ui/lemonToast.tsx b/frontend/src/lib/lemon-ui/lemonToast.tsx index b829ea33d0746..5af288acc6c96 100644 --- a/frontend/src/lib/lemon-ui/lemonToast.tsx +++ b/frontend/src/lib/lemon-ui/lemonToast.tsx @@ -1,8 +1,9 @@ -import { toast, ToastContentProps as ToastifyRenderProps, ToastOptions } from 'react-toastify' import { IconCheckmark, IconClose, IconErrorOutline, IconInfo, IconWarning } from 'lib/lemon-ui/icons' -import { LemonButton } from './LemonButton' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import posthog from 'posthog-js' +import { toast, ToastContentProps as ToastifyRenderProps, ToastOptions } from 'react-toastify' + +import { LemonButton } from './LemonButton' export function ToastCloseButton({ closeToast }: { closeToast?: () => void }): JSX.Element { return ( @@ -18,7 +19,7 @@ export function ToastCloseButton({ closeToast }: { closeToast?: () => void }): J interface ToastButton { label: string - action: () => void + action: (() => void) | (() => Promise) dataAttr?: string } @@ -47,7 +48,7 @@ export function ToastContent({ type, message, button, id }: ToastContentProps): {button && ( { - button.action() + void button.action() toast.dismiss(id) }} type="secondary" @@ -84,7 +85,7 @@ export const lemonToast = { }, warning(message: string | JSX.Element, { button, ...toastOptions }: ToastOptionsWithButton = {}): void { posthog.capture('toast warning', { - message: message.toString(), + message: String(message), button: button?.label, toastId: toastOptions.toastId, }) @@ -96,7 +97,7 @@ export const lemonToast = { }, error(message: string | JSX.Element, { button, ...toastOptions }: ToastOptionsWithButton = {}): void { posthog.capture('toast error', { - message: message.toString(), + message: String(message), button: button?.label, toastId: toastOptions.toastId, }) diff --git a/frontend/src/lib/logic/featureFlagLogic.ts b/frontend/src/lib/logic/featureFlagLogic.ts index 061bca49a70ea..592ea0f6646d7 100644 --- a/frontend/src/lib/logic/featureFlagLogic.ts +++ b/frontend/src/lib/logic/featureFlagLogic.ts @@ -1,9 +1,11 @@ -import { kea, path, actions, reducers, afterMount } from 'kea' -import type { featureFlagLogicType } from './featureFlagLogicType' -import posthog from 'posthog-js' +import { actions, afterMount, kea, path, reducers } from 'kea' import { getAppContext } from 'lib/utils/getAppContext' +import posthog from 'posthog-js' + import { AppContext } from '~/types' +import type { featureFlagLogicType } from './featureFlagLogicType' + export type FeatureFlagsSet = { [flag: string]: boolean | string } diff --git a/frontend/src/lib/logic/inAppPrompt/inAppPromptEventCaptureLogic.ts b/frontend/src/lib/logic/inAppPrompt/inAppPromptEventCaptureLogic.ts index 4eaf9dbabb158..b8ede572b2214 100644 --- a/frontend/src/lib/logic/inAppPrompt/inAppPromptEventCaptureLogic.ts +++ b/frontend/src/lib/logic/inAppPrompt/inAppPromptEventCaptureLogic.ts @@ -1,6 +1,7 @@ -import { kea, path, actions, listeners } from 'kea' -import type { inAppPromptEventCaptureLogicType } from './inAppPromptEventCaptureLogicType' +import { actions, kea, listeners, path } from 'kea' import posthog from 'posthog-js' + +import type { inAppPromptEventCaptureLogicType } from './inAppPromptEventCaptureLogicType' import { PromptType } from './inAppPromptLogic' const inAppPromptEventCaptureLogic = kea([ diff --git a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts index b270ad2621b1e..e1a029a9d2db0 100644 --- a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts +++ b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts @@ -1,12 +1,14 @@ +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { inAppPromptLogic, PromptConfig, PromptSequence, PromptUserState } from './inAppPromptLogic' +import api from 'lib/api' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' + import { useMocks } from '~/mocks/jest' -import api from 'lib/api' +import { initKeaTests } from '~/test/init' + import { inAppPromptEventCaptureLogic } from './inAppPromptEventCaptureLogic' +import { inAppPromptLogic, PromptConfig, PromptUserState } from './inAppPromptLogic' const configProductTours: PromptConfig & { state: PromptUserState } = { sequences: [ @@ -289,7 +291,7 @@ describe('inAppPromptLogic', () => { }) .toDispatchActions([ 'closePrompts', - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 0), + logic.actionCreators.runSequence(configProductTours.sequences[1], 0), inAppPromptEventCaptureLogic.actionCreators.reportPromptShown( 'tooltip', configProductTours.sequences[1].key, @@ -333,7 +335,7 @@ describe('inAppPromptLogic', () => { logic.actions.nextPrompt() }) .toDispatchActions([ - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 1), + logic.actionCreators.runSequence(configProductTours.sequences[1], 1), inAppPromptEventCaptureLogic.actionCreators.reportPromptForward( configProductTours.sequences[1].key, 1, @@ -359,7 +361,7 @@ describe('inAppPromptLogic', () => { logic.actions.previousPrompt() }) .toDispatchActions([ - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 0), + logic.actionCreators.runSequence(configProductTours.sequences[1], 0), inAppPromptEventCaptureLogic.actionCreators.reportPromptBackward( configProductTours.sequences[1].key, 0, diff --git a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.tsx b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.tsx index 3f725a72d4025..4c5cd22f04084 100644 --- a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.tsx +++ b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.tsx @@ -1,20 +1,12 @@ -import { createRoot } from 'react-dom/client' import { Placement } from '@floating-ui/react' -import { kea, path, actions, reducers, listeners, selectors, connect, afterMount, beforeUnmount } from 'kea' -import type { inAppPromptLogicType } from './inAppPromptLogicType' +import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { router, urlToAction } from 'kea-router' -import { - LemonActionableTooltip, - LemonActionableTooltipProps, -} from 'lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip' -import { inAppPromptEventCaptureLogic } from './inAppPromptEventCaptureLogic' import api from 'lib/api' import { now } from 'lib/dayjs' -import wcmatch from 'wildcard-match' import { - IconUnverifiedEvent, IconApps, IconBarChart, + IconCoffee, IconCohort, IconComment, IconExperiment, @@ -25,10 +17,19 @@ import { IconPerson, IconRecording, IconTools, - IconCoffee, IconTrendUp, + IconUnverifiedEvent, } from 'lib/lemon-ui/icons' +import { + LemonActionableTooltip, + LemonActionableTooltipProps, +} from 'lib/lemon-ui/LemonActionableTooltip/LemonActionableTooltip' import { Lettermark } from 'lib/lemon-ui/Lettermark' +import { createRoot } from 'react-dom/client' +import wcmatch from 'wildcard-match' + +import { inAppPromptEventCaptureLogic } from './inAppPromptEventCaptureLogic' +import type { inAppPromptLogicType } from './inAppPromptLogicType' /** To be extended with other types of notifications e.g. modals, bars */ export type PromptType = 'tooltip' diff --git a/frontend/src/lib/logic/newPrompt/Prompt.tsx b/frontend/src/lib/logic/newPrompt/Prompt.tsx index 37c2a473a6777..8392dfc95aa35 100644 --- a/frontend/src/lib/logic/newPrompt/Prompt.tsx +++ b/frontend/src/lib/logic/newPrompt/Prompt.tsx @@ -1,11 +1,14 @@ -import { useActions, useValues } from 'kea' import './prompt.scss' -import { promptLogic } from './promptLogic' -import clsx from 'clsx' + import { LemonButton, LemonModal } from '@posthog/lemon-ui' -import { PromptButtonType, PromptFlag, PromptPayload } from '~/types' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { FallbackCoverImage } from 'lib/components/FallbackCoverImage/FallbackCoverImage' +import { PromptButtonType, PromptFlag, PromptPayload } from '~/types' + +import { promptLogic } from './promptLogic' + export function ModalPrompt({ payload, closePrompt, diff --git a/frontend/src/lib/logic/newPrompt/prompt.scss b/frontend/src/lib/logic/newPrompt/prompt.scss index 095bddaadcbfc..355cb23c9af2e 100644 --- a/frontend/src/lib/logic/newPrompt/prompt.scss +++ b/frontend/src/lib/logic/newPrompt/prompt.scss @@ -5,7 +5,6 @@ z-index: 2000; flex-direction: column; background: white; - border: 1px solid #f0f0f0; border-radius: 8px; padding-top: 5px; min-width: 300px; diff --git a/frontend/src/lib/logic/newPrompt/prompt.stories.tsx b/frontend/src/lib/logic/newPrompt/prompt.stories.tsx index 59f9416017cf9..58eb6c9647db9 100644 --- a/frontend/src/lib/logic/newPrompt/prompt.stories.tsx +++ b/frontend/src/lib/logic/newPrompt/prompt.stories.tsx @@ -1,9 +1,11 @@ import { Meta } from '@storybook/react' import { useActions } from 'kea' +import BlankDashboardHog from 'public/blank-dashboard-hog.png' + import { PromptFlag, PromptPayload } from '~/types' + import { ModalPrompt, PopupPrompt, Prompt } from './Prompt' import { promptLogic } from './promptLogic' -import BlankDashboardHog from 'public/blank-dashboard-hog.png' const meta: Meta = { title: 'Components/Prompts', diff --git a/frontend/src/lib/logic/newPrompt/promptLogic.tsx b/frontend/src/lib/logic/newPrompt/promptLogic.tsx index ec397f791f321..064fcff1c78cd 100644 --- a/frontend/src/lib/logic/newPrompt/promptLogic.tsx +++ b/frontend/src/lib/logic/newPrompt/promptLogic.tsx @@ -1,10 +1,10 @@ import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' - -import posthog from 'posthog-js' -import { featureFlagLogic } from '../featureFlagLogic' import { router } from 'kea-router' +import posthog from 'posthog-js' + import { PromptButtonType, PromptFlag, PromptPayload } from '~/types' +import { featureFlagLogic } from '../featureFlagLogic' import type { promptLogicType } from './promptLogicType' const PROMPT_PREFIX = 'prompt' diff --git a/frontend/src/lib/logic/promptLogic.tsx b/frontend/src/lib/logic/promptLogic.tsx index 85a86f234c856..473bab99a655f 100644 --- a/frontend/src/lib/logic/promptLogic.tsx +++ b/frontend/src/lib/logic/promptLogic.tsx @@ -1,6 +1,6 @@ +import { Form, FormItemProps, Input, InputProps, Modal, ModalProps } from 'antd' +import { actions, events, kea, key, listeners, path, props } from 'kea' import { createRoot } from 'react-dom/client' -import { kea, props, path, key, actions, events, listeners } from 'kea' -import { Modal, ModalProps, Input, InputProps, Form, FormItemProps } from 'antd' import type { promptLogicType } from './promptLogicType' diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index e841ec72c7323..10be42e8fd679 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -1,10 +1,6 @@ -import { KeyMapping, PropertyFilterValue } from '~/types' -import { Link } from './lemon-ui/Link' +import { KeyMapping, KeyMappingInterface, PropertyFilterValue } from '~/types' -export interface KeyMappingInterface { - event: Record - element: Record -} +import { Link } from './lemon-ui/Link' // If adding event properties with labels, check whether they should be added to // PROPERTY_NAME_ALIASES in posthog/api/property_definition.py @@ -835,7 +831,7 @@ export function getKeyMapping( data = { ...KEY_MAPPING[type][value.replace(/^\$initial_/, '$')] } if (data.description) { data.label = `Initial ${data.label}` - data.description = `${data.description} Data from the first time this user was seen.` + data.description = `${String(data.description)} Data from the first time this user was seen.` } return data } else if (value.startsWith('$survey_responded/')) { diff --git a/frontend/src/lib/utils.test.ts b/frontend/src/lib/utils.test.ts index 49764e4fb3201..dca123df90749 100644 --- a/frontend/src/lib/utils.test.ts +++ b/frontend/src/lib/utils.test.ts @@ -1,4 +1,8 @@ +import { dayjs } from 'lib/dayjs' import tk from 'timekeeper' + +import { ElementType, EventType, PropertyType, TimeUnitType } from '~/types' + import { areObjectValuesEmpty, average, @@ -9,8 +13,6 @@ import { chooseOperatorMap, colonDelimitedDuration, compactNumber, - convertPropertiesToPropertyGroup, - convertPropertyGroupToProperties, dateFilterToText, dateMapping, dateStringToDayJs, @@ -20,43 +22,29 @@ import { ensureStringIsNotBlank, eventToDescription, floorMsToClosestSecond, - formatLabel, genericOperatorMap, getFormattedLastWeekDate, hexToRGBA, humanFriendlyDuration, + humanFriendlyLargeNumber, identifierToHuman, isExternalLink, isURL, median, midEllipsis, numericOperatorMap, - objectDiffShallow, objectClean, objectCleanWithEmpty, + objectDiffShallow, pluralize, range, reverseColonDelimitedDuration, roundToDecimal, selectorOperatorMap, + shortTimeZone, stringOperatorMap, toParams, - shortTimeZone, - humanFriendlyLargeNumber, } from './utils' -import { - ActionFilter, - AnyPropertyFilter, - ElementType, - EventType, - FilterLogicalOperator, - PropertyFilterType, - PropertyGroupFilter, - PropertyOperator, - PropertyType, - TimeUnitType, -} from '~/types' -import { dayjs } from 'lib/dayjs' describe('toParams', () => { it('handles unusual input', () => { @@ -111,46 +99,6 @@ describe('identifierToHuman()', () => { }) }) -describe('formatLabel()', () => { - const action: ActionFilter = { - id: 123, - name: 'Test Action', - properties: [], - type: 'actions', - } - - it('formats the label', () => { - expect(formatLabel('some_event', action)).toEqual('some_event') - }) - - it('DAU queries', () => { - expect(formatLabel('some_event', { ...action, math: 'dau' })).toEqual('some_event (Unique users)') - }) - - it('summing by property', () => { - expect(formatLabel('some_event', { ...action, math: 'sum', math_property: 'event_property' })).toEqual( - 'some_event (sum of event_property)' - ) - }) - - it('action with properties', () => { - expect( - formatLabel('some_event', { - ...action, - properties: [ - { - value: 'hello', - key: 'greeting', - operator: PropertyOperator.Exact, - type: PropertyFilterType.Person, - }, - { operator: PropertyOperator.GreaterThan, value: 5, key: '', type: PropertyFilterType.Person }, - ], - }) - ).toEqual('some_event (greeting = hello, > 5)') - }) -}) - describe('midEllipsis()', () => { it('returns same string if short', () => { expect(midEllipsis('12', 10)).toEqual('12') @@ -754,70 +702,6 @@ describe('{floor|ceil}MsToClosestSecond()', () => { }) }) -describe('convertPropertyGroupToProperties()', () => { - it('converts a single layer property group into an array of properties', () => { - const propertyGroup = { - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [ - { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ] as AnyPropertyFilter[], - }, - { - type: FilterLogicalOperator.And, - values: [ - { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ] as AnyPropertyFilter[], - }, - ], - } - expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([ - { key: '$browser', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$current_url', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - { key: '$lib', type: PropertyFilterType.Event, operator: PropertyOperator.IsSet }, - ]) - }) - - it('converts a deeply nested property group into an array of properties', () => { - const propertyGroup: PropertyGroupFilter = { - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [{ type: FilterLogicalOperator.And, values: [{ key: '$lib' } as any] }], - }, - { type: FilterLogicalOperator.And, values: [{ key: '$browser' } as any] }, - ], - } - expect(convertPropertyGroupToProperties(propertyGroup)).toEqual([{ key: '$lib' }, { key: '$browser' }]) - }) -}) - -describe('convertPropertiesToPropertyGroup', () => { - it('converts properties to one AND operator property group', () => { - const properties: any[] = [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }] - expect(convertPropertiesToPropertyGroup(properties)).toEqual({ - type: FilterLogicalOperator.And, - values: [ - { - type: FilterLogicalOperator.And, - values: [{ key: '$lib' }, { key: '$browser' }, { key: '$current_url' }], - }, - ], - }) - }) - - it('converts properties to one AND operator property group', () => { - expect(convertPropertiesToPropertyGroup(undefined)).toEqual({ - type: FilterLogicalOperator.And, - values: [], - }) - }) -}) - describe('calculateDays', () => { it('1 day to 1 day', () => { expect(calculateDays(1, TimeUnitType.Day)).toEqual(1) diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index be48d5618e0e5..b37a126d607e8 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1,54 +1,29 @@ +import * as Sentry from '@sentry/react' +import equal from 'fast-deep-equal' +import { tagColors } from 'lib/colors' +import { WEBHOOK_SERVICES } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { AlignType } from 'rc-trigger/lib/interface' import { CSSProperties } from 'react' -import api from './api' + import { - ActionFilter, ActionType, ActorType, - AnyCohortCriteriaType, - AnyFilterLike, - AnyFilterType, - AnyPropertyFilter, - BehavioralCohortType, - BehavioralEventType, - ChartDisplayType, - CohortCriteriaGroupFilter, - CohortType, DateMappingOption, - EmptyPropertyFilter, EventType, - FilterLogicalOperator, - FunnelVizType, GroupActorType, - InsightType, - IntervalType, - PropertyFilterValue, - PropertyGroupFilter, - PropertyGroupFilterValue, PropertyOperator, PropertyType, TimeUnitType, - TrendsFilterType, } from '~/types' -import * as Sentry from '@sentry/react' -import equal from 'fast-deep-equal' -import { tagColors } from 'lib/colors' -import { NON_TIME_SERIES_DISPLAY_TYPES, WEBHOOK_SERVICES } from 'lib/constants' -import { KeyMappingInterface } from 'lib/taxonomy' -import { AlignType } from 'rc-trigger/lib/interface' -import { dayjs } from 'lib/dayjs' + +import { CUSTOM_OPTION_KEY } from './components/DateFilter/types' import { getAppContext } from './utils/getAppContext' -import { - isHogQLPropertyFilter, - isPropertyFilterWithOperator, - isValidPropertyFilter, -} from './components/PropertyFilters/utils' -import { IconCopy } from 'lib/lemon-ui/icons' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' -import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' -import { urls } from 'scenes/urls' -import { isFunnelsFilter } from 'scenes/insights/sharedUtils' -import { CUSTOM_OPTION_KEY } from './components/DateFilter/dateFilterLogic' + +/** + * WARNING: Be very careful importing things here. This file is heavily used and can trigger a lot of cyclic imports + * Preferably create a dedicated file in utils/.. + */ export const ANTD_TOOLTIP_PLACEMENTS: Record = { // `@yiminghe/dom-align` objects @@ -182,38 +157,6 @@ export function percentage( }) } -export async function deleteWithUndo>({ - undo = false, - ...props -}: { - undo?: boolean - endpoint: string - object: T - idField?: keyof T - callback?: (undo: boolean, object: T) => void -}): Promise { - await api.update(`api/${props.endpoint}/${props.object[props.idField || 'id']}`, { - ...props.object, - deleted: !undo, - }) - props.callback?.(undo, props.object) - lemonToast[undo ? 'success' : 'info']( - <> - {props.object.name || {props.object.derived_name || 'Unnamed'}} has been{' '} - {undo ? 'restored' : 'deleted'} - , - { - toastId: `delete-item-${props.object.id}-${undo}`, - button: undo - ? undefined - : { - label: 'Undo', - action: () => deleteWithUndo({ undo: true, ...props }), - }, - } - ) -} - export const selectStyle: Record) => Partial> = { control: (base) => ({ ...base, @@ -365,50 +308,6 @@ export function isOperatorDate(operator: PropertyOperator): boolean { ) } -export function formatPropertyLabel( - item: Record, - cohortsById: Partial>, - keyMapping: KeyMappingInterface, - valueFormatter: (value: PropertyFilterValue | undefined) => string | string[] | null = (s) => [String(s)] -): string { - if (isHogQLPropertyFilter(item as AnyFilterLike)) { - return extractExpressionComment(item.key) - } - const { value, key, operator, type } = item - return type === 'cohort' - ? cohortsById[value]?.name || `ID ${value}` - : (keyMapping[type === 'element' ? 'element' : 'event'][key]?.label || key) + - (isOperatorFlag(operator) - ? ` ${allOperatorsMapping[operator]}` - : ` ${(allOperatorsMapping[operator || 'exact'] || '?').split(' ')[0]} ${ - value && value.length === 1 && value[0] === '' ? '(empty string)' : valueFormatter(value) || '' - } `) -} - -/** Format a label that gets returned from the /insights api */ -export function formatLabel(label: string, action: ActionFilter): string { - if (action.math === 'dau') { - label += ` (Unique users) ` - } else if (action.math === 'hogql') { - label += ` (${action.math_hogql})` - } else if (['sum', 'avg', 'min', 'max', 'median', 'p90', 'p95', 'p99'].includes(action.math || '')) { - label += ` (${action.math} of ${action.math_property}) ` - } - if (action.properties?.length) { - label += ` (${action.properties - .map( - (property) => - `${property.key ? `${property.key} ` : ''}${ - allOperatorsMapping[ - (isPropertyFilterWithOperator(property) && property.operator) || 'exact' - ].split(' ')[0] - } ${property.value}` - ) - .join(', ')})` - } - return label.trim() -} - /** Compare objects deeply. */ export function objectsEqual(obj1: any, obj2: any): boolean { return equal(obj1, obj2) @@ -1068,38 +967,6 @@ export function dateStringToDayJs(date: string | null): dayjs.Dayjs | null { return response } -export async function copyToClipboard(value: string, description: string = 'text'): Promise { - if (!navigator.clipboard) { - lemonToast.warning('Oops! Clipboard capabilities are only available over HTTPS or on localhost') - return false - } - - try { - await navigator.clipboard.writeText(value) - lemonToast.info(`Copied ${description} to clipboard`, { - icon: , - }) - return true - } catch (e) { - // If the Clipboard API fails, fallback to textarea method - try { - const textArea = document.createElement('textarea') - textArea.value = value - document.body.appendChild(textArea) - textArea.select() - document.execCommand('copy') - document.body.removeChild(textArea) - lemonToast.info(`Copied ${description} to clipboard`, { - icon: , - }) - return true - } catch (err) { - lemonToast.error(`Could not copy ${description} to clipboard: ${err}`) - return false - } - } -} - export function clamp(value: number, min: number, max: number): number { return value > max ? max : value < min ? min : value } @@ -1258,46 +1125,6 @@ export function midEllipsis(input: string, maxLength: number): string { return `${input.slice(0, middle - excessLeft)}…${input.slice(middle + excessRight)}` } -export const disableHourFor: Record = { - dStart: false, - '-1d': false, - '-7d': false, - '-14d': false, - '-30d': false, - '-90d': true, - mStart: false, - '-1mStart': false, - yStart: true, - all: true, - other: false, -} - -export function autocorrectInterval(filters: Partial): IntervalType | undefined { - if ('display' in filters && filters.display && NON_TIME_SERIES_DISPLAY_TYPES.includes(filters.display)) { - // Non-time-series insights should not have an interval - return undefined - } - if (isFunnelsFilter(filters) && filters.funnel_viz_type !== FunnelVizType.Trends) { - // Only trend funnels support intervals - return undefined - } - if (!filters.interval) { - return 'day' - } - - // @ts-expect-error - Old legacy interval support - const minute_disabled = filters.interval === 'minute' - const hour_disabled = disableHourFor[filters.date_from || 'other'] && filters.interval === 'hour' - - if (minute_disabled) { - return 'hour' - } else if (hour_disabled) { - return 'day' - } else { - return filters.interval - } -} - export function pluralize(count: number, singular: string, plural?: string, includeNumber: boolean = true): string { if (!plural) { plural = singular + 's' @@ -1385,7 +1212,7 @@ export function humanTzOffset(timezone?: string): string { /** Join array of string into a list ("a, b, and c"). Uses the Oxford comma, but only if there are at least 3 items. */ export function humanList(arr: readonly string[]): string { - return arr.length > 2 ? arr.slice(0, -1).join(', ') + ', and ' + arr.slice(-1) : arr.join(' and ') + return arr.length > 2 ? arr.slice(0, -1).join(', ') + ', and ' + arr.at(-1) : arr.join(' and ') } export function resolveWebhookService(webhookUrl: string): string { @@ -1426,6 +1253,11 @@ export function hexToRGBA(hex: string, alpha = 1): string { return `rgba(${[r, g, b, a].join(',')})` } +export function RGBToRGBA(rgb: string, a: number): string { + const [r, g, b] = rgb.slice(4, rgb.length - 1).split(',') + return `rgba(${[r, g, b, a].join(',')})` +} + export function lightenDarkenColor(hex: string, pct: number): string { /** * Returns a lightened or darkened color, similar to SCSS darken() @@ -1447,7 +1279,7 @@ export function lightenDarkenColor(hex: string, pct: number): string { return `rgb(${[r, g, b].join(',')})` } -export function toString(input?: any | null): string { +export function toString(input?: any): string { return input?.toString() || '' } @@ -1544,64 +1376,6 @@ export function getEventNamesForAction(actionId: string | number, allActions: Ac .flatMap((a) => a.steps?.filter((step) => step.event).map((step) => String(step.event)) as string[]) } -export function isPropertyGroup( - properties: - | PropertyGroupFilter - | PropertyGroupFilterValue - | AnyPropertyFilter[] - | AnyPropertyFilter - | Record - | null - | undefined -): properties is PropertyGroupFilter { - return ( - (properties as PropertyGroupFilter)?.type !== undefined && - (properties as PropertyGroupFilter)?.values !== undefined - ) -} - -export function flattenPropertyGroup( - flattenedProperties: AnyPropertyFilter[], - propertyGroup: PropertyGroupFilter | PropertyGroupFilterValue | AnyPropertyFilter -): AnyPropertyFilter[] { - const obj: AnyPropertyFilter = {} as EmptyPropertyFilter - Object.keys(propertyGroup).forEach(function (k) { - obj[k] = propertyGroup[k] - }) - if (isValidPropertyFilter(obj)) { - flattenedProperties.push(obj) - } - if (isPropertyGroup(propertyGroup)) { - return propertyGroup.values.reduce(flattenPropertyGroup, flattenedProperties) - } - return flattenedProperties -} - -export function convertPropertiesToPropertyGroup( - properties: PropertyGroupFilter | AnyPropertyFilter[] | undefined -): PropertyGroupFilter { - if (isPropertyGroup(properties)) { - return properties - } - if (properties && properties.length > 0) { - return { type: FilterLogicalOperator.And, values: [{ type: FilterLogicalOperator.And, values: properties }] } - } - return { type: FilterLogicalOperator.And, values: [] } -} - -/** Flatten a filter group into an array of filters. NB: Logical operators (AND/OR) are lost in the process. */ -export function convertPropertyGroupToProperties( - properties?: PropertyGroupFilter | AnyPropertyFilter[] -): AnyPropertyFilter[] | undefined { - if (isPropertyGroup(properties)) { - return flattenPropertyGroup([], properties).filter(isValidPropertyFilter) - } - if (properties) { - return properties.filter(isValidPropertyFilter) - } - return properties -} - export const isUserLoggedIn = (): boolean => !getAppContext()?.anonymous /** Sorting function for Array.prototype.sort that works for numbers and strings automatically. */ @@ -1711,41 +1485,6 @@ export function range(startOrEnd: number, end?: number): number[] { return Array.from({ length }, (_, i) => i + start) } -export function processCohort(cohort: CohortType): CohortType { - return { - ...cohort, - ...{ - /* Populate value_property with value and overwrite value with corresponding behavioral filter type */ - filters: { - properties: { - ...cohort.filters.properties, - values: (cohort.filters.properties?.values?.map((group) => - 'values' in group - ? { - ...group, - values: (group.values as AnyCohortCriteriaType[]).map((c) => - c.type && - [BehavioralFilterKey.Cohort, BehavioralFilterKey.Person].includes(c.type) && - !('value_property' in c) - ? { - ...c, - value_property: c.value, - value: - c.type === BehavioralFilterKey.Cohort - ? BehavioralCohortType.InCohort - : BehavioralEventType.HaveProperty, - } - : c - ), - } - : group - ) ?? []) as CohortCriteriaGroupFilter[] | AnyCohortCriteriaType[], - }, - }, - }, - } -} - export function interleave(arr: any[], delimiter: any): any[] { return arr.flatMap((item, index, _arr) => _arr.length - 1 !== index // check for the last item @@ -1773,51 +1512,6 @@ export function downloadFile(file: File): void { }, 0) } -export function insightUrlForEvent(event: Pick): string | undefined { - let insightParams: Partial | undefined - if (event.event === '$pageview') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: '$pageview', - name: '$pageview', - type: 'events', - order: 0, - properties: [ - { - key: '$current_url', - value: event.properties.$current_url, - type: 'event', - }, - ], - }, - ], - } - } else if (event.event !== '$autocapture') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: event.event, - name: event.event, - type: 'events', - order: 0, - properties: [], - }, - ], - } - } - - return insightParams ? urls.insightNew(insightParams) : undefined -} - export function inStorybookTestRunner(): boolean { return navigator.userAgent.includes('StorybookTestRunner') } diff --git a/frontend/src/lib/utils/copyToClipboard.tsx b/frontend/src/lib/utils/copyToClipboard.tsx new file mode 100644 index 0000000000000..b29ec0dbbe14b --- /dev/null +++ b/frontend/src/lib/utils/copyToClipboard.tsx @@ -0,0 +1,34 @@ +import { IconCopy } from '@posthog/icons' +import { lemonToast } from '@posthog/lemon-ui' + +export async function copyToClipboard(value: string, description: string = 'text'): Promise { + if (!navigator.clipboard) { + lemonToast.warning('Oops! Clipboard capabilities are only available over HTTPS or on localhost') + return false + } + + try { + await navigator.clipboard.writeText(value) + lemonToast.info(`Copied ${description} to clipboard`, { + icon: , + }) + return true + } catch (e) { + // If the Clipboard API fails, fallback to textarea method + try { + const textArea = document.createElement('textarea') + textArea.value = value + document.body.appendChild(textArea) + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + lemonToast.info(`Copied ${description} to clipboard`, { + icon: , + }) + return true + } catch (err) { + lemonToast.error(`Could not copy ${description} to clipboard: ${err}`) + return false + } + } +} diff --git a/frontend/src/lib/utils/d3Utils.ts b/frontend/src/lib/utils/d3Utils.ts index 8accdaaebf132..4c2814e6120bc 100644 --- a/frontend/src/lib/utils/d3Utils.ts +++ b/frontend/src/lib/utils/d3Utils.ts @@ -1,6 +1,6 @@ import * as d3 from 'd3' -import { INITIAL_CONFIG } from 'scenes/insights/views/Histogram/histogramUtils' import { D3Selector, D3Transition } from 'lib/hooks/useD3' +import { INITIAL_CONFIG } from 'scenes/insights/views/Histogram/histogramUtils' export const getOrCreateEl = ( container: D3Selector, diff --git a/frontend/src/lib/utils/deleteWithUndo.tsx b/frontend/src/lib/utils/deleteWithUndo.tsx new file mode 100644 index 0000000000000..023a0c1bc4ad2 --- /dev/null +++ b/frontend/src/lib/utils/deleteWithUndo.tsx @@ -0,0 +1,34 @@ +import { lemonToast } from '@posthog/lemon-ui' +import api from 'lib/api' + +export async function deleteWithUndo>({ + undo = false, + ...props +}: { + undo?: boolean + endpoint: string + object: T + idField?: keyof T + callback?: (undo: boolean, object: T) => void +}): Promise { + await api.update(`api/${props.endpoint}/${props.object[props.idField || 'id']}`, { + ...props.object, + deleted: !undo, + }) + props.callback?.(undo, props.object) + lemonToast[undo ? 'success' : 'info']( + <> + {props.object.name || {props.object.derived_name || 'Unnamed'}} has been{' '} + {undo ? 'restored' : 'deleted'} + , + { + toastId: `delete-item-${props.object.id}-${undo}`, + button: undo + ? undefined + : { + label: 'Undo', + action: () => deleteWithUndo({ undo: true, ...props }), + }, + } + ) +} diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 2ec59bf991d2d..e0a34b568417d 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -1,51 +1,51 @@ -import { kea, path, connect, actions, listeners } from 'kea' +import { actions, connect, kea, listeners, path } from 'kea' +import { convertPropertyGroupToProperties, isGroupPropertyFilter } from 'lib/components/PropertyFilters/utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import type { Dayjs } from 'lib/dayjs' +import { now } from 'lib/dayjs' import { isPostHogProp, keyMappingKeys } from 'lib/taxonomy' import posthog from 'posthog-js' +import { + isFilterWithDisplay, + isFunnelsFilter, + isPathsFilter, + isRetentionFilter, + isStickinessFilter, + isTrendsFilter, +} from 'scenes/insights/sharedUtils' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { EventIndex } from 'scenes/session-recordings/player/eventIndex' +import { SurveyTemplateType } from 'scenes/surveys/constants' import { userLogic } from 'scenes/userLogic' -import type { eventUsageLogicType } from './eventUsageLogicType' + import { - FilterType, - DashboardType, - PersonType, + AccessLevel, + AnyPartialFilterType, + AnyPropertyFilter, DashboardMode, + DashboardType, EntityType, + Experiment, + FilterLogicalOperator, + FilterType, + FunnelCorrelation, + HelpType, InsightModel, + InsightShortId, InsightType, - HelpType, - SessionRecordingUsageType, - FunnelCorrelation, ItemMode, - AnyPropertyFilter, - Experiment, - PropertyGroupFilter, - FilterLogicalOperator, + PersonType, PropertyFilterValue, - InsightShortId, - SessionPlayerData, - AnyPartialFilterType, - Resource, - AccessLevel, + PropertyGroupFilter, RecordingReportLoadTimes, + Resource, + SessionPlayerData, SessionRecordingPlayerTab, + SessionRecordingUsageType, Survey, } from '~/types' -import type { Dayjs } from 'lib/dayjs' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { convertPropertyGroupToProperties } from 'lib/utils' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { PlatformType, Framework } from 'scenes/ingestion/types' -import { now } from 'lib/dayjs' -import { - isFilterWithDisplay, - isFunnelsFilter, - isPathsFilter, - isRetentionFilter, - isStickinessFilter, - isTrendsFilter, -} from 'scenes/insights/sharedUtils' -import { isGroupPropertyFilter } from 'lib/components/PropertyFilters/utils' -import { EventIndex } from 'scenes/session-recordings/player/eventIndex' -import { SurveyTemplateType } from 'scenes/surveys/constants' + +import type { eventUsageLogicType } from './eventUsageLogicType' export enum DashboardEventSource { LongPress = 'long_press', @@ -173,9 +173,10 @@ function sanitizeFilterParams(filters: AnyPartialFilterType): Record([ ) => ({ attribute, originalLength, newLength }), reportDashboardShareToggled: (isShared: boolean) => ({ isShared }), reportUpgradeModalShown: (featureName: string) => ({ featureName }), - reportIngestionLandingSeen: true, reportTimezoneComponentViewed: ( component: 'label' | 'indicator', project_timezone?: string, @@ -439,27 +439,11 @@ export const eventUsageLogic = kea([ reportInsightOpenedFromRecentInsightList: true, reportRecordingOpenedFromRecentRecordingList: true, reportPersonOpenedFromNewlySeenPersonsList: true, - reportIngestionSelectPlatformType: (platform: PlatformType) => ({ platform }), - reportIngestionSelectFrameworkType: (framework: Framework) => ({ framework }), - reportIngestionRecordingsTurnedOff: ( - session_recording_opt_in: boolean, - capture_console_log_opt_in: boolean, - capture_performance_opt_in: boolean - ) => ({ session_recording_opt_in, capture_console_log_opt_in, capture_performance_opt_in }), - reportIngestionAutocaptureToggled: (autocapture_opt_out: boolean) => ({ autocapture_opt_out }), - reportIngestionAutocaptureExceptionsToggled: (autocapture_opt_in: boolean) => ({ autocapture_opt_in }), - reportIngestionHelpClicked: (type: string) => ({ type }), - reportIngestionTryWithBookmarkletClicked: true, - reportIngestionTryWithDemoDataClicked: true, reportIngestionContinueWithoutVerifying: true, - reportIngestionContinueWithoutBilling: true, - reportIngestionBillingCancelled: true, - reportIngestionThirdPartyAboutClicked: (name: string) => ({ name }), - reportIngestionThirdPartyConfigureClicked: (name: string) => ({ name }), - reportIngestionThirdPartyPluginInstalled: (name: string) => ({ name }), + reportAutocaptureToggled: (autocapture_opt_out: boolean) => ({ autocapture_opt_out }), + reportAutocaptureExceptionsToggled: (autocapture_opt_in: boolean) => ({ autocapture_opt_in }), reportFailedToCreateFeatureFlagWithCohort: (code: string, detail: string) => ({ code, detail }), reportInviteMembersButtonClicked: true, - reportIngestionSidebarButtonClicked: (name: string) => ({ name }), reportDashboardLoadingTime: (loadingMilliseconds: number, dashboardId: number) => ({ loadingMilliseconds, dashboardId, @@ -794,9 +778,6 @@ export const eventUsageLogic = kea([ } posthog.capture('test account filters updated', payload) }, - reportIngestionLandingSeen: async () => { - posthog.capture('ingestion landing seen') - }, reportInsightFilterRemoved: async ({ index }) => { posthog.capture('local filter removed', { index }) @@ -1049,70 +1030,17 @@ export const eventUsageLogic = kea([ reportPersonOpenedFromNewlySeenPersonsList: () => { posthog.capture('person opened from newly seen persons list') }, - reportIngestionSelectPlatformType: ({ platform }) => { - posthog.capture('ingestion select platform type', { - platform: platform, - }) - }, - reportIngestionSelectFrameworkType: ({ framework }) => { - posthog.capture('ingestion select framework type', { - framework: framework, - }) - }, - reportIngestionRecordingsTurnedOff: ({ - session_recording_opt_in, - capture_console_log_opt_in, - capture_performance_opt_in, - }) => { - posthog.capture('ingestion recordings turned off', { - session_recording_opt_in, - capture_console_log_opt_in, - capture_performance_opt_in, - }) - }, - reportIngestionAutocaptureToggled: ({ autocapture_opt_out }) => { - posthog.capture('ingestion autocapture toggled', { - autocapture_opt_out, - }) - }, - reportIngestionAutocaptureExceptionsToggled: ({ autocapture_opt_in }) => { - posthog.capture('ingestion autocapture exceptions toggled', { - autocapture_opt_in, - }) - }, - reportIngestionHelpClicked: ({ type }) => { - posthog.capture('ingestion help clicked', { - type: type, - }) - }, - reportIngestionTryWithBookmarkletClicked: () => { - posthog.capture('ingestion try posthog with bookmarklet clicked') - }, - reportIngestionTryWithDemoDataClicked: () => { - posthog.capture('ingestion try posthog with demo data clicked') - }, reportIngestionContinueWithoutVerifying: () => { posthog.capture('ingestion continue without verifying') }, - reportIngestionContinueWithoutBilling: () => { - posthog.capture('ingestion continue without adding billing details') - }, - reportIngestionBillingCancelled: () => { - posthog.capture('ingestion billing cancelled') - }, - reportIngestionThirdPartyAboutClicked: ({ name }) => { - posthog.capture('ingestion third party about clicked', { - name: name, - }) - }, - reportIngestionThirdPartyConfigureClicked: ({ name }) => { - posthog.capture('ingestion third party configure clicked', { - name: name, + reportAutocaptureToggled: ({ autocapture_opt_out }) => { + posthog.capture('autocapture toggled', { + autocapture_opt_out, }) }, - reportIngestionThirdPartyPluginInstalled: ({ name }) => { - posthog.capture('report ingestion third party plugin installed', { - name: name, + reportAutocaptureExceptionsToggled: ({ autocapture_opt_in }) => { + posthog.capture('autocapture exceptions toggled', { + autocapture_opt_in, }) }, reportFailedToCreateFeatureFlagWithCohort: ({ detail, code }) => { @@ -1121,11 +1049,6 @@ export const eventUsageLogic = kea([ reportInviteMembersButtonClicked: () => { posthog.capture('invite members button clicked') }, - reportIngestionSidebarButtonClicked: ({ name }) => { - posthog.capture('ingestion sidebar button clicked', { - name: name, - }) - }, reportTeamSettingChange: ({ name, value }) => { posthog.capture(`${name} team setting updated`, { setting: name, @@ -1181,6 +1104,7 @@ export const eventUsageLogic = kea([ posthog.capture('survey created', { name: survey.name, id: survey.id, + survey_type: survey.type, questions_length: survey.questions.length, question_types: survey.questions.map((question) => question.type), }) @@ -1189,6 +1113,7 @@ export const eventUsageLogic = kea([ posthog.capture('survey launched', { name: survey.name, id: survey.id, + survey_type: survey.type, question_types: survey.questions.map((question) => question.type), created_at: survey.created_at, start_date: survey.start_date, diff --git a/frontend/src/lib/utils/kea-logic-builders.ts b/frontend/src/lib/utils/kea-logic-builders.ts index 8724c0acfd9fb..92c4ff8fb7344 100644 --- a/frontend/src/lib/utils/kea-logic-builders.ts +++ b/frontend/src/lib/utils/kea-logic-builders.ts @@ -1,4 +1,4 @@ -import { BuiltLogic, afterMount } from 'kea' +import { afterMount, BuiltLogic } from 'kea' /** * Some kea logics are used heavily across multiple areas so we keep it mounted once loaded with this trick. */ diff --git a/frontend/src/lib/utils/logics.ts b/frontend/src/lib/utils/logics.ts index 5efc717ac4975..93efac6422618 100644 --- a/frontend/src/lib/utils/logics.ts +++ b/frontend/src/lib/utils/logics.ts @@ -1,4 +1,5 @@ import { organizationLogic } from 'scenes/organizationLogic' + import { teamLogic } from '../../scenes/teamLogic' import { OrganizationType, TeamType } from '../../types' import { getAppContext } from './getAppContext' diff --git a/frontend/src/lib/utils/permissioning.ts b/frontend/src/lib/utils/permissioning.ts index bbd0ca3d0f999..0ec496aaa4da4 100644 --- a/frontend/src/lib/utils/permissioning.ts +++ b/frontend/src/lib/utils/permissioning.ts @@ -1,8 +1,5 @@ -import { ExplicitTeamMemberType, OrganizationMemberType, UserType } from '../../types' -import { OrganizationMembershipLevel, TeamMembershipLevel } from '../constants' - -export type EitherMembershipLevel = OrganizationMembershipLevel | TeamMembershipLevel -export type EitherMemberType = OrganizationMemberType | ExplicitTeamMemberType +import { EitherMemberType, ExplicitTeamMemberType, OrganizationMemberType, UserType } from '../../types' +import { EitherMembershipLevel, OrganizationMembershipLevel, TeamMembershipLevel } from '../constants' /** If access level change is disallowed given the circumstances, returns a reason why so. Otherwise returns null. */ export function getReasonForAccessLevelChangeProhibition( diff --git a/frontend/src/loadPostHogJS.tsx b/frontend/src/loadPostHogJS.tsx index 2acd266241f82..807fce2883849 100644 --- a/frontend/src/loadPostHogJS.tsx +++ b/frontend/src/loadPostHogJS.tsx @@ -1,6 +1,6 @@ -import posthog, { PostHogConfig } from 'posthog-js' import * as Sentry from '@sentry/react' import { FEATURE_FLAGS } from 'lib/constants' +import posthog, { PostHogConfig } from 'posthog-js' const configWithSentry = (config: Partial): Partial => { if ((window as any).SENTRY_DSN) { @@ -27,8 +27,8 @@ export function loadPostHogJS(): void { bootstrap: window.POSTHOG_USER_IDENTITY_WITH_FLAGS ? window.POSTHOG_USER_IDENTITY_WITH_FLAGS : {}, opt_in_site_apps: true, loaded: (posthog) => { - if (posthog.webPerformance) { - posthog.webPerformance._forceAllowLocalhost = true + if (posthog.sessionRecording) { + posthog.sessionRecording._forceAllowLocalhostNetworkCapture = true } if (window.IMPERSONATED_SESSION) { diff --git a/frontend/src/mocks/browser.tsx b/frontend/src/mocks/browser.tsx index 6baf8552fa047..846e69bbf6e14 100644 --- a/frontend/src/mocks/browser.tsx +++ b/frontend/src/mocks/browser.tsx @@ -1,7 +1,8 @@ +import { DecoratorFunction } from '@storybook/types' import { rest, setupWorker } from 'msw' + import { handlers } from '~/mocks/handlers' import { Mocks, mocksToHandlers } from '~/mocks/utils' -import { DecoratorFunction } from '@storybook/types' // Default handlers ensure no request is unhandled by msw export const worker = setupWorker(...handlers) diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 7b17d549ffc8a..7da9a39b575fb 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,20 +1,22 @@ -import { Mocks, MockSignature, mocksToHandlers } from './utils' import { + MOCK_DEFAULT_COHORT, MOCK_DEFAULT_ORGANIZATION, MOCK_DEFAULT_ORGANIZATION_INVITE, MOCK_DEFAULT_ORGANIZATION_MEMBER, + MOCK_DEFAULT_PLUGIN, + MOCK_DEFAULT_PLUGIN_CONFIG, MOCK_DEFAULT_TEAM, MOCK_DEFAULT_USER, - MOCK_DEFAULT_COHORT, MOCK_PERSON_PROPERTIES, - MOCK_DEFAULT_PLUGIN, - MOCK_DEFAULT_PLUGIN_CONFIG, - MOCK_TEAM_ID, MOCK_SECOND_ORGANIZATION_MEMBER, + MOCK_TEAM_ID, } from 'lib/api.mock' + import { getAvailableFeatures } from '~/mocks/features' import { SharingConfigurationType } from '~/types' +import { Mocks, MockSignature, mocksToHandlers } from './utils' + export const EMPTY_PAGINATED_RESPONSE = { count: 0, results: [] as any[], next: null, previous: null } export const toPaginatedResponse = (results: any[]): typeof EMPTY_PAGINATED_RESPONSE => ({ count: results.length, diff --git a/frontend/src/mocks/jest.ts b/frontend/src/mocks/jest.ts index 2a376fa4d6857..9023bc426af2f 100644 --- a/frontend/src/mocks/jest.ts +++ b/frontend/src/mocks/jest.ts @@ -1,7 +1,8 @@ import { setupServer } from 'msw/node' + +import { useAvailableFeatures } from '~/mocks/features' import { handlers } from '~/mocks/handlers' import { Mocks, mocksToHandlers } from '~/mocks/utils' -import { useAvailableFeatures } from '~/mocks/features' export const mswServer = setupServer(...handlers) export const useMocks = (mocks: Mocks): void => mswServer.use(...mocksToHandlers(mocks)) diff --git a/frontend/src/models/actionsModel.ts b/frontend/src/models/actionsModel.ts index 72735d29555b8..b6e508a3d7ed1 100644 --- a/frontend/src/models/actionsModel.ts +++ b/frontend/src/models/actionsModel.ts @@ -1,10 +1,12 @@ +import { connect, events, kea, path, props, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, path, connect, selectors, events } from 'kea' import api from 'lib/api' +import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' + import { ActionType } from '~/types' + import type { actionsModelType } from './actionsModelType' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' -import { permanentlyMount } from 'lib/utils/kea-logic-builders' export interface ActionsModelProps { params?: string diff --git a/frontend/src/models/annotationsModel.ts b/frontend/src/models/annotationsModel.ts index d193e56cee6b3..99fd787ac50c4 100644 --- a/frontend/src/models/annotationsModel.ts +++ b/frontend/src/models/annotationsModel.ts @@ -1,12 +1,14 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' -import { deleteWithUndo } from 'lib/utils' -import type { annotationsModelType } from './annotationsModelType' -import { RawAnnotationType, AnnotationType } from '~/types' import { loaders } from 'kea-loaders' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import api from 'lib/api' import { dayjsUtcToTimezone } from 'lib/dayjs' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' + +import { AnnotationType, RawAnnotationType } from '~/types' + +import type { annotationsModelType } from './annotationsModelType' export type AnnotationData = Pick export type AnnotationDataWithoutInsight = Omit diff --git a/frontend/src/models/cohortsModel.ts b/frontend/src/models/cohortsModel.ts index 6e1d00525a766..b095a9b946472 100644 --- a/frontend/src/models/cohortsModel.ts +++ b/frontend/src/models/cohortsModel.ts @@ -1,17 +1,62 @@ +import Fuse from 'fuse.js' +import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners, beforeUnmount, afterMount } from 'kea' import api from 'lib/api' -import type { cohortsModelType } from './cohortsModelType' -import { CohortType, ExporterFormat } from '~/types' -import { personsLogic } from 'scenes/persons/personsLogic' -import { deleteWithUndo, processCohort } from 'lib/utils' import { triggerExport } from 'lib/components/ExportButton/exporter' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' -import Fuse from 'fuse.js' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' +import { personsLogic } from 'scenes/persons/personsLogic' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' + +import { + AnyCohortCriteriaType, + BehavioralCohortType, + BehavioralEventType, + CohortCriteriaGroupFilter, + CohortType, + ExporterFormat, +} from '~/types' + +import type { cohortsModelType } from './cohortsModelType' const POLL_TIMEOUT = 5000 +export function processCohort(cohort: CohortType): CohortType { + return { + ...cohort, + ...{ + /* Populate value_property with value and overwrite value with corresponding behavioral filter type */ + filters: { + properties: { + ...cohort.filters.properties, + values: (cohort.filters.properties?.values?.map((group) => + 'values' in group + ? { + ...group, + values: (group.values as AnyCohortCriteriaType[]).map((c) => + c.type && + [BehavioralFilterKey.Cohort, BehavioralFilterKey.Person].includes(c.type) && + !('value_property' in c) + ? { + ...c, + value_property: c.value, + value: + c.type === BehavioralFilterKey.Cohort + ? BehavioralCohortType.InCohort + : BehavioralEventType.HaveProperty, + } + : c + ), + } + : group + ) ?? []) as CohortCriteriaGroupFilter[] | AnyCohortCriteriaType[], + }, + }, + }, + } +} + export const cohortsModel = kea([ path(['models', 'cohortsModel']), connect({ @@ -103,8 +148,8 @@ export const cohortsModel = kea([ } await triggerExport(exportCommand) }, - deleteCohort: ({ cohort }) => { - deleteWithUndo({ + deleteCohort: async ({ cohort }) => { + await deleteWithUndo({ endpoint: api.cohorts.determineDeleteEndpoint(), object: cohort, callback: actions.loadCohorts, diff --git a/frontend/src/models/dashboardsModel.test.ts b/frontend/src/models/dashboardsModel.test.ts index dcbba56fe24b2..8c021609e5ded 100644 --- a/frontend/src/models/dashboardsModel.test.ts +++ b/frontend/src/models/dashboardsModel.test.ts @@ -1,9 +1,11 @@ -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' -import { DashboardBasicType } from '~/types' +import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { DashboardBasicType } from '~/types' + import { dashboardsModel, nameCompareFunction } from './dashboardsModel' -import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants' const dashboards: Partial[] = [ { diff --git a/frontend/src/models/dashboardsModel.tsx b/frontend/src/models/dashboardsModel.tsx index 55bf7adc27e7d..54d52b20ad986 100644 --- a/frontend/src/models/dashboardsModel.tsx +++ b/frontend/src/models/dashboardsModel.tsx @@ -1,17 +1,19 @@ +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners, afterMount } from 'kea' import { router, urlToAction } from 'kea-router' import api, { PaginatedResponse } from 'lib/api' +import { GENERATED_DASHBOARD_PREFIX } from 'lib/constants' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { idToKey, isUserLoggedIn } from 'lib/utils' import { DashboardEventSource, eventUsageLogic } from 'lib/utils/eventUsageLogic' -import type { dashboardsModelType } from './dashboardsModelType' -import { DashboardBasicType, DashboardTile, DashboardType, InsightModel, InsightShortId } from '~/types' -import { urls } from 'scenes/urls' +import { permanentlyMount } from 'lib/utils/kea-logic-builders' import { teamLogic } from 'scenes/teamLogic' -import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { urls } from 'scenes/urls' + import { tagsModel } from '~/models/tagsModel' -import { GENERATED_DASHBOARD_PREFIX } from 'lib/constants' -import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { DashboardBasicType, DashboardTile, DashboardType, InsightModel, InsightShortId } from '~/types' + +import type { dashboardsModelType } from './dashboardsModelType' export const dashboardsModel = kea([ path(['models', 'dashboardsModel']), diff --git a/frontend/src/models/funnelsModel.ts b/frontend/src/models/funnelsModel.ts index 63aa6ec71f4d6..d07a155dbe873 100644 --- a/frontend/src/models/funnelsModel.ts +++ b/frontend/src/models/funnelsModel.ts @@ -1,10 +1,12 @@ +import { actions, events, kea, listeners, path, reducers } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, actions, reducers, listeners, events } from 'kea' import api from 'lib/api' import { toParams } from 'lib/utils' -import { SavedFunnel, InsightType } from '~/types' -import type { funnelsModelType } from './funnelsModelType' + +import { InsightType, SavedFunnel } from '~/types' + import { teamLogic } from '../scenes/teamLogic' +import type { funnelsModelType } from './funnelsModelType' const parseSavedFunnel = (result: Record): SavedFunnel => { return { diff --git a/frontend/src/models/groupPropertiesModel.ts b/frontend/src/models/groupPropertiesModel.ts index 067ff5ff8beba..3a5839146e186 100644 --- a/frontend/src/models/groupPropertiesModel.ts +++ b/frontend/src/models/groupPropertiesModel.ts @@ -1,10 +1,12 @@ +import { connect, events, kea, path, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, selectors, events } from 'kea' -import type { groupPropertiesModelType } from './groupPropertiesModelType' import api from 'lib/api' -import { GroupTypeProperties, PersonProperty } from '~/types' -import { teamLogic } from 'scenes/teamLogic' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' +import { teamLogic } from 'scenes/teamLogic' + +import { GroupTypeProperties, PersonProperty } from '~/types' + +import type { groupPropertiesModelType } from './groupPropertiesModelType' export const groupPropertiesModel = kea([ path(['models', 'groupPropertiesModel']), diff --git a/frontend/src/models/groupsModel.ts b/frontend/src/models/groupsModel.ts index 1c5506f5c6c87..eb8babc316dc2 100644 --- a/frontend/src/models/groupsModel.ts +++ b/frontend/src/models/groupsModel.ts @@ -1,12 +1,14 @@ -import { kea, path, connect, selectors, afterMount } from 'kea' +import { afterMount, connect, kea, path, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' -import { GroupType, GroupTypeIndex } from '~/types' -import { teamLogic } from 'scenes/teamLogic' -import type { groupsModelType } from './groupsModelType' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' -import { subscriptions } from 'kea-subscriptions' -import { loaders } from 'kea-loaders' +import { teamLogic } from 'scenes/teamLogic' + +import { GroupType, GroupTypeIndex } from '~/types' + +import type { groupsModelType } from './groupsModelType' export interface Noun { singular: string diff --git a/frontend/src/models/insightsModel.tsx b/frontend/src/models/insightsModel.tsx index 3be3e93faa231..02e0e5f21d588 100644 --- a/frontend/src/models/insightsModel.tsx +++ b/frontend/src/models/insightsModel.tsx @@ -1,10 +1,12 @@ -import { kea, path, connect, actions, listeners } from 'kea' +import { actions, connect, kea, listeners, path } from 'kea' import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { promptLogic } from 'lib/logic/promptLogic' -import { InsightModel } from '~/types' import { teamLogic } from 'scenes/teamLogic' + +import { InsightModel } from '~/types' + import type { insightsModelType } from './insightsModelType' -import { lemonToast } from 'lib/lemon-ui/lemonToast' export const insightsModel = kea([ path(['models', 'insightsModel']), diff --git a/frontend/src/models/notebooksModel.ts b/frontend/src/models/notebooksModel.ts index 057d896c0ce41..59dd745b092d0 100644 --- a/frontend/src/models/notebooksModel.ts +++ b/frontend/src/models/notebooksModel.ts @@ -1,23 +1,22 @@ import { actions, BuiltLogic, connect, kea, listeners, path, reducers } from 'kea' - import { loaders } from 'kea-loaders' -import { DashboardType, NotebookListItemType, NotebookNodeType, NotebookTarget, NotebookType } from '~/types' - +import { router } from 'kea-router' import api from 'lib/api' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import posthog from 'posthog-js' +import { notebookLogic } from 'scenes/notebooks/Notebook/notebookLogic' +import { notebookLogicType } from 'scenes/notebooks/Notebook/notebookLogicType' +import { defaultNotebookContent, EditorFocusPosition, JSONContent } from 'scenes/notebooks/Notebook/utils' +import { notebookPanelLogic } from 'scenes/notebooks/NotebookPanel/notebookPanelLogic' import { LOCAL_NOTEBOOK_TEMPLATES } from 'scenes/notebooks/NotebookTemplates/notebookTemplates' -import { deleteWithUndo } from 'lib/utils' import { teamLogic } from 'scenes/teamLogic' -import { defaultNotebookContent, EditorFocusPosition, JSONContent } from 'scenes/notebooks/Notebook/utils' - -import type { notebooksModelType } from './notebooksModelType' -import { notebookLogicType } from 'scenes/notebooks/Notebook/notebookLogicType' import { urls } from 'scenes/urls' -import { notebookLogic } from 'scenes/notebooks/Notebook/notebookLogic' -import { router } from 'kea-router' + import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { InsightVizNode, Node, NodeKind } from '~/queries/schema' -import { notebookPanelLogic } from 'scenes/notebooks/NotebookPanel/notebookPanelLogic' +import { DashboardType, NotebookListItemType, NotebookNodeType, NotebookTarget } from '~/types' + +import type { notebooksModelType } from './notebooksModelType' export const SCRATCHPAD_NOTEBOOK: NotebookListItemType = { short_id: 'scratchpad', @@ -81,7 +80,7 @@ export const notebooksModel = kea([ reducers({ scratchpadNotebook: [ - SCRATCHPAD_NOTEBOOK as NotebookListItemType, + SCRATCHPAD_NOTEBOOK, { setScratchpadNotebook: (_, { notebook }) => notebook, }, @@ -106,7 +105,7 @@ export const notebooksModel = kea([ content: defaultNotebookContent(title, content), }) - openNotebook(notebook.short_id, location, 'end', (logic) => { + await openNotebook(notebook.short_id, location, 'end', (logic) => { onCreate?.(logic) }) @@ -118,7 +117,7 @@ export const notebooksModel = kea([ }, deleteNotebook: async ({ shortId, title }) => { - deleteWithUndo({ + await deleteWithUndo({ endpoint: `projects/${values.currentTeamId}/notebooks`, object: { name: title || shortId, id: shortId }, callback: actions.loadNotebooks, @@ -138,14 +137,14 @@ export const notebooksModel = kea([ }, ], notebookTemplates: [ - LOCAL_NOTEBOOK_TEMPLATES as NotebookType[], + LOCAL_NOTEBOOK_TEMPLATES, { // In the future we can load these from remote }, ], })), - listeners(({ actions }) => ({ + listeners(({ asyncActions }) => ({ createNotebookFromDashboard: async ({ dashboard }) => { const queries = dashboard.tiles.reduce((acc, tile) => { if (!tile.insight) { @@ -186,7 +185,7 @@ export const notebooksModel = kea([ }, })) - await actions.createNotebook(NotebookTarget.Scene, dashboard.name + ' (copied)', resources) + await asyncActions.createNotebook(NotebookTarget.Scene, dashboard.name + ' (copied)', resources) }, })), ]) diff --git a/frontend/src/models/propertyDefinitionsModel.test.ts b/frontend/src/models/propertyDefinitionsModel.test.ts index bf08b2bff4112..68e6177f98ba0 100644 --- a/frontend/src/models/propertyDefinitionsModel.test.ts +++ b/frontend/src/models/propertyDefinitionsModel.test.ts @@ -1,9 +1,10 @@ -import { initKeaTests } from '~/test/init' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { expectLogic, partial } from 'kea-test-utils' -import { PropertyDefinition, PropertyDefinitionState, PropertyDefinitionType, PropertyType } from '~/types' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + import { useMocks } from '~/mocks/jest' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { initKeaTests } from '~/test/init' +import { PropertyDefinition, PropertyDefinitionState, PropertyDefinitionType, PropertyType } from '~/types' const propertyDefinitions: PropertyDefinition[] = [ { diff --git a/frontend/src/models/propertyDefinitionsModel.ts b/frontend/src/models/propertyDefinitionsModel.ts index b2dcb6cc2952d..7c4d55c4460e1 100644 --- a/frontend/src/models/propertyDefinitionsModel.ts +++ b/frontend/src/models/propertyDefinitionsModel.ts @@ -1,5 +1,12 @@ import { actions, kea, listeners, path, reducers, selectors } from 'kea' import api, { ApiMethodOptions } from 'lib/api' +import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { dayjs } from 'lib/dayjs' +import { captureTimeToSeeData } from 'lib/internalMetrics' +import { colonDelimitedDuration } from 'lib/utils' +import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import { teamLogic } from 'scenes/teamLogic' + import { BreakdownKeyType, PropertyDefinition, @@ -8,13 +15,8 @@ import { PropertyFilterValue, PropertyType, } from '~/types' + import type { propertyDefinitionsModelType } from './propertyDefinitionsModelType' -import { dayjs } from 'lib/dayjs' -import { TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' -import { colonDelimitedDuration } from 'lib/utils' -import { captureTimeToSeeData } from 'lib/internalMetrics' -import { teamLogic } from 'scenes/teamLogic' -import { permanentlyMount } from 'lib/utils/kea-logic-builders' export type PropertyDefinitionStorage = Record diff --git a/frontend/src/models/tagsModel.ts b/frontend/src/models/tagsModel.ts index 1157e6029b4b1..e5a32d8e3c4c4 100644 --- a/frontend/src/models/tagsModel.ts +++ b/frontend/src/models/tagsModel.ts @@ -1,9 +1,9 @@ import { afterMount, connect, kea, path } from 'kea' +import { loaders } from 'kea-loaders' import api from 'lib/api' +import { organizationLogic } from 'scenes/organizationLogic' import type { tagsModelType } from './tagsModelType' -import { loaders } from 'kea-loaders' -import { organizationLogic } from 'scenes/organizationLogic' export const tagsModel = kea([ path(['models', 'tagsModel']), diff --git a/frontend/src/queries/Query/Query.tsx b/frontend/src/queries/Query/Query.tsx index 369eb6792fe59..f3e00c66cee67 100644 --- a/frontend/src/queries/Query/Query.tsx +++ b/frontend/src/queries/Query/Query.tsx @@ -1,3 +1,17 @@ +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { useEffect, useState } from 'react' + +import { ErrorBoundary } from '~/layout/ErrorBoundary' +import { DataNode } from '~/queries/nodes/DataNode/DataNode' +import { DataTable } from '~/queries/nodes/DataTable/DataTable' +import { InsightViz } from '~/queries/nodes/InsightViz/InsightViz' +import { WebOverview } from '~/queries/nodes/WebOverview/WebOverview' +import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' +import { AnyResponseType, Node, QuerySchema } from '~/queries/schema' +import { QueryContext } from '~/queries/types' + +import { SavedInsight } from '../nodes/SavedInsight/SavedInsight' +import { TimeToSeeData } from '../nodes/TimeToSeeData/TimeToSeeData' import { isDataNode, isDataTableNode, @@ -6,19 +20,6 @@ import { isTimeToSeeDataSessionsNode, isWebOverviewQuery, } from '../utils' -import { DataTable } from '~/queries/nodes/DataTable/DataTable' -import { DataNode } from '~/queries/nodes/DataNode/DataNode' -import { InsightViz } from '~/queries/nodes/InsightViz/InsightViz' -import { AnyResponseType, Node, QuerySchema } from '~/queries/schema' -import { QueryContext } from '~/queries/types' - -import { ErrorBoundary } from '~/layout/ErrorBoundary' -import { useEffect, useState } from 'react' -import { TimeToSeeData } from '../nodes/TimeToSeeData/TimeToSeeData' -import { QueryEditor } from '~/queries/QueryEditor/QueryEditor' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { SavedInsight } from '../nodes/SavedInsight/SavedInsight' -import { WebOverview } from '~/queries/nodes/WebOverview/WebOverview' export interface QueryProps { /** An optional key to identify the query */ diff --git a/frontend/src/queries/QueryEditor/QueryEditor.tsx b/frontend/src/queries/QueryEditor/QueryEditor.tsx index 9d0990b64ce26..a32bd9162b540 100644 --- a/frontend/src/queries/QueryEditor/QueryEditor.tsx +++ b/frontend/src/queries/QueryEditor/QueryEditor.tsx @@ -1,13 +1,14 @@ -import { useActions, useValues } from 'kea' import { useMonaco } from '@monaco-editor/react' -import { useEffect, useState } from 'react' -import schema from '~/queries/schema.json' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { queryEditorLogic } from '~/queries/QueryEditor/queryEditorLogic' +import { useEffect, useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import clsx from 'clsx' + +import { queryEditorLogic } from '~/queries/QueryEditor/queryEditorLogic' +import schema from '~/queries/schema.json' import { QueryContext } from '~/queries/types' -import { CodeEditor } from 'lib/components/CodeEditors' export interface QueryEditorProps { query: string diff --git a/frontend/src/queries/QueryEditor/queryEditorLogic.ts b/frontend/src/queries/QueryEditor/queryEditorLogic.ts index f73a338dc72f7..585f1b84db761 100644 --- a/frontend/src/queries/QueryEditor/queryEditorLogic.ts +++ b/frontend/src/queries/QueryEditor/queryEditorLogic.ts @@ -1,9 +1,10 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' +import { lemonToast } from 'lib/lemon-ui/lemonToast' + +import { QueryEditorProps } from '~/queries/QueryEditor/QueryEditor' import { Node } from '~/queries/schema' import type { queryEditorLogicType } from './queryEditorLogicType' -import { QueryEditorProps } from '~/queries/QueryEditor/QueryEditor' -import { lemonToast } from 'lib/lemon-ui/lemonToast' function prettyJSON(source: string): string { try { diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 13df18e2a5999..2485308f00a2a 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -1,4 +1,5 @@ // This file contains example queries, used in storybook and in the /query interface. +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' import { ActionsNode, DataTableNode, @@ -26,7 +27,6 @@ import { PropertyOperator, StepOrderValue, } from '~/types' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' const Events: EventsQuery = { kind: NodeKind.EventsQuery, diff --git a/frontend/src/queries/hooks/useDebouncedQuery.ts b/frontend/src/queries/hooks/useDebouncedQuery.ts index 2464a96e19711..dfc35f3214254 100644 --- a/frontend/src/queries/hooks/useDebouncedQuery.ts +++ b/frontend/src/queries/hooks/useDebouncedQuery.ts @@ -1,6 +1,7 @@ -import { Node } from '~/queries/schema' import { useEffect, useRef, useState } from 'react' +import { Node } from '~/queries/schema' + export function useDebouncedQuery( query: T, setQuery: ((query: T) => void) | undefined, diff --git a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx index fb1be33db264b..962ead9bda621 100644 --- a/frontend/src/queries/nodes/DataNode/AutoLoad.tsx +++ b/frontend/src/queries/nodes/DataNode/AutoLoad.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' import { useEffect } from 'react' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' + export function AutoLoad(): JSX.Element { const { autoLoadToggled } = useValues(dataNodeLogic) const { startAutoLoad, stopAutoLoad, toggleAutoLoad } = useActions(dataNodeLogic) diff --git a/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx index e91daaf1ca310..7b20caa2b8037 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.stories.tsx @@ -1,18 +1,20 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { examples } from '~/queries/examples' + import { mswDecorator } from '~/mocks/browser' +import { examples } from '~/queries/examples' +import { Query } from '~/queries/Query/Query' + import events from './__mocks__/EventsNode.json' import persons from './__mocks__/PersonsNode.json' -import { Query } from '~/queries/Query/Query' type Story = StoryObj const meta: Meta = { title: 'Queries/DataNode', component: Query, + tags: ['test-skip'], parameters: { layout: 'fullscreen', viewMode: 'story', - testOptions: { skip: true }, }, decorators: [ mswDecorator({ diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index aab8597c8f4e0..97e1155a2436f 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -1,11 +1,12 @@ +import { useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { AnyResponseType, DataNode as DataNodeType, DataTableNode } from '~/queries/schema' -import { useValues } from 'kea' + import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { OpenEditorButton } from '~/queries/nodes/Node/OpenEditorButton' -import { CodeEditor } from 'lib/components/CodeEditors' +import { AnyResponseType, DataNode as DataNodeType, DataTableNode } from '~/queries/schema' interface DataNodeProps { query: DataNodeType diff --git a/frontend/src/queries/nodes/DataNode/DateRange.tsx b/frontend/src/queries/nodes/DataNode/DateRange.tsx index ce48a8ec9d892..cd2232e95bf9c 100644 --- a/frontend/src/queries/nodes/DataNode/DateRange.tsx +++ b/frontend/src/queries/nodes/DataNode/DateRange.tsx @@ -1,4 +1,5 @@ import { DateFilter } from 'lib/components/DateFilter/DateFilter' + import { DataNode, EventsQuery, HogQLQuery } from '~/queries/schema' import { isEventsQuery, isHogQLQuery } from '~/queries/utils' @@ -10,7 +11,6 @@ export function DateRange({ query, setQuery }: DateRangeProps): JSX.Element | nu if (isEventsQuery(query)) { return ( { diff --git a/frontend/src/queries/nodes/DataNode/ElapsedTime.tsx b/frontend/src/queries/nodes/DataNode/ElapsedTime.tsx index f09ab068e5f14..29dabc5c645b6 100644 --- a/frontend/src/queries/nodes/DataNode/ElapsedTime.tsx +++ b/frontend/src/queries/nodes/DataNode/ElapsedTime.tsx @@ -1,8 +1,9 @@ +import clsx from 'clsx' import { useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { useState } from 'react' import { Popover } from 'lib/lemon-ui/Popover' -import clsx from 'clsx' +import { useState } from 'react' + +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { QueryTiming } from '~/queries/schema' export interface TimingsProps { diff --git a/frontend/src/queries/nodes/DataNode/LoadNext.tsx b/frontend/src/queries/nodes/DataNode/LoadNext.tsx index d81fea67ff685..3c3f7ab1fc9fe 100644 --- a/frontend/src/queries/nodes/DataNode/LoadNext.tsx +++ b/frontend/src/queries/nodes/DataNode/LoadNext.tsx @@ -1,6 +1,7 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { LemonButton } from 'lib/lemon-ui/LemonButton' + +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { DataNode } from '~/queries/schema' import { isPersonsNode, isPersonsQuery } from '~/queries/utils' diff --git a/frontend/src/queries/nodes/DataNode/Reload.tsx b/frontend/src/queries/nodes/DataNode/Reload.tsx index f9996e2de6c0c..0b4fffe862633 100644 --- a/frontend/src/queries/nodes/DataNode/Reload.tsx +++ b/frontend/src/queries/nodes/DataNode/Reload.tsx @@ -1,9 +1,10 @@ import { useActions, useValues } from 'kea' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconRefresh } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { Spinner } from 'lib/lemon-ui/Spinner' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' + export function Reload(): JSX.Element { const { responseLoading } = useValues(dataNodeLogic) const { loadData, cancelQuery } = useActions(dataNodeLogic) diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts index 9cf95eb03127d..55a417bdff91e 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.queryCancellation.test.ts @@ -1,10 +1,11 @@ -import { initKeaTests } from '~/test/init' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { expectLogic } from 'kea-test-utils' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import * as libUtils from 'lib/utils' + +import { useMocks } from '~/mocks/jest' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { NodeKind } from '~/queries/schema' -import { useMocks } from '~/mocks/jest' -import * as libUtils from 'lib/utils' +import { initKeaTests } from '~/test/init' const testUniqueKey = 'testUniqueKey' diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts index ce40db3bace81..fbe91ff1a4653 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts @@ -1,8 +1,9 @@ -import { initKeaTests } from '~/test/init' import { expectLogic, partial } from 'kea-test-utils' + import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { NodeKind } from '~/queries/schema' import { query } from '~/queries/query' +import { NodeKind } from '~/queries/schema' +import { initKeaTests } from '~/test/init' jest.mock('~/queries/query', () => { return { diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index a83d0398a49d4..3b8ad8e62c2de 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -1,20 +1,33 @@ -import { dayjs } from 'lib/dayjs' +import clsx from 'clsx' +import equal from 'fast-deep-equal' import { + actions, + afterMount, + beforeUnmount, + connect, kea, + key, + listeners, path, props, - key, - afterMount, - selectors, propsChanged, reducers, - actions, - beforeUnmount, - listeners, - connect, + selectors, } from 'kea' import { loaders } from 'kea-loaders' -import type { dataNodeLogicType } from './dataNodeLogicType' +import { subscriptions } from 'kea-subscriptions' +import api, { ApiMethodOptions, getJSONOrThrow } from 'lib/api' +import { FEATURE_FLAGS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { objectsEqual, shouldCancelQuery, uuid } from 'lib/utils' +import { UNSAVED_INSIGHT_MIN_REFRESH_INTERVAL_MINUTES } from 'scenes/insights/insightLogic' +import { compareInsightQuery } from 'scenes/insights/utils/compareInsightQuery' +import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + +import { removeExpressionComment } from '~/queries/nodes/DataTable/utils' +import { query } from '~/queries/query' import { AnyResponseType, DataNode, @@ -26,27 +39,16 @@ import { QueryResponse, QueryTiming, } from '~/queries/schema' -import { query } from '~/queries/query' import { - isInsightQueryNode, isEventsQuery, + isInsightQueryNode, isPersonsNode, - isQueryWithHogQLSupport, isPersonsQuery, + isQueryWithHogQLSupport, } from '~/queries/utils' -import { subscriptions } from 'kea-subscriptions' -import { objectsEqual, shouldCancelQuery, uuid } from 'lib/utils' -import clsx from 'clsx' -import api, { ApiMethodOptions, getJSONOrThrow } from 'lib/api' -import { removeExpressionComment } from '~/queries/nodes/DataTable/utils' -import { userLogic } from 'scenes/userLogic' -import { UNSAVED_INSIGHT_MIN_REFRESH_INTERVAL_MINUTES } from 'scenes/insights/insightLogic' -import { teamLogic } from 'scenes/teamLogic' -import equal from 'fast-deep-equal' + import { filtersToQueryNode } from '../InsightQuery/utils/filtersToQueryNode' -import { compareInsightQuery } from 'scenes/insights/utils/compareInsightQuery' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import type { dataNodeLogicType } from './dataNodeLogicType' export interface DataNodeLogicProps { key: string @@ -477,7 +479,7 @@ export const dataNodeLogic = kea([ abortQuery: async ({ queryId }) => { try { const { currentTeamId } = values - await api.create(`api/projects/${currentTeamId}/insights/cancel`, { client_query_id: queryId }) + await api.delete(`api/projects/${currentTeamId}/query/${queryId}/`) } catch (e) { console.warn('Failed cancelling query', e) } diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss index 89ce0aff2b6ed..bc1db304a1058 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.scss @@ -3,6 +3,7 @@ width: 700px; display: flex; column-gap: 1rem; + @media (max-width: 960px) { display: block; width: auto; @@ -15,6 +16,7 @@ .HalfColumn { width: 50%; + @media (max-width: 960px) { width: 100%; } diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx index 0670b5c664c28..80b9835e4109f 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx @@ -1,28 +1,31 @@ import './ColumnConfigurator.scss' + +import { DndContext } from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { BindLogic, useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' +import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { RestrictedArea, RestrictedComponentProps, RestrictionScope } from 'lib/components/RestrictedArea' +import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { TeamMembershipLevel } from 'lib/constants' import { IconClose, IconEdit, IconTuning, SortableDragIcon } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonModal } from 'lib/lemon-ui/LemonModal' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { useState } from 'react' -import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic' -import { defaultDataTableColumns, extractExpressionComment, removeExpressionComment } from '../utils' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' + +import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' import { DataTableNode, NodeKind } from '~/queries/schema' -import { LemonModal } from 'lib/lemon-ui/LemonModal' import { isEventsQuery, taxonomicEventFilterToHogQL, trimQuotes } from '~/queries/utils' -import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon' import { PropertyFilterType } from '~/types' -import { TeamMembershipLevel } from 'lib/constants' -import { RestrictedArea, RestrictedComponentProps, RestrictionScope } from 'lib/components/RestrictedArea' -import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' -import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' -import { DndContext } from '@dnd-kit/core' -import { CSS } from '@dnd-kit/utilities' + +import { defaultDataTableColumns, extractExpressionComment, removeExpressionComment } from '../utils' +import { columnConfiguratorLogic, ColumnConfiguratorLogicProps } from './columnConfiguratorLogic' let uniqueNode = 0 diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts index 66535ef3b3194..234e0b9d7a8ab 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.test.ts @@ -1,7 +1,9 @@ -import { columnConfiguratorLogic } from './columnConfiguratorLogic' import { expectLogic } from 'kea-test-utils' + import { initKeaTests } from '~/test/init' +import { columnConfiguratorLogic } from './columnConfiguratorLogic' + describe('columnConfiguratorLogic', () => { let logic: ReturnType diff --git a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx index ee60a269a7968..8eac6b894c9e0 100644 --- a/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx +++ b/frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx @@ -1,8 +1,10 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers } from 'kea' -import type { columnConfiguratorLogicType } from './columnConfiguratorLogicType' import { teamLogic } from 'scenes/teamLogic' + import { HOGQL_COLUMNS_KEY } from '~/queries/nodes/DataTable/defaultEventsQuery' +import type { columnConfiguratorLogicType } from './columnConfiguratorLogicType' + export interface ColumnConfiguratorLogicProps { key: string columns: string[] diff --git a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts index 1c99e52230f42..6de2be61de591 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.examples.ts +++ b/frontend/src/queries/nodes/DataTable/DataTable.examples.ts @@ -1,6 +1,6 @@ +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' import { DataTableNode, NodeKind, PersonsNode } from '~/queries/schema' import { PropertyFilterType, PropertyOperator } from '~/types' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' const AllDefaults: DataTableNode = { kind: NodeKind.DataTableNode, diff --git a/frontend/src/queries/nodes/DataTable/DataTable.scss b/frontend/src/queries/nodes/DataTable/DataTable.scss index 60f05bbd38eda..ea5d0bf3de23e 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.scss +++ b/frontend/src/queries/nodes/DataTable/DataTable.scss @@ -3,17 +3,18 @@ max-width: 20rem; } - @keyframes DataTable--highlight { + @keyframes DataTable__highlight { 0% { background-color: var(--mark); } + 100% { background-color: initial; } } .DataTable__row--highlight_once { - animation: DataTable--highlight 2000ms ease-out; + animation: DataTable__highlight 2000ms ease-out; } .DataTable__row--category_row { diff --git a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx index 60072dd702ddb..64a978e45f95d 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.stories.tsx @@ -1,18 +1,20 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { Query } from '~/queries/Query/Query' -import { examples } from './DataTable.examples' + import { mswDecorator } from '~/mocks/browser' +import { Query } from '~/queries/Query/Query' + import events from '../DataNode/__mocks__/EventsNode.json' import persons from '../DataNode/__mocks__/PersonsNode.json' +import { examples } from './DataTable.examples' type Story = StoryObj const meta: Meta = { title: 'Queries/DataTable', component: Query, + tags: ['test-skip'], parameters: { layout: 'fullscreen', viewMode: 'story', - testOptions: { skip: true }, }, decorators: [ mswDecorator({ diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 8154d03fd04aa..b814b6112fb13 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,4 +1,44 @@ import './DataTable.scss' + +import clsx from 'clsx' +import { BindLogic, useValues } from 'kea' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { TaxonomicPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' +import { useCallback, useState } from 'react' +import { EventDetails } from 'scenes/events/EventDetails' +import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' +import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' +import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' + +import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' +import { DateRange } from '~/queries/nodes/DataNode/DateRange' +import { ElapsedTime } from '~/queries/nodes/DataNode/ElapsedTime' +import { LoadNext } from '~/queries/nodes/DataNode/LoadNext' +import { Reload } from '~/queries/nodes/DataNode/Reload' +import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' +import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' +import { dataTableLogic, DataTableLogicProps, DataTableRow } from '~/queries/nodes/DataTable/dataTableLogic' +import { EventRowActions } from '~/queries/nodes/DataTable/EventRowActions' +import { QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' +import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' +import { renderColumnMeta } from '~/queries/nodes/DataTable/renderColumnMeta' +import { SavedQueries } from '~/queries/nodes/DataTable/SavedQueries' +import { + extractExpressionComment, + getDataNodeDefaultColumns, + removeExpressionComment, +} from '~/queries/nodes/DataTable/utils' +import { EventName } from '~/queries/nodes/EventsNode/EventName' +import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' +import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor' +import { EditHogQLButton } from '~/queries/nodes/Node/EditHogQLButton' +import { OpenEditorButton } from '~/queries/nodes/Node/OpenEditorButton' +import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' +import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' import { AnyResponseType, DataTableNode, @@ -9,27 +49,6 @@ import { PersonsQuery, } from '~/queries/schema' import { QueryContext } from '~/queries/types' - -import { useCallback, useState } from 'react' -import { BindLogic, useValues } from 'kea' -import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' -import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { EventName } from '~/queries/nodes/EventsNode/EventName' -import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' -import { EventDetails } from 'scenes/events/EventDetails' -import { EventRowActions } from '~/queries/nodes/DataTable/EventRowActions' -import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport' -import { Reload } from '~/queries/nodes/DataNode/Reload' -import { LoadNext } from '~/queries/nodes/DataNode/LoadNext' -import { renderColumnMeta } from '~/queries/nodes/DataTable/renderColumnMeta' -import { renderColumn } from '~/queries/nodes/DataTable/renderColumn' -import { AutoLoad } from '~/queries/nodes/DataNode/AutoLoad' -import { dataTableLogic, DataTableLogicProps, DataTableRow } from '~/queries/nodes/DataTable/dataTableLogic' -import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import clsx from 'clsx' -import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' -import { OpenEditorButton } from '~/queries/nodes/Node/OpenEditorButton' import { isEventsQuery, isHogQlAggregation, @@ -38,25 +57,7 @@ import { taxonomicEventFilterToHogQL, taxonomicPersonFilterToHogQL, } from '~/queries/utils' -import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' -import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' -import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' -import { ElapsedTime } from '~/queries/nodes/DataNode/ElapsedTime' -import { DateRange } from '~/queries/nodes/DataNode/DateRange' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { TaxonomicPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { - extractExpressionComment, - getDataNodeDefaultColumns, - removeExpressionComment, -} from '~/queries/nodes/DataTable/utils' -import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' import { EventType } from '~/types' -import { SavedQueries } from '~/queries/nodes/DataTable/SavedQueries' -import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor' -import { QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' -import { EditHogQLButton } from '~/queries/nodes/Node/EditHogQLButton' interface DataTableProps { uniqueKey?: string | number @@ -470,7 +471,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } '::' ) /* Bust the LemonTable cache when columns change */ } - dataSource={(dataTableRows ?? []) as DataTableRow[]} + dataSource={dataTableRows ?? []} rowKey={({ result }: DataTableRow, rowIndex) => { if (result) { if ( diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 4970f0fd8e429..a1b04d60e6579 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -1,26 +1,29 @@ -import Papa from 'papaparse' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { IconExport } from 'lib/lemon-ui/icons' +import { LemonDivider, lemonToast } from '@posthog/lemon-ui' +import { useValues } from 'kea' import { triggerExport } from 'lib/components/ExportButton/exporter' -import { ExporterFormat } from '~/types' -import { DataNode, DataTableNode, NodeKind } from '~/queries/schema' +import { IconExport } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import Papa from 'papaparse' +import { asDisplay } from 'scenes/persons/person-utils' +import { urls } from 'scenes/urls' + +import { ExportWithConfirmation } from '~/queries/nodes/DataTable/ExportWithConfirmation' import { defaultDataTableColumns, extractExpressionComment, removeExpressionComment, } from '~/queries/nodes/DataTable/utils' -import { isEventsQuery, isHogQLQuery, isPersonsNode } from '~/queries/utils' import { getPersonsEndpoint } from '~/queries/query' -import { ExportWithConfirmation } from '~/queries/nodes/DataTable/ExportWithConfirmation' -import { DataTableRow, dataTableLogic } from './dataTableLogic' -import { useValues } from 'kea' -import { LemonDivider, lemonToast } from '@posthog/lemon-ui' -import { asDisplay } from 'scenes/persons/person-utils' -import { urls } from 'scenes/urls' +import { DataNode, DataTableNode, NodeKind } from '~/queries/schema' +import { isEventsQuery, isHogQLQuery, isPersonsNode } from '~/queries/utils' +import { ExporterFormat } from '~/types' + +import { dataTableLogic, DataTableRow } from './dataTableLogic' const EXPORT_MAX_LIMIT = 10000 -function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { +async function startDownload(query: DataTableNode, onlySelectedColumns: boolean): Promise { const exportContext = isPersonsNode(query.source) ? { path: getPersonsEndpoint(query.source) } : { source: query.source } @@ -45,7 +48,7 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void ) } } - triggerExport({ + await triggerExport({ export_format: ExporterFormat.CSV, export_context: exportContext, }) @@ -156,9 +159,7 @@ function copyTableToCsv(dataTableRows: DataTableRow[], columns: string[], query: const csv = Papa.unparse(tableData) - navigator.clipboard.writeText(csv).then(() => { - lemonToast.success('Table copied to clipboard!') - }) + void copyToClipboard(csv, 'table') } catch { lemonToast.error('Copy failed!') } @@ -170,9 +171,7 @@ function copyTableToJson(dataTableRows: DataTableRow[], columns: string[], query const json = JSON.stringify(tableData, null, 4) - navigator.clipboard.writeText(json).then(() => { - lemonToast.success('Table copied to clipboard!') - }) + void copyToClipboard(json, 'table') } catch { lemonToast.error('Copy failed!') } @@ -204,7 +203,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | key={1} placement={'topRight'} onConfirm={() => { - startDownload(query, true) + void startDownload(query, true) }} actor={isPersonsNode(query.source) ? 'persons' : 'events'} limit={EXPORT_MAX_LIMIT} @@ -220,7 +219,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | startDownload(query, false)} + onConfirm={() => void startDownload(query, false)} actor={isPersonsNode(query.source) ? 'persons' : 'events'} limit={EXPORT_MAX_LIMIT} > diff --git a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx index 0ebb345ac4c6a..405691e4bcead 100644 --- a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx +++ b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx @@ -1,15 +1,17 @@ -import { EventType } from '~/types' -import { More } from 'lib/lemon-ui/LemonButton/More' +import { useActions } from 'kea' +import { dayjs } from 'lib/dayjs' +import { IconLink, IconPlayCircle } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { createActionFromEvent } from 'scenes/events/createActionFromEvent' -import { urls } from 'scenes/urls' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { getCurrentTeamId } from 'lib/utils/logics' -import { teamLogic } from 'scenes/teamLogic' -import { IconLink, IconPlayCircle } from 'lib/lemon-ui/icons' -import { useActions } from 'kea' +import { createActionFromEvent } from 'scenes/events/createActionFromEvent' +import { insightUrlForEvent } from 'scenes/insights/utils' import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' -import { copyToClipboard, insightUrlForEvent } from 'lib/utils' -import { dayjs } from 'lib/dayjs' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { EventType } from '~/types' interface EventActionProps { event: EventType @@ -27,7 +29,7 @@ export function EventRowActions({ event }: EventActionProps): JSX.Element { - createActionFromEvent( + void createActionFromEvent( getCurrentTeamId(), event, 0, @@ -46,8 +48,8 @@ export function EventRowActions({ event }: EventActionProps): JSX.Element { fullWidth sideIcon={} data-attr="events-table-event-link" - onClick={async () => - await copyToClipboard( + onClick={() => + void copyToClipboard( `${window.location.origin}${urls.event(String(event.uuid), event.timestamp)}`, 'link to event' ) diff --git a/frontend/src/queries/nodes/DataTable/SavedQueries.tsx b/frontend/src/queries/nodes/DataTable/SavedQueries.tsx index fc088e7edc040..84e32452258f3 100644 --- a/frontend/src/queries/nodes/DataTable/SavedQueries.tsx +++ b/frontend/src/queries/nodes/DataTable/SavedQueries.tsx @@ -1,10 +1,10 @@ -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { IconBookmarkBorder } from 'lib/lemon-ui/icons' -import { DataTableNode } from '~/queries/schema' import equal from 'fast-deep-equal' import { useValues } from 'kea' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { teamLogic } from 'scenes/teamLogic' + import { getEventsQueriesForTeam } from '~/queries/nodes/DataTable/defaultEventsQuery' +import { DataTableNode } from '~/queries/schema' interface SavedQueriesProps { query: DataTableNode @@ -48,7 +48,6 @@ export function SavedQueries({ query, setQuery }: SavedQueriesProps): JSX.Elemen }} type="secondary" status="primary-alt" - icon={} > {selectedTitle} diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.test.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.test.ts index bc87b07c458b4..d37c26c2df1c9 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.test.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.test.ts @@ -1,10 +1,11 @@ -import { initKeaTests } from '~/test/init' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { expectLogic, partial } from 'kea-test-utils' -import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { DataTableNode, NodeKind } from '~/queries/schema' +import { dataTableLogic } from '~/queries/nodes/DataTable/dataTableLogic' import { query } from '~/queries/query' +import { DataTableNode, NodeKind } from '~/queries/schema' +import { initKeaTests } from '~/test/init' jest.mock('~/queries/query') diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 86d41a46e0fd9..264f7145e50e8 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -1,5 +1,12 @@ +import equal from 'fast-deep-equal' import { actions, connect, kea, key, path, props, propsChanged, reducers, selectors } from 'kea' -import type { dataTableLogicType } from './dataTableLogicType' +import { FEATURE_FLAGS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { objectsEqual, sortedKeys } from 'lib/utils' + +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' +import { getQueryFeatures, QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' import { AnyDataNode, DataTableNode, @@ -9,15 +16,10 @@ import { TimeToSeeDataSessionsQuery, } from '~/queries/schema' import { QueryContext } from '~/queries/types' -import { getColumnsForQuery, removeExpressionComment } from './utils' -import { objectsEqual, sortedKeys } from 'lib/utils' import { isDataTableNode, isEventsQuery } from '~/queries/utils' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { dayjs } from 'lib/dayjs' -import equal from 'fast-deep-equal' -import { getQueryFeatures, QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' + +import type { dataTableLogicType } from './dataTableLogicType' +import { getColumnsForQuery, removeExpressionComment } from './utils' export interface DataTableLogicProps { vizKey: string @@ -110,7 +112,7 @@ export const dataTableLogic = kea([ // Add a label between results if the day changed if (orderKey === 'timestamp' && orderKeyIndex !== -1) { - let lastResult: any | null = null + let lastResult: any = null const newResults: DataTableRow[] = [] for (const result of results) { if ( diff --git a/frontend/src/queries/nodes/DataTable/defaultEventsQuery.ts b/frontend/src/queries/nodes/DataTable/defaultEventsQuery.ts index 4820fe7d578c4..8b027b285b4fc 100644 --- a/frontend/src/queries/nodes/DataTable/defaultEventsQuery.ts +++ b/frontend/src/queries/nodes/DataTable/defaultEventsQuery.ts @@ -1,7 +1,8 @@ -import { TeamType } from '~/types' -import { EventsQuery, NodeKind } from '~/queries/schema' import { getDefaultEventsSceneQuery } from 'scenes/events/defaults' + +import { EventsQuery, NodeKind } from '~/queries/schema' import { escapePropertyAsHogQlIdentifier } from '~/queries/utils' +import { TeamType } from '~/types' /** Indicates HogQL usage if team.live_events_columns = [HOGQL_COLUMNS_KEY, ...] */ export const HOGQL_COLUMNS_KEY = '--v2:hogql' diff --git a/frontend/src/queries/nodes/DataTable/queryFeatures.ts b/frontend/src/queries/nodes/DataTable/queryFeatures.ts index 8fe11b2b7aae6..4c2b4202ea539 100644 --- a/frontend/src/queries/nodes/DataTable/queryFeatures.ts +++ b/frontend/src/queries/nodes/DataTable/queryFeatures.ts @@ -1,3 +1,4 @@ +import { Node } from '~/queries/schema' import { isEventsQuery, isHogQLQuery, @@ -7,7 +8,6 @@ import { isWebStatsTableQuery, isWebTopClicksQuery, } from '~/queries/utils' -import { Node } from '~/queries/schema' export enum QueryFeature { columnsInResponse, diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 7333f6f42fcca..846283c4cbfbb 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -1,14 +1,22 @@ -import { AnyPropertyFilter, EventType, PersonType, PropertyFilterType, PropertyOperator } from '~/types' -import { autoCaptureEventToDescription } from 'lib/utils' +import ReactJson from '@microlink/react-json-view' +import { combineUrl, router } from 'kea-router' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { Property } from 'lib/components/Property' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { Link } from 'lib/lemon-ui/Link' import { TZLabel } from 'lib/components/TZLabel' -import { Property } from 'lib/components/Property' -import { urls } from 'scenes/urls' +import { TableCellSparkline } from 'lib/lemon-ui/LemonTable/TableCellSparkline' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Link } from 'lib/lemon-ui/Link' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { autoCaptureEventToDescription } from 'lib/utils' import { PersonDisplay, PersonDisplayProps } from 'scenes/persons/PersonDisplay' +import { urls } from 'scenes/urls' + +import { errorColumn, loadingColumn } from '~/queries/nodes/DataTable/dataTableLogic' +import { DeletePersonButton } from '~/queries/nodes/PersonsNode/DeletePersonButton' import { DataTableNode, EventsQueryPersonColumn, HasPropertiesNode } from '~/queries/schema' import { QueryContext } from '~/queries/types' - import { isEventsQuery, isHogQLQuery, @@ -17,15 +25,7 @@ import { isTimeToSeeDataSessionsQuery, trimQuotes, } from '~/queries/utils' -import { combineUrl, router } from 'kea-router' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { DeletePersonButton } from '~/queries/nodes/PersonsNode/DeletePersonButton' -import ReactJson from '@microlink/react-json-view' -import { errorColumn, loadingColumn } from '~/queries/nodes/DataTable/dataTableLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { TableCellSparkline } from 'lib/lemon-ui/LemonTable/TableCellSparkline' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { AnyPropertyFilter, EventType, PersonType, PropertyFilterType, PropertyOperator } from '~/types' export function renderColumn( key: string, diff --git a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx index f73466611bad4..06d31f17df12f 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx @@ -1,12 +1,12 @@ -import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { SortingIndicator } from 'lib/lemon-ui/LemonTable/sorting' + +import { getQueryFeatures, QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' +import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' import { DataTableNode, EventsQuery } from '~/queries/schema' import { QueryContext } from '~/queries/types' - import { isHogQLQuery, trimQuotes } from '~/queries/utils' -import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' -import { SortingIndicator } from 'lib/lemon-ui/LemonTable/sorting' -import { getQueryFeatures, QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' +import { PropertyFilterType } from '~/types' export interface ColumnMeta { title?: JSX.Element | string diff --git a/frontend/src/queries/nodes/DataTable/utils.ts b/frontend/src/queries/nodes/DataTable/utils.ts index 94c196c2afe07..64ab750ec8b00 100644 --- a/frontend/src/queries/nodes/DataTable/utils.ts +++ b/frontend/src/queries/nodes/DataTable/utils.ts @@ -1,5 +1,5 @@ -import { DataNode, DataTableNode, EventsQuery, HogQLExpression, NodeKind } from '~/queries/schema' import { getQueryFeatures, QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' +import { DataNode, DataTableNode, EventsQuery, HogQLExpression, NodeKind } from '~/queries/schema' export const defaultDataTableEventColumns: HogQLExpression[] = [ '*', diff --git a/frontend/src/queries/nodes/EventsNode/EventName.tsx b/frontend/src/queries/nodes/EventsNode/EventName.tsx index 50351d57eaf3c..a6cb81111d20c 100644 --- a/frontend/src/queries/nodes/EventsNode/EventName.tsx +++ b/frontend/src/queries/nodes/EventsNode/EventName.tsx @@ -1,6 +1,7 @@ -import { EventsNode, EventsQuery } from '~/queries/schema' import { LemonEventName } from 'scenes/actions/EventName' +import { EventsNode, EventsQuery } from '~/queries/schema' + interface EventNameProps { query: EventsNode | EventsQuery setQuery?: (query: EventsNode | EventsQuery) => void diff --git a/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx b/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx index d7dc068310111..6337cd2e474a7 100644 --- a/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx +++ b/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx @@ -1,9 +1,10 @@ -import { EventsNode, EventsQuery, HogQLQuery } from '~/queries/schema' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { AnyPropertyFilter } from '~/types' -import { useState } from 'react' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { useState } from 'react' + +import { EventsNode, EventsQuery, HogQLQuery } from '~/queries/schema' import { isHogQLQuery } from '~/queries/utils' +import { AnyPropertyFilter } from '~/types' interface EventPropertyFiltersProps { query: EventsNode | EventsQuery | HogQLQuery diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index e8e8c16035ea4..bc7bb2e4f8bfd 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -1,19 +1,21 @@ -import { useActions, useValues } from 'kea' -import { HogQLQuery } from '~/queries/schema' -import { useEffect, useRef, useState } from 'react' -import { hogQLQueryEditorLogic } from './hogQLQueryEditorLogic' import { Monaco } from '@monaco-editor/react' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { IconAutoAwesome, IconInfo } from 'lib/lemon-ui/icons' import { LemonInput, Link } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' -import type { IDisposable, editor as importedEditor, languages } from 'monaco-editor' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' +import { IconAutoAwesome, IconInfo } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { CodeEditor } from 'lib/components/CodeEditors' -import clsx from 'clsx' +import type { editor as importedEditor, IDisposable, languages } from 'monaco-editor' +import { useEffect, useRef, useState } from 'react' +import { urls } from 'scenes/urls' + +import { HogQLQuery } from '~/queries/schema' + +import { hogQLQueryEditorLogic } from './hogQLQueryEditorLogic' export interface HogQLQueryEditorProps { query: HogQLQuery diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index c77502435e944..f64615df762e2 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -1,6 +1,8 @@ +import type { Monaco } from '@monaco-editor/react' import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' -import { HogQLMetadata, HogQLNotice, HogQLQuery, NodeKind } from '~/queries/schema' -import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' +import { combineUrl } from 'kea-router' +import api from 'lib/api' +import { promptLogic } from 'lib/logic/promptLogic' // Note: we can oly import types and not values from monaco-editor, because otherwise some Monaco code breaks // auto reload in development. Specifically, on this line: // `export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar')` @@ -9,13 +11,13 @@ import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' // esbuild doesn't support manual chunks as of 2023, so we can't just put Monaco in its own chunk, which would prevent // re-importing. As for @monaco-editor/react, it does some lazy loading and doesn't have this problem. import type { editor, MarkerSeverity } from 'monaco-editor' -import { query } from '~/queries/query' -import type { Monaco } from '@monaco-editor/react' -import api from 'lib/api' -import { combineUrl } from 'kea-router' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { dataWarehouseSavedQueriesLogic } from 'scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic' -import { promptLogic } from 'lib/logic/promptLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { query } from '~/queries/query' +import { HogQLMetadata, HogQLNotice, HogQLQuery, NodeKind } from '~/queries/schema' + +import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' export interface ModelMarker extends editor.IMarkerData { hogQLFix?: string @@ -83,7 +85,7 @@ export const hogQLQueryEditorLogic = kea([ ], aiAvailable: [() => [preflightLogic.selectors.preflight], (preflight) => preflight?.openai_available], }), - listeners(({ actions, props, values }) => ({ + listeners(({ actions, asyncActions, props, values }) => ({ saveQuery: () => { const query = values.queryInput // TODO: Is below line necessary if the only way for queryInput to change is already through setQueryInput? @@ -179,7 +181,7 @@ export const hogQLQueryEditorLogic = kea([ kind: NodeKind.HogQLQuery, query: values.queryInput, } - await actions.createDataWarehouseSavedQuery({ name, query }) + await asyncActions.createDataWarehouseSavedQuery({ name, query }) }, })), ]) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts index 30af701ae91a1..ac9ebba3bc553 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts @@ -1,40 +1,42 @@ import { FunnelLayout, ShownAsValue } from 'lib/constants' + import { - InsightQueryNode, - TrendsQuery, FunnelsQuery, - RetentionQuery, - StickinessQuery, + InsightQueryNode, LifecycleQuery, NodeKind, PathsQuery, + RetentionQuery, + StickinessQuery, + TrendsQuery, } from '~/queries/schema' import { - TrendsFilterType, - RetentionFilterType, - FunnelsFilterType, - PathsFilterType, - StickinessFilterType, - LifecycleFilterType, ActionFilter, BaseMathType, + BreakdownAttributionType, ChartDisplayType, FilterLogicalOperator, FilterType, + FunnelConversionWindowTimeUnit, + FunnelPathType, + FunnelsFilterType, + FunnelStepReference, + FunnelVizType, + GroupMathType, InsightType, + LifecycleFilterType, + PathsFilterType, + PathType, PropertyFilterType, PropertyMathType, PropertyOperator, - FunnelVizType, - FunnelStepReference, - BreakdownAttributionType, - FunnelConversionWindowTimeUnit, - StepOrderValue, - PathType, - FunnelPathType, + RetentionFilterType, RetentionPeriod, - GroupMathType, + StepOrderValue, + StickinessFilterType, + TrendsFilterType, } from '~/types' + import { actionsAndEventsToSeries, cleanHiddenLegendIndexes, diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index 93f5339cc929b..3b0f0fad4aadb 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -1,33 +1,34 @@ +import * as Sentry from '@sentry/react' +import { objectCleanWithEmpty } from 'lib/utils' +import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' +import { + isFunnelsFilter, + isLifecycleFilter, + isPathsFilter, + isRetentionFilter, + isStickinessFilter, + isTrendsFilter, +} from 'scenes/insights/sharedUtils' + import { - InsightQueryNode, - EventsNode, ActionsNode, - NodeKind, + EventsNode, InsightNodeKind, + InsightQueryNode, InsightsQueryBase, + NodeKind, } from '~/queries/schema' -import { FilterType, InsightType, ActionFilter } from '~/types' import { - isTrendsQuery, isFunnelsQuery, - isRetentionQuery, - isPathsQuery, - isStickinessQuery, - isLifecycleQuery, isInsightQueryWithBreakdown, isInsightQueryWithSeries, + isLifecycleQuery, + isPathsQuery, + isRetentionQuery, + isStickinessQuery, + isTrendsQuery, } from '~/queries/utils' -import { - isTrendsFilter, - isFunnelsFilter, - isRetentionFilter, - isPathsFilter, - isStickinessFilter, - isLifecycleFilter, -} from 'scenes/insights/sharedUtils' -import { objectCleanWithEmpty } from 'lib/utils' -import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' -import * as Sentry from '@sentry/react' +import { ActionFilter, FilterType, InsightType } from '~/types' const reverseInsightMap: Record, InsightNodeKind> = { [InsightType.TRENDS]: NodeKind.TrendsQuery, diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts index e6d9bb0eccdb5..eca66074d0f9f 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts @@ -1,6 +1,6 @@ import { hiddenLegendItemsToKeys, queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { InsightType, LifecycleFilterType } from '~/types' import { LifecycleQuery, NodeKind } from '~/queries/schema' +import { InsightType, LifecycleFilterType } from '~/types' describe('queryNodeToFilter', () => { test('converts a query node to a filter', () => { diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts index 53432d3c1009f..cd6a6a2848b9a 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts @@ -1,5 +1,7 @@ +import { objectClean } from 'lib/utils' +import { isFunnelsFilter, isLifecycleFilter, isStickinessFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' + import { ActionsNode, BreakdownFilter, EventsNode, InsightNodeKind, InsightQueryNode, NodeKind } from '~/queries/schema' -import { ActionFilter, EntityTypes, FilterType, InsightType } from '~/types' import { isActionsNode, isEventsNode, @@ -10,8 +12,7 @@ import { isStickinessQuery, isTrendsQuery, } from '~/queries/utils' -import { objectClean } from 'lib/utils' -import { isFunnelsFilter, isLifecycleFilter, isStickinessFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' +import { ActionFilter, EntityTypes, FilterType, InsightType } from '~/types' type FilterTypeActionsAndEvents = { events?: ActionFilter[]; actions?: ActionFilter[]; new_entity?: ActionFilter[] } @@ -86,7 +87,6 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial date_from: query.dateRange?.date_from, entity_type: 'events', sampling_factor: query.samplingFactor, - aggregation_group_type_index: query.aggregation_group_type_index, }) if (!isRetentionQuery(query) && !isPathsQuery(query)) { @@ -107,6 +107,15 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial Object.assign(filters, objectClean>>(query.breakdown)) } + if (!isLifecycleQuery(query) && !isStickinessQuery(query)) { + Object.assign( + filters, + objectClean({ + aggregation_group_type_index: query.aggregation_group_type_index, + }) + ) + } + if (isTrendsQuery(query) || isStickinessQuery(query) || isLifecycleQuery(query) || isFunnelsQuery(query)) { filters.interval = query.interval } diff --git a/frontend/src/queries/nodes/InsightViz/Breakdown.tsx b/frontend/src/queries/nodes/InsightViz/Breakdown.tsx index 9366b7b2ff2a6..c1aa833f34910 100644 --- a/frontend/src/queries/nodes/InsightViz/Breakdown.tsx +++ b/frontend/src/queries/nodes/InsightViz/Breakdown.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' -import { EditorFilterProps } from '~/types' import { TaxonomicBreakdownFilter } from 'scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownFilter' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { EditorFilterProps } from '~/types' + export function Breakdown({ insightProps }: EditorFilterProps): JSX.Element { const { breakdown, display, isTrends } = useValues(insightVizDataLogic(insightProps)) const { updateBreakdown, updateDisplay } = useActions(insightVizDataLogic(insightProps)) diff --git a/frontend/src/queries/nodes/InsightViz/ComputationTimeWithRefresh.tsx b/frontend/src/queries/nodes/InsightViz/ComputationTimeWithRefresh.tsx index 65c8dca84247e..be5b3044e2297 100644 --- a/frontend/src/queries/nodes/InsightViz/ComputationTimeWithRefresh.tsx +++ b/frontend/src/queries/nodes/InsightViz/ComputationTimeWithRefresh.tsx @@ -1,12 +1,11 @@ +import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' - import { dayjs } from 'lib/dayjs' import { usePeriodicRerender } from 'lib/hooks/usePeriodicRerender' - -import { insightLogic } from 'scenes/insights/insightLogic' import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' + import { dataNodeLogic } from '../DataNode/dataNodeLogic' -import { Link } from '@posthog/lemon-ui' export function ComputationTimeWithRefresh({ disableRefresh }: { disableRefresh?: boolean }): JSX.Element | null { const { lastRefresh, response } = useValues(dataNodeLogic) diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx index 436788cf406f4..039325e488621 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx @@ -1,13 +1,14 @@ -import { useState } from 'react' -import type { InsightLogicProps, InsightModel, InsightEditorFilterGroup } from '~/types' -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import './EditorFilterGroup.scss' + +import { PureField } from 'lib/forms/Field' import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { slugify } from 'lib/utils' import { LemonBadge } from 'lib/lemon-ui/LemonBadge/LemonBadge' -import { PureField } from 'lib/forms/Field' -import { InsightQueryNode } from '~/queries/schema' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { slugify } from 'lib/utils' +import { Fragment, useState } from 'react' -import './EditorFilterGroup.scss' +import { InsightQueryNode } from '~/queries/schema' +import type { InsightEditorFilterGroup, InsightLogicProps, InsightModel } from '~/types' export interface EditorFilterGroupProps { editorFilterGroup: InsightEditorFilterGroup @@ -48,7 +49,7 @@ export function EditorFilterGroup({ insightProps, editorFilterGroup }: EditorFil ) } return ( -
    + : Label} info={tooltip} @@ -56,7 +57,7 @@ export function EditorFilterGroup({ insightProps, editorFilterGroup }: EditorFil > {Component ? : null} -
    + ) })}
    diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilters.scss b/frontend/src/queries/nodes/InsightViz/EditorFilters.scss index de26425709f08..fb05255736d2e 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilters.scss +++ b/frontend/src/queries/nodes/InsightViz/EditorFilters.scss @@ -1,76 +1,26 @@ @import '../../../styles/mixins'; .EditorFiltersWrapper { + --editor-panel-group-min-width: 20rem; + flex-shrink: 0; background: var(--bg-light); - container-type: inline-size; &:not(.EditorFiltersWrapper--embedded) { border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; - margin-bottom: 1rem; } .EditorFilters { - > * + * { - margin-top: 1rem; - } - - @container (min-width: 768px) { - display: flex; - gap: 2rem; - - .EditorFilterGroup { - flex: 1; - width: 50%; - } - - > * + * { - margin-top: 0; - } - } - } - - &.EditorFiltersWrapper--singlecolumn { - border: none; - background: none; - padding: 0px; - margin-right: 1rem; - container-type: normal; - - @include screen($xl) { - --editor-panel-width: max(25vw, 28rem); - - .EditorFilters { - width: var(--editor-panel-width); - display: block; - padding-right: 1rem; - } - } - - .EditorFilters { - flex-direction: column; - gap: 0rem; - - .EditorFilterGroup { - width: auto; - } - - > * + * { - margin-top: 1rem; - } - } - } - - &.EditorFiltersWrapper--embedded { - margin-right: 0rem; - - @include screen($xl) { - .EditorFilters { - width: 100%; - padding-right: 0rem; - } + display: flex; + flex-flow: row wrap; + gap: 1rem; + + .EditorFilterGroup { + flex: 1; + min-width: var(--editor-panel-group-min-width); + max-width: 100%; } } } diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx index 204dd70b069c7..6c82cf18612f6 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx @@ -1,44 +1,43 @@ -import { CSSTransition } from 'react-transition-group' +import './EditorFilters.scss' + +import { LemonBanner, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useValues } from 'kea' +import { NON_BREAKDOWN_DISPLAY_TYPES } from 'lib/constants' +import { CSSTransition } from 'react-transition-group' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { Attribution } from 'scenes/insights/EditorFilters/AttributionFilter' +import { FunnelsAdvanced } from 'scenes/insights/EditorFilters/FunnelsAdvanced' +import { FunnelsQuerySteps } from 'scenes/insights/EditorFilters/FunnelsQuerySteps' +import { PathsAdvanced } from 'scenes/insights/EditorFilters/PathsAdvanced' +import { PathsEventsTypes } from 'scenes/insights/EditorFilters/PathsEventTypes' +import { PathsExclusions } from 'scenes/insights/EditorFilters/PathsExclusions' +import { PathsHogQL } from 'scenes/insights/EditorFilters/PathsHogQL' +import { PathsTargetEnd, PathsTargetStart } from 'scenes/insights/EditorFilters/PathsTarget' +import { PathsWildcardGroups } from 'scenes/insights/EditorFilters/PathsWildcardGroups' +import { RetentionSummary } from 'scenes/insights/EditorFilters/RetentionSummary' +import { SamplingFilter } from 'scenes/insights/EditorFilters/SamplingFilter' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { userLogic } from 'scenes/userLogic' +import { InsightQueryNode } from '~/queries/schema' import { - InsightEditorFilterGroup, - InsightEditorFilter, - EditorFilterProps, - ChartDisplayType, AvailableFeature, + ChartDisplayType, + EditorFilterProps, + InsightEditorFilter, + InsightEditorFilterGroup, PathType, } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { userLogic } from 'scenes/userLogic' -import { NON_BREAKDOWN_DISPLAY_TYPES } from 'lib/constants' -import { InsightQueryNode } from '~/queries/schema' +import { Breakdown } from './Breakdown' import { EditorFilterGroup } from './EditorFilterGroup' -import { LifecycleToggles } from './LifecycleToggles' import { GlobalAndOrFilters } from './GlobalAndOrFilters' +import { LifecycleToggles } from './LifecycleToggles' +import { TrendsFormula } from './TrendsFormula' import { TrendsSeries } from './TrendsSeries' import { TrendsSeriesLabel } from './TrendsSeriesLabel' -import { TrendsFormulaLabel } from './TrendsFormulaLabel' -import { TrendsFormula } from './TrendsFormula' -import { Breakdown } from './Breakdown' -import { PathsEventsTypes } from 'scenes/insights/EditorFilters/PathsEventTypes' -import { PathsTargetEnd, PathsTargetStart } from 'scenes/insights/EditorFilters/PathsTarget' -import { PathsExclusions } from 'scenes/insights/EditorFilters/PathsExclusions' -import { PathsWildcardGroups } from 'scenes/insights/EditorFilters/PathsWildcardGroups' -import { PathsAdvanced } from 'scenes/insights/EditorFilters/PathsAdvanced' -import { FunnelsQuerySteps } from 'scenes/insights/EditorFilters/FunnelsQuerySteps' -import { Attribution } from 'scenes/insights/EditorFilters/AttributionFilter' -import { FunnelsAdvanced } from 'scenes/insights/EditorFilters/FunnelsAdvanced' -import { RetentionSummary } from 'scenes/insights/EditorFilters/RetentionSummary' -import { SamplingFilter } from 'scenes/insights/EditorFilters/SamplingFilter' - -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' - -import './EditorFilters.scss' -import { PathsHogQL } from 'scenes/insights/EditorFilters/PathsHogQL' export interface EditorFiltersProps { query: InsightQueryNode @@ -62,6 +61,8 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps): breakdown, pathsFilter, querySource, + shouldShowSessionAnalysisWarning, + hasFormula, } = useValues(insightVizDataLogic(insightProps)) const { isStepsFunnel } = useValues(funnelDataLogic(insightProps)) @@ -141,10 +142,10 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps): label: isTrends ? TrendsSeriesLabel : undefined, component: TrendsSeries, }, - isTrends + isTrends && hasFormula ? { key: 'formula', - label: TrendsFormulaLabel, + label: 'Formula', component: TrendsFormula, } : null, @@ -281,12 +282,11 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps):
    - {(isFunnels ? editorFilters : editorFilterGroups).map((editorFilterGroup) => ( + {editorFilterGroups.map((editorFilterGroup) => ( ))}
    + + {shouldShowSessionAnalysisWarning ? ( + + When using sessions and session properties, events without session IDs will be excluded from the + set of results.{' '} + Learn more about sessions. + + ) : null}
    ) diff --git a/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx b/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx index 4ac27f78933e1..97de109870bc6 100644 --- a/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx @@ -1,14 +1,16 @@ -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { PropertyGroupFilters } from './PropertyGroupFilters/PropertyGroupFilters' import { useActions, useValues } from 'kea' -import { groupsModel } from '~/models/groupsModel' -import { actionsModel } from '~/models/actionsModel' -import { getAllEventNames } from './utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { EditorFilterProps } from '~/types' -import { StickinessQuery, TrendsQuery } from '~/queries/schema' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { actionsModel } from '~/models/actionsModel' +import { groupsModel } from '~/models/groupsModel' +import { StickinessQuery, TrendsQuery } from '~/queries/schema' +import { EditorFilterProps } from '~/types' + +import { PropertyGroupFilters } from './PropertyGroupFilters/PropertyGroupFilters' +import { getAllEventNames } from './utils' + export function GlobalAndOrFilters({ insightProps }: EditorFilterProps): JSX.Element { const { actions: allActions } = useValues(actionsModel) const { groupsTaxonomicTypes } = useValues(groupsModel) diff --git a/frontend/src/queries/nodes/InsightViz/Insight.scss b/frontend/src/queries/nodes/InsightViz/Insight.scss deleted file mode 100644 index dec25f40432c6..0000000000000 --- a/frontend/src/queries/nodes/InsightViz/Insight.scss +++ /dev/null @@ -1,34 +0,0 @@ -.trends-insights-container { - position: relative; - min-height: min(calc(90vh - 16rem), 36rem); - display: flex; - justify-content: center; - - .LineGraph { - height: calc(100% - 1rem) !important; - } -} - -.funnel-insights-container { - border-radius: 0 0 var(--radius) var(--radius); - - &.non-empty-state { - min-height: 26rem; - position: relative; - margin-bottom: 0; - } - - .ant-table-wrapper { - margin-top: 0 !important; - } -} - -.funnel-significance-highlight { - display: inline-flex; - background: var(--primary); - color: var(--bg-light); - - .LemonIcon { - color: var(--bg-light); - } -} diff --git a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx index 12a3a2adc2209..34490c40427a9 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx @@ -1,69 +1,83 @@ -import { ReactNode } from 'react' +import { LemonButton } from '@posthog/lemon-ui' import { useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' -import { insightDisplayConfigLogic } from './insightDisplayConfigLogic' - -import { InsightDateFilter } from 'scenes/insights/filters/InsightDateFilter' +import { ChartFilter } from 'lib/components/ChartFilter' +import { CompareFilter } from 'lib/components/CompareFilter/CompareFilter' import { IntervalFilter } from 'lib/components/IntervalFilter' import { SmoothingFilter } from 'lib/components/SmoothingFilter/SmoothingFilter' -import { RetentionDatePicker } from 'scenes/insights/RetentionDatePicker' -import { RetentionReferencePicker } from 'scenes/insights/filters/RetentionReferencePicker' -import { PathStepPicker } from 'scenes/insights/views/Paths/PathStepPicker' -import { CompareFilter } from 'lib/components/CompareFilter/CompareFilter' import { UnitPicker } from 'lib/components/UnitPicker/UnitPicker' -import { ChartFilter } from 'lib/components/ChartFilter' -import { FunnelDisplayLayoutPicker } from 'scenes/insights/views/Funnels/FunnelDisplayLayoutPicker' -import { FunnelBinsPicker } from 'scenes/insights/views/Funnels/FunnelBinsPicker' -import { ValueOnSeriesFilter } from 'scenes/insights/EditorFilters/ValueOnSeriesFilter' -import { PercentStackViewFilter } from 'scenes/insights/EditorFilters/PercentStackViewFilter' -import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' +import { FEATURE_FLAGS, NON_TIME_SERIES_DISPLAY_TYPES } from 'lib/constants' import { LemonMenu, LemonMenuItems } from 'lib/lemon-ui/LemonMenu' -import { LemonButton } from '@posthog/lemon-ui' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { ReactNode } from 'react' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { axisLabel } from 'scenes/insights/aggregationAxisFormat' -import { ChartDisplayType } from '~/types' +import { PercentStackViewFilter } from 'scenes/insights/EditorFilters/PercentStackViewFilter' import { ShowLegendFilter } from 'scenes/insights/EditorFilters/ShowLegendFilter' +import { ValueOnSeriesFilter } from 'scenes/insights/EditorFilters/ValueOnSeriesFilter' +import { InsightDateFilter } from 'scenes/insights/filters/InsightDateFilter' +import { RetentionReferencePicker } from 'scenes/insights/filters/RetentionReferencePicker' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { RetentionDatePicker } from 'scenes/insights/RetentionDatePicker' +import { FunnelBinsPicker } from 'scenes/insights/views/Funnels/FunnelBinsPicker' +import { FunnelDisplayLayoutPicker } from 'scenes/insights/views/Funnels/FunnelDisplayLayoutPicker' +import { PathStepPicker } from 'scenes/insights/views/Paths/PathStepPicker' +import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' + +import { ChartDisplayType } from '~/types' export function InsightDisplayConfig(): JSX.Element { const { insightProps } = useValues(insightLogic) + const { featureFlags } = useValues(featureFlagLogic) + const { - showDateRange, - disableDateRange, - showCompare, - showValueOnSeries, - showPercentStackView, - showUnit, - showChart, - showInterval, - showSmoothing, - showRetention, - showPaths, - showFunnelDisplayLayout, - showFunnelBins, + isTrends, + isFunnels, + isRetention, + isPaths, + isStickiness, + isLifecycle, + supportsDisplay, display, + breakdown, trendsFilter, hasLegend, showLegend, - } = useValues(insightDisplayConfigLogic(insightProps)) - - const { showPercentStackView: isPercentStackViewOn, showValueOnSeries: isValueOnSeriesOn } = useValues( - trendsDataLogic(insightProps) + supportsValueOnSeries, + showPercentStackView, + } = useValues(insightVizDataLogic(insightProps)) + const { isTrendsFunnel, isStepsFunnel, isTimeToConvertFunnel, isEmptyFunnel } = useValues( + funnelDataLogic(insightProps) ) + const showCompare = (isTrends && display !== ChartDisplayType.ActionsAreaGraph) || isStickiness + const showInterval = + isTrendsFunnel || + isLifecycle || + ((isTrends || isStickiness) && !(display && NON_TIME_SERIES_DISPLAY_TYPES.includes(display))) + const showSmoothing = + isTrends && + !breakdown?.breakdown_type && + !trendsFilter?.compare && + (!display || display === ChartDisplayType.ActionsLineGraph) && + featureFlags[FEATURE_FLAGS.SMOOTHING_INTERVAL] + + const { showPercentStackView: isPercentStackViewOn, showValueOnSeries } = useValues(trendsDataLogic(insightProps)) + const advancedOptions: LemonMenuItems = [ - ...(showValueOnSeries || showPercentStackView || hasLegend + ...(supportsValueOnSeries || showPercentStackView || hasLegend ? [ { title: 'Display', items: [ - ...(showValueOnSeries ? [{ label: () => }] : []), + ...(supportsValueOnSeries ? [{ label: () => }] : []), ...(showPercentStackView ? [{ label: () => }] : []), ...(hasLegend ? [{ label: () => }] : []), ], }, ] : []), - ...(!isPercentStackViewOn && showUnit + ...(!isPercentStackViewOn && isTrends ? [ { title: axisLabel(display || ChartDisplayType.ActionsLineGraph), @@ -73,10 +87,10 @@ export function InsightDisplayConfig(): JSX.Element { : []), ] const advancedOptionsCount: number = - (showValueOnSeries && isValueOnSeriesOn ? 1 : 0) + + (supportsValueOnSeries && showValueOnSeries ? 1 : 0) + (showPercentStackView && isPercentStackViewOn ? 1 : 0) + (!isPercentStackViewOn && - showUnit && + isTrends && trendsFilter?.aggregation_axis_format && trendsFilter.aggregation_axis_format !== 'numeric' ? 1 @@ -84,11 +98,14 @@ export function InsightDisplayConfig(): JSX.Element { (hasLegend && showLegend ? 1 : 0) return ( -
    -
    - {showDateRange && ( +
    +
    + {!isRetention && ( - + )} @@ -104,14 +121,14 @@ export function InsightDisplayConfig(): JSX.Element { )} - {showRetention && ( + {!!isRetention && ( )} - {showPaths && ( + {!!isPaths && ( @@ -123,7 +140,7 @@ export function InsightDisplayConfig(): JSX.Element { )}
    -
    +
    {advancedOptions.length > 0 && ( @@ -133,17 +150,17 @@ export function InsightDisplayConfig(): JSX.Element { )} - {showChart && ( + {supportsDisplay && ( )} - {showFunnelDisplayLayout && ( + {!!isStepsFunnel && ( )} - {showFunnelBins && ( + {!!isTimeToConvertFunnel && ( diff --git a/frontend/src/queries/nodes/InsightViz/InsightResultMetadata.tsx b/frontend/src/queries/nodes/InsightViz/InsightResultMetadata.tsx index 66c84ac6f1a46..93c353f21704e 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightResultMetadata.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightResultMetadata.tsx @@ -1,5 +1,4 @@ import { useValues } from 'kea' - import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' diff --git a/frontend/src/queries/nodes/InsightViz/InsightViz.scss b/frontend/src/queries/nodes/InsightViz/InsightViz.scss new file mode 100644 index 0000000000000..01315b0d6877b --- /dev/null +++ b/frontend/src/queries/nodes/InsightViz/InsightViz.scss @@ -0,0 +1,157 @@ +.InsightViz { + display: flex; + flex-direction: column; + overflow: hidden; + gap: 1rem; + container-type: inline-size; + flex: 1; // important for when rendered within a flex parent + + &.InsightViz--horizontal { + flex-flow: row wrap; + align-items: start; + + .EditorFiltersWrapper { + width: 100%; + + @container (min-width: 768px) { + width: 30%; + min-width: 26rem; + max-width: 30rem; + } + } + } +} + +.InsightVizDisplay { + --insight-viz-min-height: calc(80vh - 6rem); + + display: flex; + flex-direction: column; + + .NotebookNode & { + height: 100%; + flex: 1; + } + + .InsightVizDisplay__content { + display: flex; + flex: 1; + position: relative; + flex-direction: column; + + &--with-legend { + flex-direction: row; + } + + .InsightVizDisplay__content__left { + display: flex; + flex-direction: column; + flex: 1; + position: relative; + width: 100%; + } + + .InsightVizDisplay__content__right { + flex-shrink: 1; + max-width: 45%; + max-height: var(--insight-viz-min-height); + width: fit-content; + margin: 1rem; + display: flex; + align-items: center; + } + } + + .InsightDisplayConfig { + border-bottom-width: 1px; + padding: 0.5rem; + } +} + +.WebAnalyticsDashboard { + .InsightVizDisplay { + --insight-viz-min-height: 25rem; + } +} + +.RetentionContainer { + width: 100%; + display: flex; + flex-direction: column; + flex: 1; + + .RetentionContainer__graph { + flex: 1; + margin: 0.5rem; + } + + .RetentionContainer__table { + flex-shrink: 0; + } + + .LineGraph { + position: relative; + width: 100%; + min-height: 30vh; + } + + .NotebookNode & { + .LineGraph { + position: relative; + min-height: 100px; + } + } +} + +.TrendsInsight { + position: relative; + flex: 1; + margin: 0.5rem; + display: flex; + flex-direction: column; + min-height: var(--insight-viz-min-height); + + .NotebookNode & { + min-height: auto; + } + + &--ActionsTable, + &--WorldMap, + &--BoldNumber { + margin: 0; + min-height: auto; + } + + &--BoldNumber { + display: flex; + align-items: center; + justify-content: center; + } +} + +.FunnelInsight { + display: flex; + flex-direction: column; + flex: 1; + width: 100%; + + &--type-steps-vertical, + &--type-time_to_convert, + &--type-trends { + min-height: var(--insight-viz-min-height); + + .NotebookNode & { + min-height: auto; + } + } +} + +.funnel-significance-highlight { + display: inline-flex; + background: var(--primary); + color: var(--bg-light); + + .LemonIcon { + color: var(--bg-light); + } +} diff --git a/frontend/src/queries/nodes/InsightViz/InsightViz.tsx b/frontend/src/queries/nodes/InsightViz/InsightViz.tsx index 80c3c66ca3ab6..10d8e1c745f08 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightViz.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightViz.tsx @@ -1,23 +1,23 @@ -import { BindLogic, useValues } from 'kea' -import clsx from 'clsx' +import './InsightViz.scss' +import clsx from 'clsx' +import { BindLogic, useValues } from 'kea' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { useState } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { isFunnelsQuery } from '~/queries/utils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { dataNodeLogic, DataNodeLogicProps } from '../DataNode/dataNodeLogic' import { InsightVizNode } from '~/queries/schema' import { QueryContext } from '~/queries/types' +import { isFunnelsQuery } from '~/queries/utils' +import { InsightLogicProps, ItemMode } from '~/types' -import { InsightContainer } from './InsightContainer' +import { dataNodeLogic, DataNodeLogicProps } from '../DataNode/dataNodeLogic' import { EditorFilters } from './EditorFilters' -import { InsightLogicProps, ItemMode } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { InsightVizDisplay } from './InsightVizDisplay' import { getCachedResults } from './utils' -import { useState } from 'react' - -import './Insight.scss' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' /** The key for the dataNodeLogic mounted by an InsightViz for insight of insightProps */ export const insightVizDataNodeKey = (insightProps: InsightLogicProps): string => { @@ -56,6 +56,7 @@ export function InsightViz({ uniqueKey, query, setQuery, context, readOnly }: In const { insightMode } = useValues(insightSceneLogic) const isFunnels = isFunnelsQuery(query.source) + const isHorizontalAlways = useFeatureFlag('INSIGHT_HORIZONTAL_CONTROLS') const showIfFull = !!query.full const disableHeader = !(query.showHeader ?? showIfFull) @@ -72,16 +73,16 @@ export function InsightViz({ uniqueKey, query, setQuery, context, readOnly }: In
    {!readOnly && ( )} -
    - + { if (insightDataLoading) { return ( - <> -
    - -
    +
    + {!!timedOutQueryId && ( )} - +
    ) } @@ -134,9 +129,14 @@ export function InsightContainer({ case InsightType.LIFECYCLE: return case InsightType.FUNNELS: - return + return case InsightType.RETENTION: - return + return ( + + ) case InsightType.PATHS: return default: @@ -204,77 +204,61 @@ export function InsightContainer({ return null } + const showComputationMetadata = !disableLastComputation || !!samplingFactor + return ( <> - {shouldShowSessionAnalysisWarning ? ( -
    - - When using sessions and session properties, events without session IDs will be excluded from the - set of results.{' '} - Learn more about sessions. - -
    - ) : null} {/* These are filters that are reused between insight features. They each have generic logic that updates the url */} - } +
    + {disableHeader ? null : } {showingResults && ( -
    - {isFunnels && ( -
    - {/* negative margin-top so that the border is only visible when the rows wrap */} -
    - {(!disableLastComputation || !!samplingFactor) && ( -
    - -
    + <> + {(isFunnels || isPaths || showComputationMetadata) && ( +
    +
    + {showComputationMetadata && ( + )} -
    - -
    -
    - )} - {!isFunnels && (!disableLastComputation || !!samplingFactor) && ( -
    -
    - +
    + {isPaths && } + {isFunnels && }
    - -
    {isPaths ? : null}
    )} - {BlockingEmptyState ? ( - BlockingEmptyState - ) : supportsDisplay && showLegend ? ( -
    -
    {renderActiveView()}
    -
    - -
    -
    - ) : ( - renderActiveView() - )} -
    +
    + {BlockingEmptyState ? ( + BlockingEmptyState + ) : supportsDisplay && showLegend ? ( + <> +
    {renderActiveView()}
    +
    + +
    + + ) : ( + renderActiveView() + )} +
    + )} - +
    {renderTable()} {!disableCorrelationTable && activeView === InsightType.FUNNELS && } diff --git a/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx b/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx index 311f9563cd33b..04ff9cd56e709 100644 --- a/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx +++ b/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx @@ -1,9 +1,10 @@ -import { LifecycleFilter } from '~/queries/schema' -import { EditorFilterProps, LifecycleToggle } from '~/types' import { LemonCheckbox, LemonLabel } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { LifecycleFilter } from '~/queries/schema' +import { EditorFilterProps, LifecycleToggle } from '~/types' + const lifecycles: { name: LifecycleToggle; tooltip: string; color: string }[] = [ { name: 'new', diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.stories.tsx b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.stories.tsx index 69799198d8bdc..a1ddc6d8a9942 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.stories.tsx +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.stories.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { useState } from 'react' + import { FilterLogicalOperator } from '~/types' import { AndOrFilterSelect } from './AndOrFilterSelect' diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx index 2e9407f39ac25..b6583793f1523 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx @@ -1,4 +1,5 @@ import { LemonSelect } from '@posthog/lemon-ui' + import { FilterLogicalOperator } from '~/types' interface AndOrFilterSelectProps { diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss index 746da0a960300..6637f4f265e04 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss @@ -1,18 +1,23 @@ .PropertyGroupFilters { .property-group { background-color: var(--side); + + .posthog-3000 & { + border-width: 1px; + } + padding: 0.5rem; border-radius: 4px; } .property-group-and-or-separator { color: var(--primary-alt); - padding: 0.5rem 0px; + padding: 0.5rem 0; font-size: 12px; font-weight: 600; position: relative; - &:before { + &::before { position: absolute; left: 17px; top: 0; diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx index 85ddfae03059c..f1be02c46d0b0 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx @@ -1,18 +1,21 @@ -import { useValues, BindLogic, useActions } from 'kea' -import { PropertyGroupFilterValue, AnyPropertyFilter, InsightLogicProps } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import './PropertyGroupFilters.scss' -import { propertyGroupFilterLogic } from './propertyGroupFilterLogic' + +import { LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { isPropertyGroupFilterLike } from 'lib/components/PropertyFilters/utils' -import { GlobalFiltersTitle } from 'scenes/insights/common' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { IconCopy, IconDelete, IconPlusMini } from 'lib/lemon-ui/icons' -import { TestAccountFilter } from '../filters/TestAccountFilter' -import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import React from 'react' +import { GlobalFiltersTitle } from 'scenes/insights/common' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + import { InsightQueryNode, StickinessQuery, TrendsQuery } from '~/queries/schema' +import { AnyPropertyFilter, InsightLogicProps, PropertyGroupFilterValue } from '~/types' + +import { TestAccountFilter } from '../filters/TestAccountFilter' import { AndOrFilterSelect } from './AndOrFilterSelect' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { propertyGroupFilterLogic } from './propertyGroupFilterLogic' type PropertyGroupFiltersProps = { insightProps: InsightLogicProps diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts index 772c97ddd7f75..3b20beb8139a7 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/propertyGroupFilterLogic.ts @@ -1,11 +1,12 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' +import { convertPropertiesToPropertyGroup } from 'lib/components/PropertyFilters/utils' +import { objectsEqual } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { PropertyGroupFilter, FilterLogicalOperator, EmptyPropertyFilter } from '~/types' +import { StickinessQuery, TrendsQuery } from '~/queries/schema' +import { EmptyPropertyFilter, FilterLogicalOperator, PropertyGroupFilter } from '~/types' import type { propertyGroupFilterLogicType } from './propertyGroupFilterLogicType' -import { convertPropertiesToPropertyGroup, objectsEqual } from 'lib/utils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { StickinessQuery, TrendsQuery } from '~/queries/schema' export type PropertyGroupFilterLogicProps = { pageKey: string diff --git a/frontend/src/queries/nodes/InsightViz/TrendsFormula.tsx b/frontend/src/queries/nodes/InsightViz/TrendsFormula.tsx index 8e8f565e62e00..a3003a10a7e19 100644 --- a/frontend/src/queries/nodes/InsightViz/TrendsFormula.tsx +++ b/frontend/src/queries/nodes/InsightViz/TrendsFormula.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from 'react' -import { EditorFilterProps } from '~/types' -import { useActions, useValues } from 'kea' import { LemonInput } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { useEffect, useState } from 'react' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { EditorFilterProps } from '~/types' + // When updating this regex, remember to update the regex with the same name in mixins/common.py const ALLOWED_FORMULA_CHARACTERS = /^[a-zA-Z \-*^0-9+/().]+$/ diff --git a/frontend/src/queries/nodes/InsightViz/TrendsFormulaLabel.tsx b/frontend/src/queries/nodes/InsightViz/TrendsFormulaLabel.tsx deleted file mode 100644 index c694649f26c43..0000000000000 --- a/frontend/src/queries/nodes/InsightViz/TrendsFormulaLabel.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useValues } from 'kea' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { EditorFilterProps } from '~/types' - -export function TrendsFormulaLabel({ insightProps }: EditorFilterProps): JSX.Element | null { - const { hasFormula } = useValues(insightVizDataLogic(insightProps)) - return hasFormula ? <>Formula : null -} diff --git a/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx b/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx index b726171a6e43d..fa0f8c49d2aef 100644 --- a/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx +++ b/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx @@ -1,20 +1,21 @@ -import { useValues, useActions } from 'kea' -import { groupsModel } from '~/models/groupsModel' -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { FilterType } from '~/types' -import { alphabet } from 'lib/utils' -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { useActions, useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' -import { TrendsQuery, FunnelsQuery, LifecycleQuery, StickinessQuery } from '~/queries/schema' -import { isInsightQueryNode } from '~/queries/utils' -import { queryNodeToFilter } from '../InsightQuery/utils/queryNodeToFilter' -import { actionsAndEventsToSeries } from '../InsightQuery/utils/filtersToQueryNode' - -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { alphabet } from 'lib/utils' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { groupsModel } from '~/models/groupsModel' +import { FunnelsQuery, LifecycleQuery, StickinessQuery, TrendsQuery } from '~/queries/schema' +import { isInsightQueryNode } from '~/queries/utils' +import { FilterType } from '~/types' + +import { actionsAndEventsToSeries } from '../InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '../InsightQuery/utils/queryNodeToFilter' + export function TrendsSeries(): JSX.Element | null { const { insightProps } = useValues(insightLogic) const { querySource, isTrends, isLifecycle, isStickiness, display, hasFormula } = useValues( diff --git a/frontend/src/queries/nodes/InsightViz/TrendsSeriesLabel.tsx b/frontend/src/queries/nodes/InsightViz/TrendsSeriesLabel.tsx index 9e1e8e183ba68..b524f539f0b93 100644 --- a/frontend/src/queries/nodes/InsightViz/TrendsSeriesLabel.tsx +++ b/frontend/src/queries/nodes/InsightViz/TrendsSeriesLabel.tsx @@ -1,11 +1,12 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { EditorFilterProps } from '~/types' import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' -import { LemonButton } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { IconCalculate } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { EditorFilterProps } from '~/types' + export function TrendsSeriesLabel({ insightProps }: EditorFilterProps): JSX.Element { const { hasFormula, isTrends, display, series } = useValues(insightVizDataLogic(insightProps)) const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) diff --git a/frontend/src/queries/nodes/InsightViz/filters/TestAccountFilter.tsx b/frontend/src/queries/nodes/InsightViz/filters/TestAccountFilter.tsx index 1aae07d9f5860..186b4e7eeba3f 100644 --- a/frontend/src/queries/nodes/InsightViz/filters/TestAccountFilter.tsx +++ b/frontend/src/queries/nodes/InsightViz/filters/TestAccountFilter.tsx @@ -1,12 +1,12 @@ -import { useActions, useValues } from 'kea' - -import { teamLogic } from 'scenes/teamLogic' import { LemonButton, LemonSwitch } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { IconSettings } from 'lib/lemon-ui/icons' -import { InsightQueryNode } from '~/queries/schema' import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' +import { InsightQueryNode } from '~/queries/schema' + type TestAccountFilterProps = { query: InsightQueryNode setQuery: (query: InsightQueryNode) => void diff --git a/frontend/src/queries/nodes/InsightViz/insightDisplayConfigLogic.ts b/frontend/src/queries/nodes/InsightViz/insightDisplayConfigLogic.ts deleted file mode 100644 index 784b56ef7429b..0000000000000 --- a/frontend/src/queries/nodes/InsightViz/insightDisplayConfigLogic.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { kea, props, key, path, selectors, connect } from 'kea' -import { ChartDisplayType, InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' - -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { FEATURE_FLAGS, NON_TIME_SERIES_DISPLAY_TYPES, NON_VALUES_ON_SERIES_DISPLAY_TYPES } from 'lib/constants' - -import type { insightDisplayConfigLogicType } from './insightDisplayConfigLogicType' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' - -export const insightDisplayConfigLogic = kea([ - props({} as InsightLogicProps), - key(keyForInsightLogicProps('new')), - path((key) => ['queries', 'nodes', 'InsightViz', 'insightDisplayConfigLogic', key]), - - connect((props: InsightLogicProps) => ({ - values: [ - featureFlagLogic, - ['featureFlags'], - insightVizDataLogic(props), - [ - 'isTrends', - 'isFunnels', - 'isRetention', - 'isPaths', - 'isStickiness', - 'isLifecycle', - 'supportsDisplay', - 'supportsPercentStackView as showPercentStackView', - 'display', - 'compare', - 'breakdown', - 'trendsFilter', - 'hasLegend', - 'showLegend', - ], - funnelDataLogic(props), - ['isEmptyFunnel', 'isStepsFunnel', 'isTimeToConvertFunnel', 'isTrendsFunnel'], - ], - })), - - selectors({ - showDateRange: [(s) => [s.isRetention], (isRetention) => !isRetention], - disableDateRange: [ - (s) => [s.isFunnels, s.isEmptyFunnel], - (isFunnels, isEmptyFunnel) => isFunnels && !!isEmptyFunnel, - ], - showCompare: [ - (s) => [s.isTrends, s.isStickiness, s.display], - (isTrends, isStickiness, display) => - (isTrends && display !== ChartDisplayType.ActionsAreaGraph) || isStickiness, - ], - showValueOnSeries: [ - (s) => [s.isTrends, s.isStickiness, s.isLifecycle, s.display], - (isTrends, isStickiness, isLifecycle, display) => { - if (isTrends || isStickiness) { - return !NON_VALUES_ON_SERIES_DISPLAY_TYPES.includes(display || ChartDisplayType.ActionsLineGraph) - } else if (isLifecycle) { - return true - } else { - return false - } - }, - ], - showUnit: [(s) => [s.supportsDisplay, s.isTrends], (supportsDisplay, isTrends) => supportsDisplay && isTrends], - showChart: [(s) => [s.supportsDisplay], (supportsDisplay) => supportsDisplay], - showInterval: [ - (s) => [s.isTrends, s.isStickiness, s.isLifecycle, s.isTrendsFunnel, s.display], - (isTrends, isStickiness, isLifecycle, isTrendsFunnel, display) => - isTrendsFunnel || - isLifecycle || - ((isTrends || isStickiness) && !(display && NON_TIME_SERIES_DISPLAY_TYPES.includes(display))), - ], - showSmoothing: [ - (s) => [s.isTrends, s.breakdown, s.display, s.trendsFilter, s.featureFlags], - (isTrends, breakdown, display, trendsFilter, featureFlags) => - isTrends && - !breakdown?.breakdown_type && - !trendsFilter?.compare && - (!display || display === ChartDisplayType.ActionsLineGraph) && - featureFlags[FEATURE_FLAGS.SMOOTHING_INTERVAL], - ], - showRetention: [(s) => [s.isRetention], (isRetention) => !!isRetention], - showPaths: [(s) => [s.isPaths], (isPaths) => !!isPaths], - showFunnelDisplayLayout: [(s) => [s.isStepsFunnel], (isStepsFunnel) => !!isStepsFunnel], - showFunnelBins: [(s) => [s.isTimeToConvertFunnel], (isTimeToConvertFunnel) => !!isTimeToConvertFunnel], - }), -]) diff --git a/frontend/src/queries/nodes/InsightViz/utils.ts b/frontend/src/queries/nodes/InsightViz/utils.ts index b21948c769209..b06ed5b228248 100644 --- a/frontend/src/queries/nodes/InsightViz/utils.ts +++ b/frontend/src/queries/nodes/InsightViz/utils.ts @@ -1,7 +1,7 @@ -import { ActionsNode, BreakdownFilter, EventsNode, InsightQueryNode, TrendsQuery } from '~/queries/schema' -import { ActionType, ChartDisplayType, InsightModel, IntervalType } from '~/types' -import { seriesToActionsAndEvents } from '../InsightQuery/utils/queryNodeToFilter' +import equal from 'fast-deep-equal' import { getEventNamesForAction, isEmptyObject } from 'lib/utils' + +import { ActionsNode, BreakdownFilter, EventsNode, InsightQueryNode, TrendsQuery } from '~/queries/schema' import { isInsightQueryWithBreakdown, isInsightQueryWithSeries, @@ -9,8 +9,10 @@ import { isStickinessQuery, isTrendsQuery, } from '~/queries/utils' +import { ActionType, ChartDisplayType, InsightModel, IntervalType } from '~/types' + import { filtersToQueryNode } from '../InsightQuery/utils/filtersToQueryNode' -import equal from 'fast-deep-equal' +import { seriesToActionsAndEvents } from '../InsightQuery/utils/queryNodeToFilter' export const getAllEventNames = (query: InsightQueryNode, allActions: ActionType[]): string[] => { const { actions, events } = seriesToActionsAndEvents((query as TrendsQuery).series || []) @@ -103,6 +105,14 @@ export const getShowValueOnSeries = (query: InsightQueryNode): boolean | undefin } } +export const getShowLabelsOnSeries = (query: InsightQueryNode): boolean | undefined => { + if (isTrendsQuery(query)) { + return query.trendsFilter?.show_labels_on_series + } else { + return undefined + } +} + export const getShowPercentStackView = (query: InsightQueryNode): boolean | undefined => { if (isTrendsQuery(query)) { return query.trendsFilter?.show_percent_stack_view diff --git a/frontend/src/queries/nodes/Node/EditHogQLButton.tsx b/frontend/src/queries/nodes/Node/EditHogQLButton.tsx index f3a901c00aba1..74e1e6c41fef6 100644 --- a/frontend/src/queries/nodes/Node/EditHogQLButton.tsx +++ b/frontend/src/queries/nodes/Node/EditHogQLButton.tsx @@ -1,7 +1,8 @@ +import { IconQueryEditor } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' -import { NodeKind } from '~/queries/schema' import { urls } from 'scenes/urls' -import { IconQueryEditor } from 'lib/lemon-ui/icons' + +import { NodeKind } from '~/queries/schema' export interface EditHogQLButtonProps extends LemonButtonProps { hogql: string diff --git a/frontend/src/queries/nodes/Node/OpenEditorButton.tsx b/frontend/src/queries/nodes/Node/OpenEditorButton.tsx index dd409bbfa6d54..23d2b55543178 100644 --- a/frontend/src/queries/nodes/Node/OpenEditorButton.tsx +++ b/frontend/src/queries/nodes/Node/OpenEditorButton.tsx @@ -1,7 +1,8 @@ +import { IconPreview } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' -import { Node } from '~/queries/schema' import { urls } from 'scenes/urls' -import { IconPreview } from 'lib/lemon-ui/icons' + +import { Node } from '~/queries/schema' export interface OpenEditorButtonProps extends LemonButtonProps { query: Node | null diff --git a/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx b/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx index b7523213593ec..4565a6cd5b22a 100644 --- a/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx +++ b/frontend/src/queries/nodes/PersonsNode/DeletePersonButton.tsx @@ -1,9 +1,10 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconDelete } from 'lib/lemon-ui/icons' import { useActions } from 'kea' -import { PersonType } from '~/types' +import { IconDelete } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' + import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' +import { PersonType } from '~/types' interface DeletePersonButtonProps { person: PersonType diff --git a/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx index f8ddaa48b44b1..43f633da0d80a 100644 --- a/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx +++ b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx @@ -1,9 +1,10 @@ -import { PersonsNode, PersonsQuery } from '~/queries/schema' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { PersonPropertyFilter } from '~/types' -import { useState } from 'react' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { useState } from 'react' + +import { PersonsNode, PersonsQuery } from '~/queries/schema' import { isPersonsQuery } from '~/queries/utils' +import { PersonPropertyFilter } from '~/types' interface PersonPropertyFiltersProps { query: PersonsNode | PersonsQuery diff --git a/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx index a190d1b4434b6..b660556ea23a8 100644 --- a/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx +++ b/frontend/src/queries/nodes/PersonsNode/PersonsSearch.tsx @@ -1,8 +1,9 @@ -import { PersonsNode, PersonsQuery } from '~/queries/schema' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { IconInfo } from 'lib/lemon-ui/icons' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { Tooltip } from 'lib/lemon-ui/Tooltip' + import { useDebouncedQuery } from '~/queries/hooks/useDebouncedQuery' +import { PersonsNode, PersonsQuery } from '~/queries/schema' interface PersonSearchProps { query: PersonsNode | PersonsQuery diff --git a/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx b/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx index 11b4e87e03b1e..5ab40c629a87e 100644 --- a/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx +++ b/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx @@ -1,14 +1,13 @@ import { useValues } from 'kea' - +import { AnimationType } from 'lib/animations/animations' +import { Animation } from 'lib/components/Animation/Animation' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' import { insightLogic } from 'scenes/insights/insightLogic' + import { Query } from '~/queries/Query/Query' import { SavedInsightNode } from '~/queries/schema' import { QueryContext } from '~/queries/types' - import { InsightLogicProps } from '~/types' -import { Animation } from 'lib/components/Animation/Animation' -import { AnimationType } from 'lib/animations/animations' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' interface InsightProps { query: SavedInsightNode diff --git a/frontend/src/queries/nodes/TimeToSeeData/TimeToSeeData.tsx b/frontend/src/queries/nodes/TimeToSeeData/TimeToSeeData.tsx index 510078a1c1e80..f4ab309ec11cf 100644 --- a/frontend/src/queries/nodes/TimeToSeeData/TimeToSeeData.tsx +++ b/frontend/src/queries/nodes/TimeToSeeData/TimeToSeeData.tsx @@ -1,12 +1,14 @@ +import { useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { useState } from 'react' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' + import { AnyResponseType, NodeKind, TimeToSeeDataNode } from '~/queries/schema' -import { useValues } from 'kea' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' + import { dataNodeLogic } from '../DataNode/dataNodeLogic' import { Trace } from './Trace/Trace' import { TimeToSeeSessionNode } from './types' -import { CodeEditor } from 'lib/components/CodeEditors' let uniqueNode = 0 diff --git a/frontend/src/queries/nodes/TimeToSeeData/Trace/Trace.tsx b/frontend/src/queries/nodes/TimeToSeeData/Trace/Trace.tsx index bcd5f3e4acc3d..64293e6b71621 100644 --- a/frontend/src/queries/nodes/TimeToSeeData/Trace/Trace.tsx +++ b/frontend/src/queries/nodes/TimeToSeeData/Trace/Trace.tsx @@ -1,15 +1,16 @@ +import { Tooltip } from '@posthog/lemon-ui' import clsx from 'clsx' import { BindLogic, useValues } from 'kea' import { getSeriesColor } from 'lib/colors' import { TZLabel } from 'lib/components/TZLabel' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconSad } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { humanFriendlyDuration, humanFriendlyMilliseconds } from 'lib/utils' import { RefCallback, useEffect, useState } from 'react' import useResizeObserver from 'use-resize-observer' + import { isInteractionNode, isQueryNode, isSessionNode, TimeToSeeNode, TimeToSeeSessionNode } from '../types' import { sessionNodeFacts, SpanData, traceLogic } from './traceLogic' -import { Tooltip } from '@posthog/lemon-ui' export interface TraceProps { timeToSeeSession: TimeToSeeSessionNode diff --git a/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx b/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx index 37f2654050acf..c36416b2fe033 100644 --- a/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx +++ b/frontend/src/queries/nodes/TimeToSeeData/Trace/traceLogic.tsx @@ -2,6 +2,7 @@ import { kea, key, path, props, selectors } from 'kea' import { dayjs } from 'lib/dayjs' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { humanFriendlyMilliseconds } from 'lib/utils' + import { isSessionNode, TimeToSeeInteractionNode, @@ -9,7 +10,6 @@ import { TimeToSeeQueryNode, TimeToSeeSessionNode, } from '../types' - import type { traceLogicType } from './traceLogicType' export interface TraceLogicProps { diff --git a/frontend/src/queries/nodes/WebOverview/EvenlyDistributedRows.tsx b/frontend/src/queries/nodes/WebOverview/EvenlyDistributedRows.tsx index d40781079d9e4..eb9d32084d6c0 100644 --- a/frontend/src/queries/nodes/WebOverview/EvenlyDistributedRows.tsx +++ b/frontend/src/queries/nodes/WebOverview/EvenlyDistributedRows.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' import clsx from 'clsx' +import React, { useCallback, useEffect, useRef, useState } from 'react' export const EvenlyDistributedRows = ({ children, diff --git a/frontend/src/queries/nodes/WebOverview/WebOverview.tsx b/frontend/src/queries/nodes/WebOverview/WebOverview.tsx index 9de6ccdbac0b3..5612f1060eda8 100644 --- a/frontend/src/queries/nodes/WebOverview/WebOverview.tsx +++ b/frontend/src/queries/nodes/WebOverview/WebOverview.tsx @@ -1,13 +1,15 @@ -import { useState } from 'react' -import { AnyResponseType, WebOverviewItem, WebOverviewQuery, WebOverviewQueryResponse } from '~/queries/schema' import { useValues } from 'kea' +import { getColorVar } from 'lib/colors' +import { IconTrendingDown, IconTrendingFlat, IconTrendingUp } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { dataNodeLogic } from '../DataNode/dataNodeLogic' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { humanFriendlyDuration, humanFriendlyLargeNumber, isNotNil } from 'lib/utils' -import { IconTrendingDown, IconTrendingFlat, IconTrendingUp } from 'lib/lemon-ui/icons' -import { getColorVar } from 'lib/colors' +import { useState } from 'react' + import { EvenlyDistributedRows } from '~/queries/nodes/WebOverview/EvenlyDistributedRows' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { AnyResponseType, WebOverviewItem, WebOverviewQuery, WebOverviewQueryResponse } from '~/queries/schema' + +import { dataNodeLogic } from '../DataNode/dataNodeLogic' let uniqueNode = 0 export function WebOverview(props: { query: WebOverviewQuery; cachedResults?: AnyResponseType }): JSX.Element | null { diff --git a/frontend/src/queries/query.test.ts b/frontend/src/queries/query.test.ts index 5ae28a1a657e0..7393f59d2868c 100644 --- a/frontend/src/queries/query.test.ts +++ b/frontend/src/queries/query.test.ts @@ -1,9 +1,10 @@ +import posthog from 'posthog-js' + +import { useMocks } from '~/mocks/jest' import { query, queryExportContext } from '~/queries/query' import { EventsQuery, HogQLQuery, NodeKind } from '~/queries/schema' -import { PropertyFilterType, PropertyOperator } from '~/types' import { initKeaTests } from '~/test/init' -import posthog from 'posthog-js' -import { useMocks } from '~/mocks/jest' +import { PropertyFilterType, PropertyOperator } from '~/types' describe('query', () => { beforeEach(() => { diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index df71c0b1cef4b..29d08863a62c0 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -1,22 +1,11 @@ -import posthog from 'posthog-js' -import { DataNode, HogQLQuery, HogQLQueryResponse, NodeKind, PersonsNode } from './schema' -import { - isInsightQueryNode, - isEventsQuery, - isPersonsNode, - isTimeToSeeDataSessionsQuery, - isTimeToSeeDataQuery, - isDataTableNode, - isTimeToSeeDataSessionsNode, - isHogQLQuery, - isInsightVizNode, - isQueryWithHogQLSupport, - isPersonsQuery, - isLifecycleQuery, -} from './utils' import api, { ApiMethodOptions } from 'lib/api' +import { FEATURE_FLAGS } from 'lib/constants' +import { now } from 'lib/dayjs' +import { currentSessionId } from 'lib/internalMetrics' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { delay, flattenObject, toParams } from 'lib/utils' import { getCurrentTeamId } from 'lib/utils/logics' -import { AnyPartialFilterType, OnlineExportContext, QueryExportContext } from '~/types' +import posthog from 'posthog-js' import { filterTrendsClientSideParams, isFunnelsFilter, @@ -26,12 +15,28 @@ import { isStickinessFilter, isTrendsFilter, } from 'scenes/insights/sharedUtils' -import { flattenObject, toParams } from 'lib/utils' + +import { AnyPartialFilterType, OnlineExportContext, QueryExportContext } from '~/types' + import { queryNodeToFilter } from './nodes/InsightQuery/utils/queryNodeToFilter' -import { now } from 'lib/dayjs' -import { currentSessionId } from 'lib/internalMetrics' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import { DataNode, HogQLQuery, HogQLQueryResponse, NodeKind, PersonsNode } from './schema' +import { + isDataTableNode, + isEventsQuery, + isHogQLQuery, + isInsightQueryNode, + isInsightVizNode, + isLifecycleQuery, + isPersonsNode, + isPersonsQuery, + isQueryWithHogQLSupport, + isTimeToSeeDataQuery, + isTimeToSeeDataSessionsNode, + isTimeToSeeDataSessionsQuery, +} from './utils' + +const QUERY_ASYNC_MAX_INTERVAL_SECONDS = 10 +const QUERY_ASYNC_TOTAL_POLL_SECONDS = 300 //get export context for a given query export function queryExportContext( @@ -91,6 +96,43 @@ export function queryExportContext( throw new Error(`Unsupported query: ${query.kind}`) } +async function executeQuery( + queryNode: N, + methodOptions?: ApiMethodOptions, + refresh?: boolean, + queryId?: string +): Promise> { + const queryAsyncEnabled = Boolean(featureFlagLogic.findMounted()?.values.featureFlags?.[FEATURE_FLAGS.QUERY_ASYNC]) + const excludedKinds = ['HogQLMetadata'] + const queryAsync = queryAsyncEnabled && !excludedKinds.includes(queryNode.kind) + const response = await api.query(queryNode, methodOptions, queryId, refresh, queryAsync) + + if (!queryAsync || !response.query_async) { + return response + } + + const pollStart = performance.now() + let currentDelay = 300 // start low, because all queries will take at minimum this + + while (performance.now() - pollStart < QUERY_ASYNC_TOTAL_POLL_SECONDS * 1000) { + await delay(currentDelay) + currentDelay = Math.min(currentDelay * 2, QUERY_ASYNC_MAX_INTERVAL_SECONDS * 1000) + + if (methodOptions?.signal?.aborted) { + const customAbortError = new Error('Query aborted') + customAbortError.name = 'AbortError' + throw customAbortError + } + + const statusResponse = await api.queryStatus.get(response.id) + + if (statusResponse.complete || statusResponse.error) { + return statusResponse.results + } + } + throw new Error('Query timed out') +} + // Return data for a given query export async function query( queryNode: N, @@ -216,7 +258,7 @@ export async function query( response = await fetchLegacyInsights() } } else { - response = await api.query(queryNode, methodOptions, queryId, refresh) + response = await executeQuery(queryNode, methodOptions, refresh, queryId) if (isHogQLQuery(queryNode) && response && typeof response === 'object') { logParams.clickhouse_sql = (response as HogQLQueryResponse)?.clickhouse } diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index d1eed070437ff..7d5f3cb358ffc 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1526,6 +1526,10 @@ "enum": ["leftjoin", "subquery"], "type": "string" }, + "materializationMode": { + "enum": ["auto", "legacy_null_as_string", "legacy_null_as_null", "disabled"], + "type": "string" + }, "personsArgMaxVersion": { "enum": ["auto", "v1", "v2"], "type": "string" @@ -1732,6 +1736,9 @@ }, "suppressSessionAnalysisWarning": { "type": "boolean" + }, + "vizSpecificOptions": { + "$ref": "#/definitions/VizSpecificOptions" } }, "required": ["kind", "source"], @@ -1801,10 +1808,6 @@ "LifecycleQuery": { "additionalProperties": false, "properties": { - "aggregation_group_type_index": { - "description": "Groups aggregation", - "type": "number" - }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -2410,6 +2413,51 @@ } ] }, + "QueryStatus": { + "additionalProperties": false, + "properties": { + "complete": { + "default": false, + "type": "boolean" + }, + "end_time": { + "format": "date-time", + "type": "string" + }, + "error": { + "default": false, + "type": "boolean" + }, + "error_message": { + "default": "", + "type": "string" + }, + "expiration_time": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "query_async": { + "default": true, + "type": "boolean" + }, + "results": {}, + "start_time": { + "format": "date-time", + "type": "string" + }, + "task_id": { + "type": "string" + }, + "team_id": { + "type": "integer" + } + }, + "required": ["id", "query_async", "team_id", "error", "complete", "error_message"], + "type": "object" + }, "QueryTiming": { "additionalProperties": false, "properties": { @@ -2644,6 +2692,9 @@ }, "suppressSessionAnalysisWarning": { "type": "boolean" + }, + "vizSpecificOptions": { + "$ref": "#/definitions/VizSpecificOptions" } }, "required": ["kind", "shortId"], @@ -2757,10 +2808,6 @@ "StickinessQuery": { "additionalProperties": false, "properties": { - "aggregation_group_type_index": { - "description": "Groups aggregation", - "type": "number" - }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -2973,6 +3020,9 @@ }, "type": "array" }, + "show_labels_on_series": { + "type": "boolean" + }, "show_legend": { "type": "boolean" }, @@ -3089,6 +3139,40 @@ "required": ["results"], "type": "object" }, + "VizSpecificOptions": { + "additionalProperties": false, + "description": "Chart specific rendering options. Use ChartRenderingMetadata for non-serializable values, e.g. onClick handlers", + "properties": { + "ActionsPie": { + "additionalProperties": false, + "properties": { + "disableHoverOffset": { + "type": "boolean" + }, + "hideAggregation": { + "type": "boolean" + } + }, + "type": "object" + }, + "RETENTION": { + "additionalProperties": false, + "properties": { + "hideLineGraph": { + "type": "boolean" + }, + "hideSizeColumn": { + "type": "boolean" + }, + "useSmallLayout": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, "WebAnalyticsPropertyFilter": { "anyOf": [ { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index fdba8908a3a71..1f035b9d1acba 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -4,6 +4,7 @@ import { Breakdown, BreakdownKeyType, BreakdownType, + ChartDisplayType, CountPerActorMathType, EventPropertyFilter, EventType, @@ -12,6 +13,7 @@ import { GroupMathType, HogQLMathType, InsightShortId, + InsightType, IntervalType, LifecycleFilterType, LifecycleToggle, @@ -136,6 +138,7 @@ export interface HogQLQueryModifiers { personsOnEventsMode?: 'disabled' | 'v1_enabled' | 'v1_mixed' | 'v2_enabled' personsArgMaxVersion?: 'auto' | 'v1' | 'v2' inCohortVia?: 'leftjoin' | 'subquery' + materializationMode?: 'auto' | 'legacy_null_as_string' | 'legacy_null_as_null' | 'disabled' } export interface HogQLQueryResponse { @@ -391,6 +394,22 @@ export interface SavedInsightNode extends Node, InsightVizNodeViewProps, DataTab // Insight viz node +/** Chart specific rendering options. + * Use ChartRenderingMetadata for non-serializable values, e.g. onClick handlers + * @see ChartRenderingMetadata + * **/ +export interface VizSpecificOptions { + [InsightType.RETENTION]?: { + hideLineGraph?: boolean + hideSizeColumn?: boolean + useSmallLayout?: boolean + } + [ChartDisplayType.ActionsPie]?: { + disableHoverOffset?: boolean + hideAggregation?: boolean + } +} + export interface InsightVizNode extends Node, InsightVizNodeViewProps { kind: NodeKind.InsightVizNode source: InsightQueryNode @@ -410,6 +429,7 @@ interface InsightVizNodeViewProps { embedded?: boolean suppressSessionAnalysisWarning?: boolean hidePersonsModal?: boolean + vizSpecificOptions?: VizSpecificOptions } /** Base class for insight query nodes. Should not be used directly. */ @@ -501,7 +521,7 @@ export type StickinessFilter = Omit< StickinessFilterType & { hidden_legend_indexes?: number[] }, keyof FilterType | 'hidden_legend_keys' | 'stickiness_days' | 'shown_as' > -export interface StickinessQuery extends InsightsQueryBase { +export interface StickinessQuery extends Omit { kind: NodeKind.StickinessQuery /** Granularity of the response. Can be one of `hour`, `day`, `week` or `month` */ interval?: IntervalType @@ -526,11 +546,33 @@ export interface QueryResponse { next_allowed_client_refresh?: string } +export type QueryStatus = { + id: string + /** @default true */ + query_async: boolean + /** @asType integer */ + team_id: number + /** @default false */ + error: boolean + /** @default false */ + complete: boolean + /** @default "" */ + error_message: string + results?: any + /** @format date-time */ + start_time?: string + /** @format date-time */ + end_time?: string + /** @format date-time */ + expiration_time?: string + task_id?: string +} + export interface LifecycleQueryResponse extends QueryResponse { results: Record[] } -export interface LifecycleQuery extends InsightsQueryBase { +export interface LifecycleQuery extends Omit { kind: NodeKind.LifecycleQuery /** Granularity of the response. Can be one of `hour`, `day`, `week` or `month` */ interval?: IntervalType diff --git a/frontend/src/queries/types.ts b/frontend/src/queries/types.ts index f1e63d8f54549..2ae3a75e65009 100644 --- a/frontend/src/queries/types.ts +++ b/frontend/src/queries/types.ts @@ -1,6 +1,7 @@ -import { ChartDisplayType, InsightLogicProps, TrendResult } from '~/types' import { ComponentType, HTMLProps } from 'react' + import { DataTableNode } from '~/queries/schema' +import { ChartDisplayType, GraphPointPayload, InsightLogicProps, TrendResult } from '~/types' /** Pass custom metadata to queries. Used for e.g. custom columns in the DataTable. */ export interface QueryContext { @@ -24,6 +25,9 @@ export interface ChartRenderingMetadata { [ChartDisplayType.WorldMap]?: { countryProps?: (countryCode: string, countryData: TrendResult | undefined) => Omit, 'key'> } + [ChartDisplayType.ActionsPie]?: { + onSegmentClick?: (payload: GraphPointPayload) => void + } } export type QueryContextColumnTitleComponent = ComponentType<{ diff --git a/frontend/src/queries/utils.test.ts b/frontend/src/queries/utils.test.ts index d20ea8c377af2..60a561eb68b2c 100644 --- a/frontend/src/queries/utils.test.ts +++ b/frontend/src/queries/utils.test.ts @@ -1,10 +1,12 @@ +import { MOCK_TEAM_ID } from 'lib/api.mock' import { dayjs } from 'lib/dayjs' -import { hogql } from './utils' import { teamLogic } from 'scenes/teamLogic' + import { initKeaTests } from '~/test/init' -import { MOCK_TEAM_ID } from 'lib/api.mock' import { AppContext, TeamType } from '~/types' +import { hogql } from './utils' + window.POSTHOG_APP_CONTEXT = { current_team: { id: MOCK_TEAM_ID } } as unknown as AppContext describe('hogql tag', () => { diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index de393a7fbd659..79151b209a89e 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -1,3 +1,7 @@ +import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { dayjs } from 'lib/dayjs' +import { teamLogic } from 'scenes/teamLogic' + import { ActionsNode, DatabaseSchemaQuery, @@ -32,9 +36,6 @@ import { WebStatsTableQuery, WebTopClicksQuery, } from '~/queries/schema' -import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' -import { dayjs } from 'lib/dayjs' -import { teamLogic } from 'scenes/teamLogic' export function isDataNode(node?: Node | null): node is EventsQuery | PersonsNode | TimeToSeeDataSessionsQuery { return ( diff --git a/frontend/src/scenes/App.tsx b/frontend/src/scenes/App.tsx index bfeb6836c9ef0..1bc924fbc7bfc 100644 --- a/frontend/src/scenes/App.tsx +++ b/frontend/src/scenes/App.tsx @@ -1,29 +1,31 @@ -import { kea, useMountedLogic, useValues, BindLogic, path, connect, actions, reducers, selectors, events } from 'kea' -import { ToastContainer, Slide } from 'react-toastify' -import { preflightLogic } from './PreflightCheck/preflightLogic' -import { userLogic } from 'scenes/userLogic' -import { sceneLogic } from 'scenes/sceneLogic' +import { actions, BindLogic, connect, events, kea, path, reducers, selectors, useMountedLogic, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { ToastCloseButton } from 'lib/lemon-ui/lemonToast' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import type { appLogicType } from './AppType' -import { teamLogic } from './teamLogic' -import { LoadedScene } from 'scenes/sceneTypes' +import { inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' +import { useEffect } from 'react' +import { Slide, ToastContainer } from 'react-toastify' +import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' import { appScenes } from 'scenes/appScenes' -import { Navigation as NavigationClassic } from '~/layout/navigation/Navigation' +import { organizationLogic } from 'scenes/organizationLogic' +import { sceneLogic } from 'scenes/sceneLogic' +import { LoadedScene } from 'scenes/sceneTypes' +import { userLogic } from 'scenes/userLogic' + import { ErrorBoundary } from '~/layout/ErrorBoundary' +import { GlobalModals } from '~/layout/GlobalModals' import { breadcrumbsLogic } from '~/layout/navigation/Breadcrumbs/breadcrumbsLogic' -import { organizationLogic } from 'scenes/organizationLogic' -import { ToastCloseButton } from 'lib/lemon-ui/lemonToast' -import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' -import { inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { FEATURE_FLAGS } from 'lib/constants' +import { Navigation as NavigationClassic } from '~/layout/navigation/Navigation' import { Navigation as Navigation3000 } from '~/layout/navigation-3000/Navigation' -import { useEffect } from 'react' import { themeLogic } from '~/layout/navigation-3000/themeLogic' -import { GlobalModals } from '~/layout/GlobalModals' import { actionsModel } from '~/models/actionsModel' import { cohortsModel } from '~/models/cohortsModel' +import type { appLogicType } from './AppType' +import { preflightLogic } from './PreflightCheck/preflightLogic' +import { teamLogic } from './teamLogic' + export const appLogic = kea([ path(['scenes', 'App']), connect([teamLogic, organizationLogic, frontendAppsLogic, inAppPromptLogic, actionsModel, cohortsModel]), @@ -165,9 +167,7 @@ function AppScene(): JSX.Element | null { return ( <> - - {wrappedSceneElement} - + {wrappedSceneElement} {toastContainer} diff --git a/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx b/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx index df3757fcfcdf5..3a1cb65525eb6 100644 --- a/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx +++ b/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx @@ -1,5 +1,5 @@ -import { SceneExport } from 'scenes/sceneTypes' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { SceneExport } from 'scenes/sceneTypes' import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' export const scene: SceneExport = { diff --git a/frontend/src/scenes/PreflightCheck/PreflightCheck.scss b/frontend/src/scenes/PreflightCheck/PreflightCheck.scss index 21859e46108bb..0628c2ac5d99f 100644 --- a/frontend/src/scenes/PreflightCheck/PreflightCheck.scss +++ b/frontend/src/scenes/PreflightCheck/PreflightCheck.scss @@ -1,6 +1,7 @@ .Preflight { max-width: 400px; padding: 1rem; + .Preflight__header { p { margin-bottom: 0.5rem; @@ -80,7 +81,7 @@ svg, .Preflight__status-text { - color: var(--primary); + color: var(--primary-3000); } } diff --git a/frontend/src/scenes/PreflightCheck/PreflightCheck.tsx b/frontend/src/scenes/PreflightCheck/PreflightCheck.tsx index dfbadfeb38a13..fab3d72dbe6c0 100644 --- a/frontend/src/scenes/PreflightCheck/PreflightCheck.tsx +++ b/frontend/src/scenes/PreflightCheck/PreflightCheck.tsx @@ -1,23 +1,25 @@ -import { useValues, useActions } from 'kea' -import { PreflightCheckStatus, PreflightItem, preflightLogic } from './preflightLogic' import './PreflightCheck.scss' -import { capitalizeFirstLetter } from 'lib/utils' -import { SceneExport } from 'scenes/sceneTypes' -import { LemonButton } from 'lib/lemon-ui/LemonButton' + +import { Link, Spinner } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' +import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { IconCheckCircleOutline, IconErrorOutline, + IconRefresh, IconUnfoldLess, IconUnfoldMore, - IconRefresh, IconWarning, } from 'lib/lemon-ui/icons' -import clsx from 'clsx' -import { LemonRow } from 'lib/lemon-ui/LemonRow' -import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { Link, Spinner } from '@posthog/lemon-ui' +import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { capitalizeFirstLetter } from 'lib/utils' +import { SceneExport } from 'scenes/sceneTypes' + +import { PreflightCheckStatus, PreflightItem, preflightLogic } from './preflightLogic' export const scene: SceneExport = { component: PreflightCheck, diff --git a/frontend/src/scenes/PreflightCheck/preflightLogic.test.ts b/frontend/src/scenes/PreflightCheck/preflightLogic.test.ts index 5ae4c1839df05..9ef3b88c8c0a8 100644 --- a/frontend/src/scenes/PreflightCheck/preflightLogic.test.ts +++ b/frontend/src/scenes/PreflightCheck/preflightLogic.test.ts @@ -1,7 +1,9 @@ import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' import { urls } from 'scenes/urls' + import { initKeaTests } from '~/test/init' + import { preflightLogic } from './preflightLogic' describe('preflightLogic', () => { diff --git a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx index ae958b00c996b..c57b8a20178b8 100644 --- a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx +++ b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx @@ -1,12 +1,14 @@ import { actions, events, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { actionToUrl, router, urlToAction } from 'kea-router' import api from 'lib/api' -import { PreflightStatus, Realm } from '~/types' -import posthog from 'posthog-js' import { getAppContext } from 'lib/utils/getAppContext' -import type { preflightLogicType } from './preflightLogicType' +import posthog from 'posthog-js' import { urls } from 'scenes/urls' -import { actionToUrl, router, urlToAction } from 'kea-router' -import { loaders } from 'kea-loaders' + +import { PreflightStatus, Realm } from '~/types' + +import type { preflightLogicType } from './preflightLogicType' export type PreflightMode = 'experimentation' | 'live' @@ -37,7 +39,7 @@ export const preflightLogic = kea([ null as PreflightStatus | null, { loadPreflight: async () => { - const response = (await api.get('_preflight/')) as PreflightStatus + const response = await api.get('_preflight/') return response }, }, diff --git a/frontend/src/scenes/ResourcePermissionModal.tsx b/frontend/src/scenes/ResourcePermissionModal.tsx index 00dd60a034956..4e81f46ccc99b 100644 --- a/frontend/src/scenes/ResourcePermissionModal.tsx +++ b/frontend/src/scenes/ResourcePermissionModal.tsx @@ -1,13 +1,15 @@ import { LemonButton, LemonModal, LemonTable } from '@posthog/lemon-ui' import { useValues } from 'kea' +import { TitleWithIcon } from 'lib/components/TitleWithIcon' import { IconDelete, IconSettings } from 'lib/lemon-ui/icons' import { LemonSelectMultiple, LemonSelectMultipleOptionItem, } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { TitleWithIcon } from 'lib/components/TitleWithIcon' + import { AccessLevel, Resource, RoleType } from '~/types' + import { FormattedResourceLevel, permissionsLogic, diff --git a/frontend/src/scenes/Unsubscribe/Unsubscribe.tsx b/frontend/src/scenes/Unsubscribe/Unsubscribe.tsx index 2b2d2426633f8..b7658b413e04f 100644 --- a/frontend/src/scenes/Unsubscribe/Unsubscribe.tsx +++ b/frontend/src/scenes/Unsubscribe/Unsubscribe.tsx @@ -1,8 +1,9 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { unsubscribeLogic } from './unsubscribeLogic' import { useValues } from 'kea' import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { SceneExport } from 'scenes/sceneTypes' + +import { unsubscribeLogic } from './unsubscribeLogic' export const scene: SceneExport = { component: Unsubscribe, diff --git a/frontend/src/scenes/UpgradeModal.tsx b/frontend/src/scenes/UpgradeModal.tsx index bd10d36595fc1..ea065da4c6a3b 100644 --- a/frontend/src/scenes/UpgradeModal.tsx +++ b/frontend/src/scenes/UpgradeModal.tsx @@ -2,6 +2,7 @@ import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { capitalizeFirstLetter } from 'lib/utils' import { posthog } from 'posthog-js' + import { sceneLogic } from './sceneLogic' import { urls } from './urls' diff --git a/frontend/src/scenes/actions/Action.tsx b/frontend/src/scenes/actions/Action.tsx index e4e7b934b4f3d..61b1ccb3d2f0c 100644 --- a/frontend/src/scenes/actions/Action.tsx +++ b/frontend/src/scenes/actions/Action.tsx @@ -1,14 +1,16 @@ -import { ActionEdit } from './ActionEdit' import { useActions, useValues } from 'kea' import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import { ActionType } from '~/types' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { SceneExport } from 'scenes/sceneTypes' import { actionLogic, ActionLogicProps } from 'scenes/actions/actionLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' +import { ActionType } from '~/types' + +import { ActionEdit } from './ActionEdit' export const scene: SceneExport = { logic: actionLogic, diff --git a/frontend/src/scenes/actions/ActionEdit.tsx b/frontend/src/scenes/actions/ActionEdit.tsx index e7a3182f8ee7d..14f524ee0e958 100644 --- a/frontend/src/scenes/actions/ActionEdit.tsx +++ b/frontend/src/scenes/actions/ActionEdit.tsx @@ -1,25 +1,27 @@ -import { compactNumber, uuid } from 'lib/utils' -import { Link } from 'lib/lemon-ui/Link' +import { LemonTextArea } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { actionEditLogic, ActionEditLogicProps } from './actionEditLogic' -import { ActionStep } from './ActionStep' +import { Form } from 'kea-forms' import { combineUrl, router } from 'kea-router' -import { PageHeader } from 'lib/components/PageHeader' -import { teamLogic } from 'scenes/teamLogic' -import { urls } from 'scenes/urls' import { EditableField } from 'lib/components/EditableField/EditableField' -import { ActionStepType, AvailableFeature } from '~/types' -import { userLogic } from 'scenes/userLogic' 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 { Form } from 'kea-forms' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { IconInfo, IconPlayCircle, IconPlus, IconWarning } from 'lib/lemon-ui/icons' -import { tagsModel } from '~/models/tagsModel' +import { Link } from 'lib/lemon-ui/Link' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonTextArea } from '@posthog/lemon-ui' +import { compactNumber, uuid } from 'lib/utils' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { tagsModel } from '~/models/tagsModel' +import { ActionStepType, AvailableFeature } from '~/types' + +import { actionEditLogic, ActionEditLogicProps } from './actionEditLogic' +import { ActionStep } from './ActionStep' export function ActionEdit({ action: loadedAction, id, onSave, temporaryToken }: ActionEditLogicProps): JSX.Element { const logicProps: ActionEditLogicProps = { diff --git a/frontend/src/scenes/actions/ActionStep.tsx b/frontend/src/scenes/actions/ActionStep.tsx index 9f41316c52ab1..3bab146512df3 100644 --- a/frontend/src/scenes/actions/ActionStep.tsx +++ b/frontend/src/scenes/actions/ActionStep.tsx @@ -1,19 +1,20 @@ -import { LemonEventName } from './EventName' +import './ActionStep.scss' + +import { LemonButton, LemonInput, LemonSegmentedButton, Link } from '@posthog/lemon-ui' +import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' +import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { OperandTag } from 'lib/components/PropertyFilters/components/OperandTag' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { URL_MATCHING_HINTS } from 'scenes/actions/hints' -import { Radio, RadioChangeEvent } from 'antd' -import { ActionStepType, StringMatching } from '~/types' -import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { IconClose, IconOpenInApp } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' -import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { useState } from 'react' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { URL_MATCHING_HINTS } from 'scenes/actions/hints' -import './ActionStep.scss' -import { OperandTag } from 'lib/components/PropertyFilters/components/OperandTag' +import { ActionStepType, StringMatching } from '~/types' + +import { LemonEventName } from './EventName' const learnMoreLink = 'https://posthog.com/docs/user-guides/actions?utm_medium=in-product&utm_campaign=action-page' @@ -166,9 +167,10 @@ function Option({ return (
    - - {label} {extra_options} - +
    + {label} + {extra_options} +
    {caption &&
    {caption}
    } void }): JSX.Element { - const handleChange = (e: RadioChangeEvent): void => { - const type = e.target.value + const handleChange = (type: string): void => { if (type === '$autocapture') { sendStep({ ...step, event: '$autocapture' }) } else if (type === 'event') { @@ -301,19 +302,30 @@ function TypeSwitcher({ return (
    - - Autocapture - Custom event - Page view - + options={[ + { + value: '$autocapture', + label: 'Autocapture', + }, + { + value: 'event', + label: 'Custom event', + }, + { + value: '$pageview', + label: 'Page view', + }, + ]} + fullWidth + size="small" + />
    ) } @@ -328,15 +340,32 @@ function StringMatchingSelection({ sendStep: (stepToSend: ActionStepType) => void }): JSX.Element { const key = `${field}_matching` - const handleURLMatchChange = (e: RadioChangeEvent): void => { - sendStep({ ...step, [key]: e.target.value }) + const handleURLMatchChange = (value: string): void => { + sendStep({ ...step, [key]: value }) } const defaultValue = field === 'url' ? StringMatching.Contains : StringMatching.Exact return ( - - matches exactly - matches regex - contains - +
    + +
    ) } diff --git a/frontend/src/scenes/actions/EventName.tsx b/frontend/src/scenes/actions/EventName.tsx index aadaf08a4cee1..e5ddd0f0e1c6c 100644 --- a/frontend/src/scenes/actions/EventName.tsx +++ b/frontend/src/scenes/actions/EventName.tsx @@ -1,6 +1,6 @@ import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { TaxonomicPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { TaxonomicPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' interface LemonEventNamePropsWithoutAllEvents { value: string @@ -38,6 +38,7 @@ export function LemonEventName({ renderValue={(v) => (v !== null ? : null)} allowClear={allEventsOption === 'clear'} excludedProperties={allEventsOption !== 'explicit' ? { events: [null] } : undefined} + size="small" /> ) } diff --git a/frontend/src/scenes/actions/NewActionButton.tsx b/frontend/src/scenes/actions/NewActionButton.tsx index 4d98c98b77253..4ad666f60f5b8 100644 --- a/frontend/src/scenes/actions/NewActionButton.tsx +++ b/frontend/src/scenes/actions/NewActionButton.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react' +import { LemonModal } from '@posthog/lemon-ui' import { router } from 'kea-router' -import { urls } from 'scenes/urls' import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' import { IconEdit, IconMagnifier } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonModal } from '@posthog/lemon-ui' +import { useState } from 'react' +import { urls } from 'scenes/urls' export function NewActionButton({ onSelectOption }: { onSelectOption?: () => void }): JSX.Element { const [visible, setVisible] = useState(false) diff --git a/frontend/src/scenes/actions/actionEditLogic.tsx b/frontend/src/scenes/actions/actionEditLogic.tsx index 396678d57014a..a32cd4a3a44c4 100644 --- a/frontend/src/scenes/actions/actionEditLogic.tsx +++ b/frontend/src/scenes/actions/actionEditLogic.tsx @@ -1,19 +1,22 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers } from 'kea' -import api from 'lib/api' -import { deleteWithUndo, uuid } from 'lib/utils' -import { actionsModel } from '~/models/actionsModel' -import type { actionEditLogicType } from './actionEditLogicType' -import { ActionStepType, ActionType } from '~/types' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { loaders } from 'kea-loaders' import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' import { beforeUnload, router, urlToAction } from 'kea-router' -import { urls } from 'scenes/urls' -import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' +import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { Link } from 'lib/lemon-ui/Link' -import { tagsModel } from '~/models/tagsModel' +import { uuid } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { actionsModel } from '~/models/actionsModel' +import { tagsModel } from '~/models/tagsModel' +import { ActionStepType, ActionType } from '~/types' + +import type { actionEditLogicType } from './actionEditLogicType' export type NewActionType = Partial & Pick @@ -129,8 +132,8 @@ export const actionEditLogic = kea([ })), listeners(({ values, actions }) => ({ - deleteAction: () => { - deleteWithUndo({ + deleteAction: async () => { + await deleteWithUndo({ endpoint: api.actions.determineDeleteEndpoint(), object: values.action, callback: () => { diff --git a/frontend/src/scenes/actions/actionLogic.ts b/frontend/src/scenes/actions/actionLogic.ts index e3781d37902f0..7aa1934af84ce 100644 --- a/frontend/src/scenes/actions/actionLogic.ts +++ b/frontend/src/scenes/actions/actionLogic.ts @@ -1,10 +1,14 @@ +import { actions, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, actions, reducers, selectors, listeners, events } from 'kea' import api from 'lib/api' -import type { actionLogicType } from './actionLogicType' -import { ActionType, Breadcrumb } from '~/types' +import { DataManagementTab } from 'scenes/data-management/DataManagementScene' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { ActionType, Breadcrumb } from '~/types' + +import type { actionLogicType } from './actionLogicType' + export interface ActionLogicProps { id?: ActionType['id'] } @@ -50,14 +54,17 @@ export const actionLogic = kea([ (s) => [s.action], (action): Breadcrumb[] => [ { + key: Scene.DataManagement, name: `Data Management`, path: urls.eventDefinitions(), }, { + key: DataManagementTab.Actions, name: 'Actions', path: urls.actions(), }, { + key: action?.id || 'new', name: action?.name || 'Unnamed', path: action ? urls.action(action.id) : undefined, }, diff --git a/frontend/src/scenes/actions/actionsLogic.ts b/frontend/src/scenes/actions/actionsLogic.ts index bd4d667ae8450..063ba899d5d38 100644 --- a/frontend/src/scenes/actions/actionsLogic.ts +++ b/frontend/src/scenes/actions/actionsLogic.ts @@ -1,14 +1,19 @@ -import { kea, selectors, path, actions, reducers, connect } from 'kea' -import { ActionType, Breadcrumb, ProductKey } from '~/types' -import { urls } from 'scenes/urls' - -import type { actionsLogicType } from './actionsLogicType' -import { actionsModel } from '~/models/actionsModel' import Fuse from 'fuse.js' -import { userLogic } from 'scenes/userLogic' +import { actions, connect, kea, path, reducers, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { DataManagementTab } from 'scenes/data-management/DataManagementScene' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { actionsModel } from '~/models/actionsModel' +import { ActionType, Breadcrumb, ProductKey } from '~/types' + +import type { actionsLogicType } from './actionsLogicType' + +export type ActionsFilterType = 'all' | 'me' export const actionsFuse = new Fuse([], { keys: [{ name: 'name', weight: 2 }, 'description', 'tags'], @@ -30,15 +35,15 @@ export const actionsLogic = kea([ ], })), actions({ - setFilterByMe: (filterByMe: boolean) => ({ filterByMe }), + setFilterType: (filterType: ActionsFilterType) => ({ filterType }), setSearchTerm: (searchTerm: string) => ({ searchTerm }), }), reducers({ - filterByMe: [ - false, + filterType: [ + 'all' as ActionsFilterType, { persist: true }, { - setFilterByMe: (_, { filterByMe }) => filterByMe, + setFilterType: (_, { filterType }) => filterType, }, ], searchTerm: [ @@ -50,13 +55,13 @@ export const actionsLogic = kea([ }), selectors({ actionsFiltered: [ - (s) => [s.actions, s.filterByMe, s.searchTerm, s.user], - (actions, filterByMe, searchTerm, user) => { + (s) => [s.actions, s.filterType, s.searchTerm, s.user], + (actions, filterType, searchTerm, user) => { let data = actions if (searchTerm) { data = actionsFuse.search(searchTerm).map((result) => result.item) } - if (filterByMe) { + if (filterType === 'me') { data = data.filter((item) => item.created_by?.uuid === user?.uuid) } return data @@ -66,10 +71,12 @@ export const actionsLogic = kea([ () => [], (): Breadcrumb[] => [ { - name: `Data Management`, + key: Scene.DataManagement, + name: `Data management`, path: urls.eventDefinitions(), }, { + key: DataManagementTab.Actions, name: 'Actions', path: urls.actions(), }, diff --git a/frontend/src/scenes/annotations/AnnotationModal.tsx b/frontend/src/scenes/annotations/AnnotationModal.tsx index a697a84456ce0..ad4edf2b00b23 100644 --- a/frontend/src/scenes/annotations/AnnotationModal.tsx +++ b/frontend/src/scenes/annotations/AnnotationModal.tsx @@ -1,14 +1,16 @@ import { LemonButton, LemonModal, LemonModalProps, LemonSelect, LemonTextArea, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import { DatePicker } from 'lib/components/DatePicker' -import { annotationScopeToName, annotationModalLogic, ANNOTATION_DAYJS_FORMAT } from './annotationModalLogic' -import { AnnotationScope } from '~/types' +import { Field } from 'lib/forms/Field' import { IconWarning } from 'lib/lemon-ui/icons' import { shortTimeZone } from 'lib/utils' import { urls } from 'scenes/urls' +import { AnnotationScope } from '~/types' + +import { ANNOTATION_DAYJS_FORMAT, annotationModalLogic, annotationScopeToName } from './annotationModalLogic' + export function NewAnnotationButton(): JSX.Element { const { openModalToCreateAnnotation } = useActions(annotationModalLogic) return ( diff --git a/frontend/src/scenes/annotations/Annotations.stories.tsx b/frontend/src/scenes/annotations/Annotations.stories.tsx index 8a99d5c64f7a5..79550faaba47b 100644 --- a/frontend/src/scenes/annotations/Annotations.stories.tsx +++ b/frontend/src/scenes/annotations/Annotations.stories.tsx @@ -1,9 +1,11 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' -import { App } from 'scenes/App' import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' + import annotations from './__mocks__/annotations.json' const meta: Meta = { diff --git a/frontend/src/scenes/annotations/Annotations.tsx b/frontend/src/scenes/annotations/Annotations.tsx index b71b8969d0899..974de8f172104 100644 --- a/frontend/src/scenes/annotations/Annotations.tsx +++ b/frontend/src/scenes/annotations/Annotations.tsx @@ -1,26 +1,28 @@ -import { useValues, useActions } from 'kea' -import { - annotationScopeToLevel, - annotationScopeToName, - annotationModalLogic, - ANNOTATION_DAYJS_FORMAT, -} from './annotationModalLogic' -import { AnnotationScope, InsightShortId, AnnotationType, ProductKey } from '~/types' -import { LemonTable, LemonTableColumns, LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { MicrophoneHog } from 'lib/components/hedgehogs' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { IconEdit } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { IconEdit } from 'lib/lemon-ui/icons' -import { Link } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { teamLogic } from 'scenes/teamLogic' +import { shortTimeZone } from 'lib/utils' import { organizationLogic } from 'scenes/organizationLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { AnnotationScope, AnnotationType, InsightShortId, ProductKey } from '~/types' + import { AnnotationModal } from './AnnotationModal' -import { shortTimeZone } from 'lib/utils' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' -import { MicrophoneHog } from 'lib/components/hedgehogs' +import { + ANNOTATION_DAYJS_FORMAT, + annotationModalLogic, + annotationScopeToLevel, + annotationScopeToName, +} from './annotationModalLogic' export function Annotations(): JSX.Element { const { currentTeam } = useValues(teamLogic) diff --git a/frontend/src/scenes/annotations/annotationModalLogic.ts b/frontend/src/scenes/annotations/annotationModalLogic.ts index d1dd40c68061e..74b32ef8d58a1 100644 --- a/frontend/src/scenes/annotations/annotationModalLogic.ts +++ b/frontend/src/scenes/annotations/annotationModalLogic.ts @@ -1,16 +1,18 @@ import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' -import { AnnotationScope, AnnotationType, InsightModel, ProductKey } from '~/types' import { forms } from 'kea-forms' -import { dayjs, Dayjs } from 'lib/dayjs' -import { annotationsModel, deserializeAnnotation } from '~/models/annotationsModel' -import type { annotationModalLogicType } from './annotationModalLogicType' -import { teamLogic } from 'scenes/teamLogic' +import { urlToAction } from 'kea-router' +import api from 'lib/api' import { FEATURE_FLAGS } from 'lib/constants' -import { userLogic } from 'scenes/userLogic' +import { Dayjs, dayjs } from 'lib/dayjs' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { urlToAction } from 'kea-router' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { annotationsModel, deserializeAnnotation } from '~/models/annotationsModel' +import { AnnotationScope, AnnotationType, InsightModel, ProductKey } from '~/types' + +import type { annotationModalLogicType } from './annotationModalLogicType' export const ANNOTATION_DAYJS_FORMAT = 'MMMM DD, YYYY h:mm A' diff --git a/frontend/src/scenes/appContextLogic.ts b/frontend/src/scenes/appContextLogic.ts index 30a005f3c3808..ce3a6a11a317c 100644 --- a/frontend/src/scenes/appContextLogic.ts +++ b/frontend/src/scenes/appContextLogic.ts @@ -1,14 +1,14 @@ +import * as Sentry from '@sentry/react' import { afterMount, connect, kea, path } from 'kea' import api from 'lib/api' import { getAppContext } from 'lib/utils/getAppContext' -import * as Sentry from '@sentry/react' -import { userLogic } from './userLogic' +import { UserType } from '~/types' import type { appContextLogicType } from './appContextLogicType' import { organizationLogic } from './organizationLogic' import { teamLogic } from './teamLogic' -import { UserType } from '~/types' +import { userLogic } from './userLogic' export const appContextLogic = kea([ path(['scenes', 'appContextLogic']), @@ -27,7 +27,7 @@ export const appContextLogic = kea([ const preloadedUser = appContext?.current_user if (appContext && preloadedUser) { - api.get('api/users/@me/').then((remoteUser: UserType) => { + void api.get('api/users/@me/').then((remoteUser: UserType) => { if (remoteUser.uuid !== preloadedUser.uuid) { console.error(`Preloaded user ${preloadedUser.uuid} does not match remote user ${remoteUser.uuid}`) Sentry.captureException( diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 38bf3e1dedd7e..8c7a8c5ab8c09 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -1,5 +1,5 @@ -import { Scene } from 'scenes/sceneTypes' import { preloadedScenes } from 'scenes/scenes' +import { Scene } from 'scenes/sceneTypes' export const appScenes: Record any> = { [Scene.Error404]: () => ({ default: preloadedScenes[Scene.Error404].component }), @@ -23,6 +23,7 @@ export const appScenes: Record any> = { [Scene.PersonsManagement]: () => import('./persons-management/PersonsManagementScene'), [Scene.Person]: () => import('./persons/PersonScene'), [Scene.Pipeline]: () => import('./pipeline/Pipeline'), + [Scene.PipelineApp]: () => import('./pipeline/PipelineApp'), [Scene.Group]: () => import('./groups/Group'), [Scene.Action]: () => import('./actions/Action'), [Scene.Experiments]: () => import('./experiments/Experiments'), @@ -52,7 +53,6 @@ export const appScenes: Record any> = { [Scene.PreflightCheck]: () => import('./PreflightCheck/PreflightCheck'), [Scene.Signup]: () => import('./authentication/signup/SignupContainer'), [Scene.InviteSignup]: () => import('./authentication/InviteSignup'), - [Scene.Ingestion]: () => import('./ingestion/IngestionWizard'), [Scene.Billing]: () => import('./billing/Billing'), [Scene.Apps]: () => import('./plugins/AppsScene'), [Scene.FrontendAppScene]: () => import('./apps/FrontendAppScene'), diff --git a/frontend/src/scenes/apps/AppLogsTab.tsx b/frontend/src/scenes/apps/AppLogsTab.tsx index 1bd8a87d36d71..440cf8dc8dcce 100644 --- a/frontend/src/scenes/apps/AppLogsTab.tsx +++ b/frontend/src/scenes/apps/AppLogsTab.tsx @@ -1,8 +1,9 @@ -import { appMetricsSceneLogic } from './appMetricsSceneLogic' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { useValues } from 'kea' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { PluginLogs } from 'scenes/plugins/plugin/PluginLogs' +import { appMetricsSceneLogic } from './appMetricsSceneLogic' + export function AppLogsTab(): JSX.Element { const { activeTab, pluginConfig, pluginConfigLoading } = useValues(appMetricsSceneLogic) diff --git a/frontend/src/scenes/apps/AppMetricsGraph.tsx b/frontend/src/scenes/apps/AppMetricsGraph.tsx index b8bb316a2bf3f..d72280f043770 100644 --- a/frontend/src/scenes/apps/AppMetricsGraph.tsx +++ b/frontend/src/scenes/apps/AppMetricsGraph.tsx @@ -1,12 +1,15 @@ -import { useEffect, useRef } from 'react' -import { getColorVar } from 'lib/colors' +import './AppMetricsGraph.scss' + import { Chart, ChartDataset, ChartItem } from 'lib/Chart' -import { DescriptionColumns } from './constants' +import { getColorVar } from 'lib/colors' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' - -import './AppMetricsGraph.scss' import { inStorybookTestRunner, lightenDarkenColor } from 'lib/utils' -import { AppMetrics, AppMetricsTab } from './appMetricsSceneLogic' +import { useEffect, useRef } from 'react' + +import { AppMetricsTab } from '~/types' + +import { AppMetrics } from './appMetricsSceneLogic' +import { DescriptionColumns } from './constants' export interface AppMetricsGraphProps { tab: AppMetricsTab @@ -31,21 +34,21 @@ export function AppMetricsGraph({ tab, metrics, metricsLoading }: AppMetricsGrap label: descriptions.successes, data: metrics.successes, borderColor: '', - ...colorConfig('data-brand-blue'), + ...colorConfig('data-color-1'), }, ...(descriptions.successes_on_retry ? [ { label: descriptions.successes_on_retry, data: metrics.successes_on_retry, - ...colorConfig('data-yellow'), + ...colorConfig('data-color-13'), }, ] : []), { label: descriptions.failures, data: metrics.failures, - ...colorConfig('data-vermilion'), + ...colorConfig('data-color-5'), }, ], }, diff --git a/frontend/src/scenes/apps/AppMetricsScene.stories.tsx b/frontend/src/scenes/apps/AppMetricsScene.stories.tsx index 638bb317fc462..645f275c49214 100644 --- a/frontend/src/scenes/apps/AppMetricsScene.stories.tsx +++ b/frontend/src/scenes/apps/AppMetricsScene.stories.tsx @@ -1,12 +1,14 @@ import { Meta, Story } from '@storybook/react' -import { App } from 'scenes/App' -import { useEffect } from 'react' import { router } from 'kea-router' -import { mswDecorator } from '~/mocks/browser' -import { AppMetricsResponse } from './appMetricsSceneLogic' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' -import { AvailableFeature } from '~/types' + +import { mswDecorator } from '~/mocks/browser' import { useAvailableFeatures } from '~/mocks/features' +import { AvailableFeature } from '~/types' + +import { AppMetricsResponse } from './appMetricsSceneLogic' const meta: Meta = { title: 'Scenes-App/Apps/App Metrics', diff --git a/frontend/src/scenes/apps/AppMetricsScene.tsx b/frontend/src/scenes/apps/AppMetricsScene.tsx index d79c0d160d51e..e129ff60be3b0 100644 --- a/frontend/src/scenes/apps/AppMetricsScene.tsx +++ b/frontend/src/scenes/apps/AppMetricsScene.tsx @@ -1,20 +1,23 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { appMetricsSceneLogic, AppMetricsTab } from 'scenes/apps/appMetricsSceneLogic' -import { PageHeader } from 'lib/components/PageHeader' -import { useValues, useActions } from 'kea' -import { MetricsTab } from './MetricsTab' -import { HistoricalExportsTab } from './HistoricalExportsTab' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { ErrorDetailsModal } from './ErrorDetailsModal' +import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { PageHeader } from 'lib/components/PageHeader' +import { IconSettings } from 'lib/lemon-ui/icons' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { PluginTags } from 'scenes/plugins/tabs/apps/components' +import { appMetricsSceneLogic } from 'scenes/apps/appMetricsSceneLogic' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' -import { AppLogsTab } from './AppLogsTab' -import { LemonButton } from '@posthog/lemon-ui' -import { IconSettings } from 'lib/lemon-ui/icons' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginTags } from 'scenes/plugins/tabs/apps/components' +import { SceneExport } from 'scenes/sceneTypes' + +import { AppMetricsTab } from '~/types' + +import { AppLogsTab } from './AppLogsTab' +import { ErrorDetailsModal } from './ErrorDetailsModal' +import { HistoricalExportsTab } from './HistoricalExportsTab' +import { MetricsTab } from './MetricsTab' export const scene: SceneExport = { component: AppMetrics, @@ -80,6 +83,11 @@ export function AppMetrics(): JSX.Element { label: <>onEvent metrics, content: , }, + showTab(AppMetricsTab.ComposeWebhook) && { + key: AppMetricsTab.ComposeWebhook, + label: <>composeWebhook metrics, + content: , + }, showTab(AppMetricsTab.ExportEvents) && { key: AppMetricsTab.ExportEvents, label: <>exportEvents metrics, diff --git a/frontend/src/scenes/apps/ErrorDetailsModal.tsx b/frontend/src/scenes/apps/ErrorDetailsModal.tsx index f5cafdedde71d..e4c643a5b422b 100644 --- a/frontend/src/scenes/apps/ErrorDetailsModal.tsx +++ b/frontend/src/scenes/apps/ErrorDetailsModal.tsx @@ -1,13 +1,14 @@ -import { useState } from 'react' import { useActions, useValues } from 'kea' -import { AppMetricErrorDetail, appMetricsSceneLogic } from './appMetricsSceneLogic' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { TZLabel } from 'lib/components/TZLabel' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconChevronLeft, IconChevronRight, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { useState } from 'react' + +import { AppMetricErrorDetail, appMetricsSceneLogic } from './appMetricsSceneLogic' export function ErrorDetailsModal(): JSX.Element { const { errorDetails, errorDetailsModalError, errorDetailsLoading } = useValues(appMetricsSceneLogic) @@ -26,7 +27,7 @@ export function ErrorDetailsModal(): JSX.Element { footer={
    {errorDetailsLoading ? ( - + ) : ( <> diff --git a/frontend/src/scenes/apps/FrontendAppScene.tsx b/frontend/src/scenes/apps/FrontendAppScene.tsx index 84bb63518c32f..bc0f2146fdf6f 100644 --- a/frontend/src/scenes/apps/FrontendAppScene.tsx +++ b/frontend/src/scenes/apps/FrontendAppScene.tsx @@ -1,8 +1,8 @@ -import { SceneExport } from 'scenes/sceneTypes' import { useValues } from 'kea' -import { frontendAppSceneLogic } from 'scenes/apps/frontendAppSceneLogic' import { PageHeader } from 'lib/components/PageHeader' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { frontendAppSceneLogic } from 'scenes/apps/frontendAppSceneLogic' +import { SceneExport } from 'scenes/sceneTypes' export function FrontendAppScene(): JSX.Element { const { Component, appConfig, breadcrumbs } = useValues(frontendAppSceneLogic) diff --git a/frontend/src/scenes/apps/HistoricalExport.tsx b/frontend/src/scenes/apps/HistoricalExport.tsx index e32cadc276244..2a9415e75146f 100644 --- a/frontend/src/scenes/apps/HistoricalExport.tsx +++ b/frontend/src/scenes/apps/HistoricalExport.tsx @@ -1,7 +1,9 @@ import { Card } from 'antd' import { useValues } from 'kea' + +import { AppMetricsTab } from '~/types' + import { AppMetricsGraph } from './AppMetricsGraph' -import { AppMetricsTab } from './appMetricsSceneLogic' import { historicalExportLogic, HistoricalExportLogicProps } from './historicalExportLogic' import { ErrorsOverview, MetricsOverview } from './MetricsTab' diff --git a/frontend/src/scenes/apps/HistoricalExportsTab.tsx b/frontend/src/scenes/apps/HistoricalExportsTab.tsx index ed94dcbeb1433..974337a54cb48 100644 --- a/frontend/src/scenes/apps/HistoricalExportsTab.tsx +++ b/frontend/src/scenes/apps/HistoricalExportsTab.tsx @@ -1,15 +1,16 @@ +import { Progress } from 'antd' import { useActions, useValues } from 'kea' -import { appMetricsSceneLogic, HistoricalExportInfo } from './appMetricsSceneLogic' +import { LemonButton } from 'lib/lemon-ui/LemonButton/LemonButton' import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { HistoricalExport } from './HistoricalExport' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { Progress } from 'antd' -import { PluginJobModal } from 'scenes/plugins/edit/interface-jobs/PluginJobConfiguration' import { useEffect } from 'react' -import { LemonButton } from 'lib/lemon-ui/LemonButton/LemonButton' +import { PluginJobModal } from 'scenes/plugins/edit/interface-jobs/PluginJobConfiguration' import { userLogic } from 'scenes/userLogic' +import { appMetricsSceneLogic, HistoricalExportInfo } from './appMetricsSceneLogic' +import { HistoricalExport } from './HistoricalExport' + const RELOAD_HISTORICAL_EXPORTS_FREQUENCY_MS = 20000 export function HistoricalExportsTab(): JSX.Element { diff --git a/frontend/src/scenes/apps/MetricsTab.tsx b/frontend/src/scenes/apps/MetricsTab.tsx index 89aed5b90fa45..635039f37377a 100644 --- a/frontend/src/scenes/apps/MetricsTab.tsx +++ b/frontend/src/scenes/apps/MetricsTab.tsx @@ -1,15 +1,18 @@ -import { AppErrorSummary, AppMetrics, appMetricsSceneLogic, AppMetricsTab } from './appMetricsSceneLogic' -import { DescriptionColumns } from './constants' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { humanFriendlyDuration, humanFriendlyNumber } from 'lib/utils' -import { AppMetricsGraph } from './AppMetricsGraph' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { useActions, useValues } from 'kea' -import { LemonTable } from 'lib/lemon-ui/LemonTable' import { TZLabel } from 'lib/components/TZLabel' +import { IconInfo } from 'lib/lemon-ui/icons' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { LemonTable } from 'lib/lemon-ui/LemonTable' import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconInfo } from 'lib/lemon-ui/icons' +import { humanFriendlyDuration, humanFriendlyNumber } from 'lib/utils' + +import { AppMetricsTab } from '~/types' + +import { AppMetricsGraph } from './AppMetricsGraph' +import { AppErrorSummary, AppMetrics, appMetricsSceneLogic } from './appMetricsSceneLogic' +import { DescriptionColumns } from './constants' export interface MetricsTabProps { tab: AppMetricsTab @@ -39,7 +42,7 @@ export function MetricsTab({ tab }: MetricsTabProps): JSX.Element { setDateFrom(newValue as string)} + onChange={(newValue) => setDateFrom(newValue)} options={[ { label: 'Last 30 days', value: '-30d' }, { label: 'Last 7 days', value: '-7d' }, @@ -78,7 +81,7 @@ export function MetricsOverview({ exportFailureReason, }: MetricsOverviewProps): JSX.Element { if (metricsLoading) { - return + return } return ( diff --git a/frontend/src/scenes/apps/appMetricsSceneLogic.ts b/frontend/src/scenes/apps/appMetricsSceneLogic.ts index 9122d93b48db0..ca4b4239691c5 100644 --- a/frontend/src/scenes/apps/appMetricsSceneLogic.ts +++ b/frontend/src/scenes/apps/appMetricsSceneLogic.ts @@ -1,41 +1,29 @@ -import { kea, key, props, path, actions, selectors, reducers, listeners } from 'kea' +import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' - -import type { appMetricsSceneLogicType } from './appMetricsSceneLogicType' -import { urls } from 'scenes/urls' -import { Breadcrumb, PluginConfigWithPluginInfo, UserBasicType } from '~/types' -import api, { PaginatedResponse } from 'lib/api' -import { teamLogic } from 'scenes/teamLogic' import { actionToUrl, urlToAction } from 'kea-router' +import { router } from 'kea-router' +import api, { PaginatedResponse } from 'lib/api' +import { dayjs } from 'lib/dayjs' import { toParams } from 'lib/utils' import { HISTORICAL_EXPORT_JOB_NAME_V2 } from 'scenes/plugins/edit/interface-jobs/PluginJobConfiguration' +import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { AppMetricsTab, AppMetricsUrlParams, Breadcrumb, PluginConfigWithPluginInfo, UserBasicType } from '~/types' + import { interfaceJobsLogic, InterfaceJobsProps } from '../plugins/edit/interface-jobs/interfaceJobsLogic' -import { dayjs } from 'lib/dayjs' -import { router } from 'kea-router' +import type { appMetricsSceneLogicType } from './appMetricsSceneLogicType' export interface AppMetricsLogicProps { /** Used as the logic's key */ pluginConfigId: number } -export interface AppMetricsUrlParams { - tab?: AppMetricsTab - from?: string - error?: [string, string] -} - -export enum AppMetricsTab { - Logs = 'logs', - ProcessEvent = 'processEvent', - OnEvent = 'onEvent', - ExportEvents = 'exportEvents', - ScheduledTask = 'scheduledTask', - HistoricalExports = 'historical_exports', - History = 'history', -} export const TabsWithMetrics = [ AppMetricsTab.ProcessEvent, AppMetricsTab.OnEvent, + AppMetricsTab.ComposeWebhook, AppMetricsTab.ExportEvents, AppMetricsTab.ScheduledTask, AppMetricsTab.HistoricalExports, @@ -95,6 +83,7 @@ const DEFAULT_DATE_FROM = '-30d' const INITIAL_TABS: Array = [ AppMetricsTab.ProcessEvent, AppMetricsTab.OnEvent, + AppMetricsTab.ComposeWebhook, AppMetricsTab.ExportEvents, AppMetricsTab.ScheduledTask, ] @@ -194,10 +183,12 @@ export const appMetricsSceneLogic = kea([ (s, p) => [s.pluginConfig, p.pluginConfigId], (pluginConfig, pluginConfigId: number): Breadcrumb[] => [ { + key: Scene.Apps, name: 'Apps', path: urls.projectApps(), }, { + key: pluginConfigId, name: pluginConfig?.plugin_info?.name, path: urls.appMetrics(pluginConfigId), }, diff --git a/frontend/src/scenes/apps/constants.tsx b/frontend/src/scenes/apps/constants.tsx index 4d89a8202eae1..fb5077b3df336 100644 --- a/frontend/src/scenes/apps/constants.tsx +++ b/frontend/src/scenes/apps/constants.tsx @@ -1,4 +1,4 @@ -import { AppMetricsTab } from './appMetricsSceneLogic' +import { AppMetricsTab } from '~/types' interface Description { successes: string @@ -47,6 +47,26 @@ export const DescriptionColumns: Record = { ), }, + [AppMetricsTab.ComposeWebhook]: { + successes: 'Events processed', + successes_tooltip: ( + <> + These events were successfully processed by the composeWebhook app method on the first try. + + ), + successes_on_retry: 'Events processed on retry', + successes_on_retry_tooltip: ( + <> + These events were successfully processed by the composeWebhook app method after being retried. + + ), + failures: 'Failed events', + failures_tooltip: ( + <> + These events had errors when being processed by the composeWebhook app method. + + ), + }, [AppMetricsTab.ExportEvents]: { successes: 'Events delivered', successes_tooltip: ( diff --git a/frontend/src/scenes/apps/frontendAppRequire.ts b/frontend/src/scenes/apps/frontendAppRequire.ts index f0df491e00ee4..2714d4e0c20c0 100644 --- a/frontend/src/scenes/apps/frontendAppRequire.ts +++ b/frontend/src/scenes/apps/frontendAppRequire.ts @@ -1,11 +1,11 @@ +import * as appsCommon from '@posthog/apps-common' +import * as lemonUi from '@posthog/lemon-ui' import * as allKea from 'kea' -import * as allKeaRouter from 'kea-router' -import * as allKeaLoaders from 'kea-loaders' import * as allKeaForms from 'kea-forms' -import * as allKeaWindowValues from 'kea-window-values' +import * as allKeaLoaders from 'kea-loaders' +import * as allKeaRouter from 'kea-router' import * as allKeaSubscriptions from 'kea-subscriptions' -import * as appsCommon from '@posthog/apps-common' -import * as lemonUi from '@posthog/lemon-ui' +import * as allKeaWindowValues from 'kea-window-values' import React from 'react' const packages = { diff --git a/frontend/src/scenes/apps/frontendAppSceneLogic.ts b/frontend/src/scenes/apps/frontendAppSceneLogic.ts index a165a79be9482..d60075bb66c84 100644 --- a/frontend/src/scenes/apps/frontendAppSceneLogic.ts +++ b/frontend/src/scenes/apps/frontendAppSceneLogic.ts @@ -1,9 +1,11 @@ -import { BuiltLogic, connect, kea, key, LogicWrapper, props, selectors, path } from 'kea' +import { BuiltLogic, connect, kea, key, LogicWrapper, path, props, selectors } from 'kea' +import { subscriptions } from 'kea-subscriptions' +import { objectsEqual } from 'lib/utils' import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' + import { Breadcrumb, FrontendApp, FrontendAppConfig } from '~/types' + import type { frontendAppSceneLogicType } from './frontendAppSceneLogicType' -import { subscriptions } from 'kea-subscriptions' -import { objectsEqual } from 'lib/utils' export interface FrontendAppSceneLogicProps { /** Used as the logic's key */ @@ -15,9 +17,9 @@ export const frontendAppSceneLogic = kea([ path(['scenes', 'apps', 'frontendAppSceneLogic']), props({} as FrontendAppSceneLogicProps), key((props) => props.id), - connect({ + connect(() => ({ values: [frontendAppsLogic, ['frontendApps', 'appConfigs']], - }), + })), selectors(() => ({ // Frontend app created after receiving a bundle via import('').getFrontendApp() frontendApp: [ diff --git a/frontend/src/scenes/apps/frontendAppsLogic.tsx b/frontend/src/scenes/apps/frontendAppsLogic.tsx index 1405c620eed87..a8e24d32a3dfd 100644 --- a/frontend/src/scenes/apps/frontendAppsLogic.tsx +++ b/frontend/src/scenes/apps/frontendAppsLogic.tsx @@ -1,13 +1,15 @@ import { actions, afterMount, connect, defaults, kea, path, reducers } from 'kea' -import type { frontendAppsLogicType } from './frontendAppsLogicType' -import { getAppContext } from 'lib/utils/getAppContext' import { loaders } from 'kea-loaders' -import { FrontendApp, FrontendAppConfig } from '~/types' -import { frontendAppRequire } from './frontendAppRequire' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { getAppContext } from 'lib/utils/getAppContext' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { urls } from 'scenes/urls' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + +import { FrontendApp, FrontendAppConfig } from '~/types' + +import { frontendAppRequire } from './frontendAppRequire' +import type { frontendAppsLogicType } from './frontendAppsLogicType' /** Manages the loading and lifecycle of frontend apps. */ export const frontendAppsLogic = kea([ diff --git a/frontend/src/scenes/apps/historicalExportLogic.ts b/frontend/src/scenes/apps/historicalExportLogic.ts index 80fdfe5735f82..710a90823f067 100644 --- a/frontend/src/scenes/apps/historicalExportLogic.ts +++ b/frontend/src/scenes/apps/historicalExportLogic.ts @@ -1,9 +1,9 @@ -import { kea, events, key, props, path } from 'kea' +import { events, kea, key, path, props } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' + import { teamLogic } from '../teamLogic' import { AppErrorSummary, AppMetrics, HistoricalExportInfo } from './appMetricsSceneLogic' - import type { historicalExportLogicType } from './historicalExportLogicType' export interface HistoricalExportLogicProps { diff --git a/frontend/src/scenes/authentication/InviteSignup.stories.tsx b/frontend/src/scenes/authentication/InviteSignup.stories.tsx index 2f138aefa6c66..6cc336caf2d18 100644 --- a/frontend/src/scenes/authentication/InviteSignup.stories.tsx +++ b/frontend/src/scenes/authentication/InviteSignup.stories.tsx @@ -1,8 +1,10 @@ // Signup.stories.tsx import { Meta } from '@storybook/react' import { useEffect } from 'react' + import { mswDecorator, useStorybookMocks } from '~/mocks/browser' import preflightJson from '~/mocks/fixtures/_preflight.json' + import { InviteSignup } from './InviteSignup' import { inviteSignupLogic } from './inviteSignupLogic' diff --git a/frontend/src/scenes/authentication/InviteSignup.tsx b/frontend/src/scenes/authentication/InviteSignup.tsx index 3c4c780eb3f6e..00705231015a4 100644 --- a/frontend/src/scenes/authentication/InviteSignup.tsx +++ b/frontend/src/scenes/authentication/InviteSignup.tsx @@ -1,22 +1,24 @@ +import { LemonButton, LemonCheckbox, LemonDivider, LemonInput } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { inviteSignupLogic, ErrorCodes } from './inviteSignupLogic' -import { userLogic } from 'scenes/userLogic' -import { PrevalidatedInvite } from '~/types' -import { Link } from 'lib/lemon-ui/Link' +import { Form } from 'kea-forms' +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 { urls } from 'scenes/urls' -import { SceneExport } from 'scenes/sceneTypes' +import { Field, PureField } from 'lib/forms/Field' +import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons' -import { LemonButton, LemonCheckbox, LemonDivider, LemonInput } from '@posthog/lemon-ui' -import { Form } from 'kea-forms' -import { Field, PureField } from 'lib/forms/Field' -import PasswordStrength from 'lib/components/PasswordStrength' -import clsx from 'clsx' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import SignupRoleSelect from 'lib/components/SignupRoleSelect' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { PrevalidatedInvite } from '~/types' + +import { ErrorCodes, inviteSignupLogic } from './inviteSignupLogic' import { SupportModalButton } from './SupportModalButton' export const scene: SceneExport = { diff --git a/frontend/src/scenes/authentication/Login.stories.tsx b/frontend/src/scenes/authentication/Login.stories.tsx index f6ffca2742686..8c92d465c6143 100644 --- a/frontend/src/scenes/authentication/Login.stories.tsx +++ b/frontend/src/scenes/authentication/Login.stories.tsx @@ -1,13 +1,15 @@ // Login.stories.tsx import { Meta, StoryFn } from '@storybook/react' -import { Login } from './Login' -import { mswDecorator, useStorybookMocks } from '~/mocks/browser' -import { useEffect } from 'react' -import preflightJson from '../../mocks/fixtures/_preflight.json' import { router } from 'kea-router' +import { useEffect } from 'react' import { urls } from 'scenes/urls' -import { loginLogic } from './loginLogic' + +import { mswDecorator, useStorybookMocks } from '~/mocks/browser' + +import preflightJson from '../../mocks/fixtures/_preflight.json' +import { Login } from './Login' import { Login2FA } from './Login2FA' +import { loginLogic } from './loginLogic' const meta: Meta = { title: 'Scenes-Other/Login', diff --git a/frontend/src/scenes/authentication/Login.tsx b/frontend/src/scenes/authentication/Login.tsx index e7fbf6d8c7f74..6e3045f29b51b 100644 --- a/frontend/src/scenes/authentication/Login.tsx +++ b/frontend/src/scenes/authentication/Login.tsx @@ -1,23 +1,24 @@ -import { useEffect, useRef } from 'react' import './Login.scss' -import { useActions, useValues } from 'kea' -import { loginLogic } from './loginLogic' -import { Link } from 'lib/lemon-ui/Link' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { SocialLoginButtons, SSOEnforcedLoginButton } from 'lib/components/SocialLoginButton/SocialLoginButton' -import clsx from 'clsx' -import { SceneExport } from 'scenes/sceneTypes' + import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' +import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import { SocialLoginButtons, SSOEnforcedLoginButton } from 'lib/components/SocialLoginButton/SocialLoginButton' import { Field } from 'lib/forms/Field' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import RegionSelect from './RegionSelect' +import { Link } from 'lib/lemon-ui/Link' +import { useEffect, useRef } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { loginLogic } from './loginLogic' import { redirectIfLoggedInOtherInstance } from './redirectToLoggedInInstance' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { captureException } from '@sentry/react' +import RegionSelect from './RegionSelect' import { SupportModalButton } from './SupportModalButton' +import { useButtonStyle } from './useButtonStyles' export const ERROR_MESSAGES: Record = { no_new_organizations: @@ -54,19 +55,18 @@ export function Login(): JSX.Element { const { precheck } = useActions(loginLogic) const { precheckResponse, precheckResponseLoading, login, isLoginSubmitting, generalError } = useValues(loginLogic) const { preflight } = useValues(preflightLogic) - const { featureFlags } = useValues(featureFlagLogic) const passwordInputRef = useRef(null) const isPasswordHidden = precheckResponse.status === 'pending' || precheckResponse.sso_enforcement + const buttonStyles = useButtonStyle() useEffect(() => { - try { - // Turn on E2E test when this flag is removed - if (featureFlags[FEATURE_FLAGS.AUTO_REDIRECT]) { + if (preflight?.cloud) { + try { redirectIfLoggedInOtherInstance() + } catch (e) { + captureException(e) } - } catch (e) { - captureException(e) } }, []) @@ -150,6 +150,7 @@ export function Login(): JSX.Element { type="primary" center loading={isLoginSubmitting || precheckResponseLoading} + {...buttonStyles} > Log in diff --git a/frontend/src/scenes/authentication/Login2FA.tsx b/frontend/src/scenes/authentication/Login2FA.tsx index 6bd87ca93a34b..c1bb53c1e59d4 100644 --- a/frontend/src/scenes/authentication/Login2FA.tsx +++ b/frontend/src/scenes/authentication/Login2FA.tsx @@ -1,15 +1,19 @@ +import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { login2FALogic } from './login2FALogic' import { Form } from 'kea-forms' +import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { Field } from 'lib/forms/Field' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonButton, LemonInput } from '@posthog/lemon-ui' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { login2FALogic } from './login2FALogic' +import { useButtonStyle } from './useButtonStyles' export function Login2FA(): JSX.Element { const { isTwofactortokenSubmitting, generalError } = useValues(login2FALogic) const { preflight } = useValues(preflightLogic) + const buttonStyles = useButtonStyle() + return ( Login diff --git a/frontend/src/scenes/authentication/PasswordReset.stories.tsx b/frontend/src/scenes/authentication/PasswordReset.stories.tsx index 6837428f76278..4b3c6b4cdd399 100644 --- a/frontend/src/scenes/authentication/PasswordReset.stories.tsx +++ b/frontend/src/scenes/authentication/PasswordReset.stories.tsx @@ -1,10 +1,12 @@ // PasswordReset.stories.tsx import { Meta } from '@storybook/react' -import { PasswordReset } from './PasswordReset' import { useEffect } from 'react' +import { passwordResetLogic } from 'scenes/authentication/passwordResetLogic' + import { useStorybookMocks } from '~/mocks/browser' import preflightJson from '~/mocks/fixtures/_preflight.json' -import { passwordResetLogic } from 'scenes/authentication/passwordResetLogic' + +import { PasswordReset } from './PasswordReset' // some metadata and optional parameters const meta: Meta = { diff --git a/frontend/src/scenes/authentication/PasswordReset.tsx b/frontend/src/scenes/authentication/PasswordReset.tsx index d1c2b3a037314..a3a7590cd48c9 100644 --- a/frontend/src/scenes/authentication/PasswordReset.tsx +++ b/frontend/src/scenes/authentication/PasswordReset.tsx @@ -1,19 +1,21 @@ /* Scene to request a password reset email. */ -import { useActions, useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { passwordResetLogic } from './passwordResetLogic' -import { router } from 'kea-router' -import { SceneExport } from 'scenes/sceneTypes' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' +import { router } from 'kea-router' import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { Field } from 'lib/forms/Field' import { IconCheckCircleOutline, IconErrorOutline } from 'lib/lemon-ui/icons' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { passwordResetLogic } from './passwordResetLogic' import { SupportModalButton } from './SupportModalButton' +import { useButtonStyle } from './useButtonStyles' export const scene: SceneExport = { component: PasswordReset, @@ -85,6 +87,7 @@ function EmailUnavailable(): JSX.Element { function ResetForm(): JSX.Element { const { isRequestPasswordResetSubmitting } = useValues(passwordResetLogic) + const buttonStyles = useButtonStyle() return (
    @@ -108,6 +111,7 @@ function ResetForm(): JSX.Element { htmlType="submit" data-attr="password-reset" loading={isRequestPasswordResetSubmitting} + {...buttonStyles} > Continue @@ -118,13 +122,21 @@ function ResetForm(): JSX.Element { function ResetSuccess(): JSX.Element { const { requestPasswordReset } = useValues(passwordResetLogic) const { push } = useActions(router) + const buttonStyles = useButtonStyle() return (
    Request received successfully! If the email {requestPasswordReset?.email || 'you typed'} exists, you’ll receive an email with a reset link soon.
    - push('/login')}> + push('/login')} + {...buttonStyles} + > Back to login
    @@ -135,6 +147,7 @@ function ResetSuccess(): JSX.Element { function ResetThrottled(): JSX.Element { const { requestPasswordReset } = useValues(passwordResetLogic) const { push } = useActions(router) + const buttonStyles = useButtonStyle() return (
    @@ -145,7 +158,14 @@ function ResetThrottled(): JSX.Element { {' '} if you think this has been a mistake.
    - push('/login')}> + push('/login')} + {...buttonStyles} + > Back to login
    diff --git a/frontend/src/scenes/authentication/PasswordResetComplete.stories.tsx b/frontend/src/scenes/authentication/PasswordResetComplete.stories.tsx index d6c551b553594..7387cc6a3b7b0 100644 --- a/frontend/src/scenes/authentication/PasswordResetComplete.stories.tsx +++ b/frontend/src/scenes/authentication/PasswordResetComplete.stories.tsx @@ -1,11 +1,13 @@ // PasswordResetComplete.stories.tsx import { Meta } from '@storybook/react' -import { PasswordResetComplete } from './PasswordResetComplete' -import { useEffect } from 'react' import { router } from 'kea-router' +import { useEffect } from 'react' import { urls } from 'scenes/urls' + import { useStorybookMocks } from '~/mocks/browser' +import { PasswordResetComplete } from './PasswordResetComplete' + // some metadata and optional parameters const meta: Meta = { title: 'Scenes-Other/Password Reset Complete', diff --git a/frontend/src/scenes/authentication/PasswordResetComplete.tsx b/frontend/src/scenes/authentication/PasswordResetComplete.tsx index 27a32962d2961..49e1a4e93742d 100644 --- a/frontend/src/scenes/authentication/PasswordResetComplete.tsx +++ b/frontend/src/scenes/authentication/PasswordResetComplete.tsx @@ -1,17 +1,18 @@ /* Scene to enter a new password from a received reset link */ -import { useValues } from 'kea' -import { passwordResetLogic } from './passwordResetLogic' -import { SceneExport } from 'scenes/sceneTypes' -import { Field } from 'lib/forms/Field' import { LemonButton, LemonInput } from '@posthog/lemon-ui' -import PasswordStrength from 'lib/components/PasswordStrength' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { useValues } from 'kea' import { Form } from 'kea-forms' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import PasswordStrength from 'lib/components/PasswordStrength' +import { Field } from 'lib/forms/Field' import { IconErrorOutline } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { SceneExport } from 'scenes/sceneTypes' + +import { passwordResetLogic } from './passwordResetLogic' export const scene: SceneExport = { component: PasswordResetComplete, diff --git a/frontend/src/scenes/authentication/RegionSelect.tsx b/frontend/src/scenes/authentication/RegionSelect.tsx index c380579f55d7c..161ced67a365d 100644 --- a/frontend/src/scenes/authentication/RegionSelect.tsx +++ b/frontend/src/scenes/authentication/RegionSelect.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react' -import { useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { LemonModal, LemonSelect } from '@posthog/lemon-ui' -import { Region } from '~/types' -import { CLOUD_HOSTNAMES, FEATURE_FLAGS } from 'lib/constants' +import { useValues } from 'kea' import { router } from 'kea-router' - +import { CLOUD_HOSTNAMES, FEATURE_FLAGS } from 'lib/constants' import { PureField } from 'lib/forms/Field' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { IconCheckmark } from 'lib/lemon-ui/icons' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useState } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { Region } from '~/types' const sections = [ { diff --git a/frontend/src/scenes/authentication/Setup2FA.tsx b/frontend/src/scenes/authentication/Setup2FA.tsx index 8c9360d8a01ee..dcc3113d18e44 100644 --- a/frontend/src/scenes/authentication/Setup2FA.tsx +++ b/frontend/src/scenes/authentication/Setup2FA.tsx @@ -1,10 +1,12 @@ -import { setup2FALogic } from './setup2FALogic' +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 { useValues } from 'kea' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import './Setup2FA.scss' + +import { setup2FALogic } from './setup2FALogic' export function Setup2FA({ onSuccess }: { onSuccess: () => void }): JSX.Element | null { const { startSetupLoading, generalError } = useValues(setup2FALogic({ onSuccess })) diff --git a/frontend/src/scenes/authentication/SupportModalButton.tsx b/frontend/src/scenes/authentication/SupportModalButton.tsx index 557b894d609f3..1beaed9a86e02 100644 --- a/frontend/src/scenes/authentication/SupportModalButton.tsx +++ b/frontend/src/scenes/authentication/SupportModalButton.tsx @@ -1,7 +1,7 @@ import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { SupportModal } from 'lib/components/Support/SupportModal' import { supportLogic } from 'lib/components/Support/supportLogic' +import { SupportModal } from 'lib/components/Support/SupportModal' import { IconBugShield } from 'lib/lemon-ui/icons' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' diff --git a/frontend/src/scenes/authentication/WelcomeLogo.tsx b/frontend/src/scenes/authentication/WelcomeLogo.tsx index ac96c6f39989a..74f86005ab386 100644 --- a/frontend/src/scenes/authentication/WelcomeLogo.tsx +++ b/frontend/src/scenes/authentication/WelcomeLogo.tsx @@ -1,9 +1,9 @@ +import { Link } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import defaultLogo from 'public/posthog-logo.svg' import cloudLogo from 'public/posthog-logo-cloud.svg' import demoLogo from 'public/posthog-logo-demo.svg' -import defaultLogo from 'public/posthog-logo.svg' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { useValues } from 'kea' -import { Link } from '@posthog/lemon-ui' export function WelcomeLogo({ view }: { view?: string }): JSX.Element { const UTM_TAGS = `utm_campaign=in-product&utm_tag=${view || 'welcome'}-header` diff --git a/frontend/src/scenes/authentication/inviteSignupLogic.ts b/frontend/src/scenes/authentication/inviteSignupLogic.ts index 7632e74ba1373..4950cac2750ca 100644 --- a/frontend/src/scenes/authentication/inviteSignupLogic.ts +++ b/frontend/src/scenes/authentication/inviteSignupLogic.ts @@ -1,9 +1,11 @@ -import { kea, path, actions, reducers, listeners } from 'kea' +import { actions, kea, listeners, path, reducers } from 'kea' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { urlToAction } from 'kea-router' -import { forms } from 'kea-forms' import api from 'lib/api' + import { PrevalidatedInvite } from '~/types' + import type { inviteSignupLogicType } from './inviteSignupLogicType' export enum ErrorCodes { @@ -88,7 +90,7 @@ export const inviteSignupLogic = kea([ first_name: !first_name ? 'Please enter your name' : undefined, }), submit: async (payload, breakpoint) => { - await breakpoint() + breakpoint() if (!values.invite) { return diff --git a/frontend/src/scenes/authentication/login2FALogic.ts b/frontend/src/scenes/authentication/login2FALogic.ts index ed76c57f79e76..4da5deb50adbe 100644 --- a/frontend/src/scenes/authentication/login2FALogic.ts +++ b/frontend/src/scenes/authentication/login2FALogic.ts @@ -1,12 +1,13 @@ -import { kea, path, connect, listeners, actions, reducers } from 'kea' +import { actions, connect, kea, listeners, path, reducers } from 'kea' import { forms } from 'kea-forms' import api from 'lib/api' -import { SSOProvider } from '~/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { handleLoginRedirect } from './loginLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { SSOProvider } from '~/types' import type { login2FALogicType } from './login2FALogicType' +import { handleLoginRedirect } from './loginLogic' export interface AuthenticateResponseType { success: boolean @@ -66,7 +67,7 @@ export const login2FALogic = kea([ : null, }), submit: async ({ token }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/login/token', { token }) } catch (e) { diff --git a/frontend/src/scenes/authentication/loginLogic.test.ts b/frontend/src/scenes/authentication/loginLogic.test.ts index 063c22f5df8cf..de694690d4298 100644 --- a/frontend/src/scenes/authentication/loginLogic.test.ts +++ b/frontend/src/scenes/authentication/loginLogic.test.ts @@ -1,8 +1,9 @@ -import { handleLoginRedirect, loginLogic } from 'scenes/authentication/loginLogic' -import { initKeaTests } from '~/test/init' import { router } from 'kea-router' -import { initKea } from '~/initKea' import { testUtilsPlugin } from 'kea-test-utils' +import { handleLoginRedirect, loginLogic } from 'scenes/authentication/loginLogic' + +import { initKea } from '~/initKea' +import { initKeaTests } from '~/test/init' describe('loginLogic', () => { describe('parseLoginRedirectURL', () => { diff --git a/frontend/src/scenes/authentication/loginLogic.ts b/frontend/src/scenes/authentication/loginLogic.ts index 1b574472e2afb..419f04e3c91d6 100644 --- a/frontend/src/scenes/authentication/loginLogic.ts +++ b/frontend/src/scenes/authentication/loginLogic.ts @@ -1,16 +1,18 @@ -import { kea, path, connect, listeners, actions, reducers } from 'kea' +import { actions, connect, kea, listeners, path, reducers } from 'kea' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { urlToAction } from 'kea-router' -import { forms } from 'kea-forms' -import api from 'lib/api' -import type { loginLogicType } from './loginLogicType' import { router } from 'kea-router' -import { SSOProvider } from '~/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import api from 'lib/api' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { urls } from 'scenes/urls' +import { SSOProvider } from '~/types' + +import type { loginLogicType } from './loginLogicType' + export interface AuthenticateResponseType { success: boolean errorCode?: string @@ -82,7 +84,7 @@ export const loginLogic = kea([ return { status: 'pending' } } - await breakpoint() + breakpoint() const response = await api.create('api/login/precheck', { email }) return { status: 'completed', ...response } }, @@ -101,7 +103,7 @@ export const loginLogic = kea([ : undefined, }), submit: async ({ email, password }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/login', { email, password }) } catch (e) { diff --git a/frontend/src/scenes/authentication/passwordResetLogic.ts b/frontend/src/scenes/authentication/passwordResetLogic.ts index b9df684b9cfa9..487510514e474 100644 --- a/frontend/src/scenes/authentication/passwordResetLogic.ts +++ b/frontend/src/scenes/authentication/passwordResetLogic.ts @@ -1,9 +1,10 @@ import { kea, path, reducers } from 'kea' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { urlToAction } from 'kea-router' -import { forms } from 'kea-forms' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' + import type { passwordResetLogicType } from './passwordResetLogicType' export interface ResponseType { @@ -63,7 +64,7 @@ export const passwordResetLogic = kea([ email: !email ? 'Please enter your email to continue' : undefined, }), submit: async ({ email }, breakpoint) => { - await breakpoint() + breakpoint() try { await api.create('api/reset/', { email }) diff --git a/frontend/src/scenes/authentication/redirectToLoggedInInstance.ts b/frontend/src/scenes/authentication/redirectToLoggedInInstance.ts index 479a9a50f4b78..b5e5bf852f8ec 100644 --- a/frontend/src/scenes/authentication/redirectToLoggedInInstance.ts +++ b/frontend/src/scenes/authentication/redirectToLoggedInInstance.ts @@ -22,8 +22,8 @@ */ import { lemonToast } from '@posthog/lemon-ui' -import { getCookie } from 'lib/api' import { captureException } from '@sentry/react' +import { getCookie } from 'lib/api' // cookie values const PH_CURRENT_INSTANCE = 'ph_current_instance' diff --git a/frontend/src/scenes/authentication/setup2FALogic.ts b/frontend/src/scenes/authentication/setup2FALogic.ts index 22d6acb5b0ec5..7fe843a95fdaf 100644 --- a/frontend/src/scenes/authentication/setup2FALogic.ts +++ b/frontend/src/scenes/authentication/setup2FALogic.ts @@ -1,10 +1,10 @@ -import { kea, path, connect, afterMount, listeners, actions, reducers, props } from 'kea' -import { loaders } from 'kea-loaders' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, afterMount, connect, kea, listeners, path, props, reducers } from 'kea' import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' import api from 'lib/api' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { lemonToast } from '@posthog/lemon-ui' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import type { setup2FALogicType } from './setup2FALogicType' @@ -42,7 +42,7 @@ export const setup2FALogic = kea([ {}, { setup: async (_, breakpoint) => { - await breakpoint() + breakpoint() await api.get('api/users/@me/start_2fa_setup/') return { status: 'completed' } }, @@ -56,7 +56,7 @@ export const setup2FALogic = kea([ token: !token ? 'Please enter a token to continue' : undefined, }), submit: async ({ token }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/users/@me/validate_2fa/', { token }) } catch (e) { diff --git a/frontend/src/scenes/authentication/signup/Signup.stories.tsx b/frontend/src/scenes/authentication/signup/Signup.stories.tsx index 0ebdd83f170fd..7fe843e9433c2 100644 --- a/frontend/src/scenes/authentication/signup/Signup.stories.tsx +++ b/frontend/src/scenes/authentication/signup/Signup.stories.tsx @@ -1,9 +1,11 @@ // Signup.stories.tsx import { Meta } from '@storybook/react' import { useEffect } from 'react' -import { mswDecorator, useStorybookMocks } from '~/mocks/browser' import { userLogic } from 'scenes/userLogic' + +import { mswDecorator, useStorybookMocks } from '~/mocks/browser' import preflightJson from '~/mocks/fixtures/_preflight.json' + import { SignupContainer } from './SignupContainer' const meta: Meta = { diff --git a/frontend/src/scenes/authentication/signup/SignupContainer.tsx b/frontend/src/scenes/authentication/signup/SignupContainer.tsx index 6dce9f08abe6d..1fea3ff456e73 100644 --- a/frontend/src/scenes/authentication/signup/SignupContainer.tsx +++ b/frontend/src/scenes/authentication/signup/SignupContainer.tsx @@ -1,15 +1,17 @@ import { useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { userLogic } from 'scenes/userLogic' -import { SceneExport } from 'scenes/sceneTypes' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { SignupForm } from './signupForm/SignupForm' -import { Region } from '~/types' import { router } from 'kea-router' -import { Link } from 'lib/lemon-ui/Link' -import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' +import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { CLOUD_HOSTNAMES, FEATURE_FLAGS } from 'lib/constants' +import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { userLogic } from 'scenes/userLogic' + +import { Region } from '~/types' + +import { SignupForm } from './signupForm/SignupForm' export const scene: SceneExport = { component: SignupContainer, diff --git a/frontend/src/scenes/authentication/signup/signupForm/SignupForm.tsx b/frontend/src/scenes/authentication/signup/signupForm/SignupForm.tsx index 897fa059db68d..3569e017d4645 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/SignupForm.tsx +++ b/frontend/src/scenes/authentication/signup/signupForm/SignupForm.tsx @@ -1,15 +1,16 @@ -import { useEffect, useState } from 'react' -import { useActions, useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { signupLogic } from './signupLogic' -import { userLogic } from '../../../userLogic' -import { SceneExport } from 'scenes/sceneTypes' import { LemonButton } from '@posthog/lemon-ui' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { useActions, useValues } from 'kea' import { IconArrowLeft } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { useEffect, useState } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { userLogic } from '../../../userLogic' import { SignupPanel1 } from './panels/SignupPanel1' import { SignupPanel2 } from './panels/SignupPanel2' +import { signupLogic } from './signupLogic' export const scene: SceneExport = { component: SignupForm, diff --git a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx index f950fb7ae2a39..088644afa997d 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx +++ b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel1.tsx @@ -1,18 +1,21 @@ -import { useRef, useEffect } from 'react' -import { LemonInput, LemonButton } from '@posthog/lemon-ui' +import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' import PasswordStrength from 'lib/components/PasswordStrength' import { SocialLoginButtons } from 'lib/components/SocialLoginButton/SocialLoginButton' +import { Field } from 'lib/forms/Field' +import { Link } from 'lib/lemon-ui/Link' +import { useEffect, useRef } from 'react' +import { useButtonStyle } from 'scenes/authentication/useButtonStyles' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { signupLogic } from '../signupLogic' -import { Link } from 'lib/lemon-ui/Link' export function SignupPanel1(): JSX.Element | null { const { preflight } = useValues(preflightLogic) const { isSignupPanel1Submitting, signupPanel1 } = useValues(signupLogic) const emailInputRef = useRef(null) + const buttonStyles = useButtonStyle() useEffect(() => { // There's no password in the demo environment @@ -71,6 +74,7 @@ export function SignupPanel1(): JSX.Element | null { data-attr="signup-start" loading={isSignupPanel1Submitting} disabled={isSignupPanel1Submitting} + {...buttonStyles} > Continue diff --git a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx index 07ea86b6bd31a..a6d893ca97537 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx +++ b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx @@ -1,17 +1,20 @@ -import { LemonInput, LemonButton, Link } from '@posthog/lemon-ui' +import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' +import SignupReferralSource from 'lib/components/SignupReferralSource' import SignupRoleSelect from 'lib/components/SignupRoleSelect' import { Field } from 'lib/forms/Field' +import { useButtonStyle } from 'scenes/authentication/useButtonStyles' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { signupLogic } from '../signupLogic' -import SignupReferralSource from 'lib/components/SignupReferralSource' const UTM_TAGS = 'utm_campaign=in-product&utm_tag=signup-header' export function SignupPanel2(): JSX.Element | null { const { preflight } = useValues(preflightLogic) const { isSignupPanel2Submitting } = useValues(signupLogic) + const buttonStyles = useButtonStyle() return (
    @@ -44,6 +47,7 @@ export function SignupPanel2(): JSX.Element | null { data-attr="signup-submit" loading={isSignupPanel2Submitting} disabled={isSignupPanel2Submitting} + {...buttonStyles} > {!preflight?.demo ? 'Create account' diff --git a/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts b/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts index a7a6b798d82d7..803f47ecad608 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts +++ b/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts @@ -1,14 +1,15 @@ -import { kea, path, connect, actions, reducers } from 'kea' -import { urlToAction } from 'kea-router' +import { lemonToast } from '@posthog/lemon-ui' +import { isString } from '@tiptap/core' +import { actions, connect, kea, path, reducers } from 'kea' import { forms } from 'kea-forms' +import { urlToAction } from 'kea-router' import api from 'lib/api' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import type { signupLogicType } from './signupLogicType' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { CLOUD_HOSTNAMES, FEATURE_FLAGS } from 'lib/constants' -import { lemonToast } from '@posthog/lemon-ui' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { urls } from 'scenes/urls' -import { isString } from '@tiptap/core' + +import type { signupLogicType } from './signupLogicType' export interface AccountResponse { success: boolean @@ -87,7 +88,7 @@ export const signupLogic = kea([ organization_name: !organization_name ? 'Please enter your organization name' : undefined, }), submit: async (payload, breakpoint) => { - await breakpoint() + breakpoint() try { const res = await api.create('api/signup/', { ...values.signupPanel1, ...payload }) location.href = res.redirect_url || '/' diff --git a/frontend/src/scenes/authentication/signup/verify-email/VerifyEmail.tsx b/frontend/src/scenes/authentication/signup/verify-email/VerifyEmail.tsx index e646a86352de5..723bb6061d41b 100644 --- a/frontend/src/scenes/authentication/signup/verify-email/VerifyEmail.tsx +++ b/frontend/src/scenes/authentication/signup/verify-email/VerifyEmail.tsx @@ -2,11 +2,12 @@ import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { BridgePage } from 'lib/components/BridgePage/BridgePage' import { HeartHog, MailHog, SurprisedHog } from 'lib/components/hedgehogs' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { SupportModal } from 'lib/components/Support/SupportModal' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { SceneExport } from 'scenes/sceneTypes' + import { verifyEmailLogic } from './verifyEmailLogic' -import { SupportModal } from 'lib/components/Support/SupportModal' -import { supportLogic } from 'lib/components/Support/supportLogic' export const scene: SceneExport = { component: VerifyEmail, @@ -51,7 +52,7 @@ export function VerifyEmail(): JSX.Element { return (
    - +
    {view === 'pending' ? ( <> diff --git a/frontend/src/scenes/authentication/signup/verify-email/verifyEmailLogic.ts b/frontend/src/scenes/authentication/signup/verify-email/verifyEmailLogic.ts index 72c68e8b58423..8b28386fff3e3 100644 --- a/frontend/src/scenes/authentication/signup/verify-email/verifyEmailLogic.ts +++ b/frontend/src/scenes/authentication/signup/verify-email/verifyEmailLogic.ts @@ -3,6 +3,7 @@ import { loaders } from 'kea-loaders' import { urlToAction } from 'kea-router' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' + import type { verifyEmailLogicType } from './verifyEmailLogicType' export interface ResponseType { diff --git a/frontend/src/scenes/authentication/useButtonStyles.ts b/frontend/src/scenes/authentication/useButtonStyles.ts new file mode 100644 index 0000000000000..9f678edaa2018 --- /dev/null +++ b/frontend/src/scenes/authentication/useButtonStyles.ts @@ -0,0 +1,13 @@ +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' + +export function useButtonStyle(): Record { + const is3000 = useFeatureFlag('POSTHOG_3000') + + return is3000 + ? { + size: 'large', + } + : { + size: 'medium', + } +} diff --git a/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx b/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx index c9a92bb9d1aa8..94d2416f57ca2 100644 --- a/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportBackfillModal.tsx @@ -1,12 +1,11 @@ import { useActions, useValues } from 'kea' - -import { LemonButton } from 'lib/lemon-ui/LemonButton' - -import { LemonModal } from 'lib/lemon-ui/LemonModal' import { Form } from 'kea-forms' import { Field } from 'lib/forms/Field' -import { batchExportLogic } from './batchExportLogic' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCalendarSelectInput } from 'lib/lemon-ui/LemonCalendar/LemonCalendarSelect' +import { LemonModal } from 'lib/lemon-ui/LemonModal' + +import { batchExportLogic } from './batchExportLogic' export function BatchExportBackfillModal(): JSX.Element { const { batchExportConfig, isBackfillModalOpen, isBackfillFormSubmitting } = useValues(batchExportLogic) diff --git a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx index 78a5d8d4dd2e6..e2774c7b38a98 100644 --- a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx @@ -1,17 +1,18 @@ -import { LemonInput, LemonSelect, LemonCheckbox, LemonDivider, LemonButton } from '@posthog/lemon-ui' -import { useValues, useActions } from 'kea' +import { LemonButton, LemonCheckbox, LemonDivider, LemonInput, LemonSelect } from '@posthog/lemon-ui' +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 { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' -import { IconInfo } from 'lib/lemon-ui/icons' -import { BatchExportsEditLogicProps, batchExportsEditLogic } from './batchExportEditLogic' -import { Field } from 'lib/forms/Field' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' + +import { batchExportsEditLogic, BatchExportsEditLogicProps } from './batchExportEditLogic' export function BatchExportsEditForm(props: BatchExportsEditLogicProps): JSX.Element { const logic = batchExportsEditLogic(props) diff --git a/frontend/src/scenes/batch_exports/BatchExportEditScene.tsx b/frontend/src/scenes/batch_exports/BatchExportEditScene.tsx index 7f5a66fd4798d..3c9e42f872ef8 100644 --- a/frontend/src/scenes/batch_exports/BatchExportEditScene.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportEditScene.tsx @@ -1,9 +1,10 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' import { useValues } from 'kea' -import { BatchExportsEditLogicProps, batchExportsEditLogic } from './batchExportEditLogic' -import { batchExportsEditSceneLogic } from './batchExportEditSceneLogic' +import { PageHeader } from 'lib/components/PageHeader' +import { SceneExport } from 'scenes/sceneTypes' + import { BatchExportsEditForm } from './BatchExportEditForm' +import { batchExportsEditLogic, BatchExportsEditLogicProps } from './batchExportEditLogic' +import { batchExportsEditSceneLogic } from './batchExportEditSceneLogic' export const scene: SceneExport = { component: BatchExportsEditScene, @@ -19,7 +20,7 @@ export function BatchExportsEditScene(): JSX.Element { return ( <> - +
    diff --git a/frontend/src/scenes/batch_exports/BatchExportScene.tsx b/frontend/src/scenes/batch_exports/BatchExportScene.tsx index 148297c77d355..244c1f55b236a 100644 --- a/frontend/src/scenes/batch_exports/BatchExportScene.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportScene.tsx @@ -1,36 +1,38 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' +import { TZLabel } from '@posthog/apps-common' import { LemonButton, + LemonCheckbox, LemonDivider, - LemonTable, - LemonTag, LemonInput, + LemonTable, LemonTableColumns, - LemonCheckbox, + LemonTag, } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' import { useActions, useValues } from 'kea' -import { useEffect, useState } from 'react' -import { BatchExportLogicProps, batchExportLogic, BatchExportTab } from './batchExportLogic' -import { BatchExportLogsProps, batchExportLogsLogic, LOGS_PORTION_LIMIT } from './batchExportLogsLogic' -import { BatchExportRunIcon, BatchExportTag } from './components' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' +import { dayjs } from 'lib/dayjs' import { IconEllipsis, IconRefresh } from 'lib/lemon-ui/icons' -import { capitalizeFirstLetter, identifierToHuman } from 'lib/utils' -import { BatchExportBackfillModal } from './BatchExportBackfillModal' -import { humanizeDestination, intervalToFrequency, isRunInProgress } from './utils' -import { TZLabel } from '@posthog/apps-common' -import { Popover } from 'lib/lemon-ui/Popover' import { LemonCalendarRange } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRange' -import { NotFound } from 'lib/components/NotFound' -import { LemonMenu } from 'lib/lemon-ui/LemonMenu' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { LemonMenu } from 'lib/lemon-ui/LemonMenu' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { dayjs } from 'lib/dayjs' -import { BatchExportLogEntryLevel, BatchExportLogEntry } from '~/types' +import { Popover } from 'lib/lemon-ui/Popover' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter, identifierToHuman } from 'lib/utils' import { pluralize } from 'lib/utils' +import { useEffect, useState } from 'react' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { BatchExportLogEntry, BatchExportLogEntryLevel } from '~/types' + +import { BatchExportBackfillModal } from './BatchExportBackfillModal' +import { batchExportLogic, BatchExportLogicProps, BatchExportTab } from './batchExportLogic' +import { batchExportLogsLogic, BatchExportLogsProps, LOGS_PORTION_LIMIT } from './batchExportLogsLogic' +import { BatchExportRunIcon, BatchExportTag } from './components' +import { humanizeDestination, intervalToFrequency, isRunInProgress } from './utils' export const scene: SceneExport = { component: BatchExportScene, @@ -518,7 +520,7 @@ export function BatchExportScene(): JSX.Element {
    ) : ( - + )}
    diff --git a/frontend/src/scenes/batch_exports/BatchExports.scss b/frontend/src/scenes/batch_exports/BatchExports.scss index da0646417ab23..507ce76135072 100644 --- a/frontend/src/scenes/batch_exports/BatchExports.scss +++ b/frontend/src/scenes/batch_exports/BatchExports.scss @@ -1,18 +1,20 @@ .BatchExportRunIcon--pulse { outline: 2px solid transparent; outline-offset: 0; - animation: pulse 2s infinite ease-out; + animation: BatchExportRunIcon__pulse 2s infinite ease-out; } -@keyframes pulse { +@keyframes BatchExportRunIcon__pulse { 0% { outline-offset: 0; - outline-color: var(--primary-light); + outline-color: var(--primary-3000-hover); } + 80% { outline-offset: 20px; outline-color: transparent; } + 100% { outline-offset: 20px; outline-color: transparent; diff --git a/frontend/src/scenes/batch_exports/BatchExports.stories.tsx b/frontend/src/scenes/batch_exports/BatchExports.stories.tsx index cc4bbde8ba5ae..5b1c2f9966acf 100644 --- a/frontend/src/scenes/batch_exports/BatchExports.stories.tsx +++ b/frontend/src/scenes/batch_exports/BatchExports.stories.tsx @@ -1,9 +1,11 @@ import { Meta, StoryFn } from '@storybook/react' -import { App } from 'scenes/App' -import { useEffect } from 'react' import { router } from 'kea-router' -import { mswDecorator } from '~/mocks/browser' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' + import { createExportServiceHandlers } from './__mocks__/api-mocks' export default { diff --git a/frontend/src/scenes/batch_exports/BatchExportsListScene.tsx b/frontend/src/scenes/batch_exports/BatchExportsListScene.tsx index d1883ab08dbdc..228d2ae96a82d 100644 --- a/frontend/src/scenes/batch_exports/BatchExportsListScene.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportsListScene.tsx @@ -1,11 +1,12 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' import { LemonButton, LemonTable, Link } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' import { useActions, useValues } from 'kea' -import { batchExportsListLogic } from './batchExportsListLogic' -import { LemonMenu, LemonMenuItems } from 'lib/lemon-ui/LemonMenu' +import { PageHeader } from 'lib/components/PageHeader' import { IconEllipsis } from 'lib/lemon-ui/icons' +import { LemonMenu, LemonMenuItems } from 'lib/lemon-ui/LemonMenu' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { batchExportsListLogic } from './batchExportsListLogic' import { BatchExportRunIcon, BatchExportTag } from './components' export const scene: SceneExport = { diff --git a/frontend/src/scenes/batch_exports/__mocks__/api-mocks.ts b/frontend/src/scenes/batch_exports/__mocks__/api-mocks.ts index ab62ca297b39e..b56c742569c45 100644 --- a/frontend/src/scenes/batch_exports/__mocks__/api-mocks.ts +++ b/frontend/src/scenes/batch_exports/__mocks__/api-mocks.ts @@ -1,4 +1,5 @@ import { CountedPaginatedResponse } from 'lib/api' + import { BatchExportConfiguration } from '~/types' export const createExportServiceHandlers = ( diff --git a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts index 5931b444d080d..c2b9620d09ac9 100644 --- a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts @@ -1,4 +1,10 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, selectors } from 'kea' +import { forms } from 'kea-forms' +import { beforeUnload, router } from 'kea-router' +import api from 'lib/api' +import { Dayjs, dayjs } from 'lib/dayjs' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { BatchExportConfiguration, @@ -11,17 +17,11 @@ import { Breadcrumb, } from '~/types' -import api from 'lib/api' -import { forms } from 'kea-forms' -import { urls } from 'scenes/urls' -import { beforeUnload, router } from 'kea-router' - import type { batchExportsEditLogicType } from './batchExportEditLogicType' -import { dayjs, Dayjs } from 'lib/dayjs' import { batchExportLogic } from './batchExportLogic' export type BatchExportsEditLogicProps = { - id: string | 'new' + id: string } export type BatchExportConfigurationForm = Omit< @@ -267,22 +267,25 @@ export const batchExportsEditLogic = kea([ (s) => [s.batchExportConfig, s.isNew], (config, isNew): Breadcrumb[] => [ { + key: Scene.BatchExports, name: 'Batch Exports', path: urls.batchExports(), }, ...(isNew ? [ { + key: 'new', name: 'New', }, ] : [ { - name: config?.name ?? 'Loading', + key: config?.id ?? 'loading', + name: config?.name, path: config?.id ? urls.batchExport(config.id) : undefined, }, - { + key: 'edit', name: 'Edit', }, ]), diff --git a/frontend/src/scenes/batch_exports/batchExportEditSceneLogic.ts b/frontend/src/scenes/batch_exports/batchExportEditSceneLogic.ts index b944ad32f546a..e0766163ed448 100644 --- a/frontend/src/scenes/batch_exports/batchExportEditSceneLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportEditSceneLogic.ts @@ -1,13 +1,12 @@ import { connect, kea, key, path, props, selectors } from 'kea' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { Breadcrumb } from '~/types' -import { urls } from 'scenes/urls' - -import { batchExportLogic } from './batchExportLogic' import { BatchExportsEditLogicProps } from './batchExportEditLogic' - import type { batchExportsEditSceneLogicType } from './batchExportEditSceneLogicType' +import { batchExportLogic } from './batchExportLogic' export const batchExportsEditSceneLogic = kea([ props({} as BatchExportsEditLogicProps), @@ -23,22 +22,25 @@ export const batchExportsEditSceneLogic = kea([ (s) => [s.batchExportConfig, s.id], (config, id): Breadcrumb[] => [ { + key: Scene.BatchExports, name: 'Batch Exports', path: urls.batchExports(), }, ...(id === 'new' ? [ { + key: 'new', name: 'New', }, ] : [ { - name: config?.name ?? 'Loading', + key: config?.id || 'loading', + name: config?.name, path: config?.id ? urls.batchExport(config.id) : undefined, }, - { + key: 'edit', name: 'Edit', }, ]), diff --git a/frontend/src/scenes/batch_exports/batchExportLogic.ts b/frontend/src/scenes/batch_exports/batchExportLogic.ts index 66cf9de3e2bfa..37cc5fc86e649 100644 --- a/frontend/src/scenes/batch_exports/batchExportLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportLogic.ts @@ -1,16 +1,16 @@ +import { lemonToast } from '@posthog/lemon-ui' import { actions, beforeUnmount, kea, key, listeners, path, props, reducers, selectors } from 'kea' - +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' -import { BatchExportConfiguration, BatchExportRun, Breadcrumb, GroupedBatchExportRuns } from '~/types' - +import { router } from 'kea-router' import api, { PaginatedResponse } from 'lib/api' - -import { lemonToast } from '@posthog/lemon-ui' -import { forms } from 'kea-forms' -import { dayjs, Dayjs } from 'lib/dayjs' +import { Dayjs, dayjs } from 'lib/dayjs' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + +import { BatchExportConfiguration, BatchExportRun, Breadcrumb, GroupedBatchExportRuns } from '~/types' + import type { batchExportLogicType } from './batchExportLogicType' -import { router } from 'kea-router' export type BatchExportLogicProps = { id: string @@ -228,11 +228,13 @@ export const batchExportLogic = kea([ (s) => [s.batchExportConfig], (config): Breadcrumb[] => [ { + key: Scene.BatchExports, name: 'Batch Exports', path: urls.batchExports(), }, { - name: config?.name ?? 'Loading', + key: config?.id || 'loading', + name: config?.name, }, ], ], diff --git a/frontend/src/scenes/batch_exports/batchExportLogsLogic.ts b/frontend/src/scenes/batch_exports/batchExportLogsLogic.ts index b63e4f50abf20..d361b286bde6d 100644 --- a/frontend/src/scenes/batch_exports/batchExportLogsLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportLogsLogic.ts @@ -1,10 +1,11 @@ -import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import api from '~/lib/api' -import { BatchExportLogEntryLevel, BatchExportLogEntry } from '~/types' import { CheckboxValueType } from 'antd/lib/checkbox/Group' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import { teamLogic } from 'scenes/teamLogic' +import api from '~/lib/api' +import { BatchExportLogEntry, BatchExportLogEntryLevel } from '~/types' + import type { batchExportLogsLogicType } from './batchExportLogsLogicType' export interface BatchExportLogsProps { diff --git a/frontend/src/scenes/batch_exports/batchExportsListLogic.ts b/frontend/src/scenes/batch_exports/batchExportsListLogic.ts index 5ac29c5336dcf..98171e05009ac 100644 --- a/frontend/src/scenes/batch_exports/batchExportsListLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportsListLogic.ts @@ -1,13 +1,12 @@ +import { lemonToast } from '@posthog/lemon-ui' import { actions, afterMount, beforeUnmount, kea, listeners, path, reducers, selectors } from 'kea' - import { loaders } from 'kea-loaders' -import { BatchExportConfiguration } from '~/types' - import api, { CountedPaginatedResponse } from 'lib/api' +import { PaginationManual } from 'lib/lemon-ui/PaginationControl' + +import { BatchExportConfiguration } from '~/types' import type { batchExportsListLogicType } from './batchExportsListLogicType' -import { PaginationManual } from 'lib/lemon-ui/PaginationControl' -import { lemonToast } from '@posthog/lemon-ui' const PAGE_SIZE = 10 // Refresh the current page of exports periodically to see whats up. diff --git a/frontend/src/scenes/batch_exports/components.tsx b/frontend/src/scenes/batch_exports/components.tsx index 5e3856ce78aee..7191de9f40685 100644 --- a/frontend/src/scenes/batch_exports/components.tsx +++ b/frontend/src/scenes/batch_exports/components.tsx @@ -1,9 +1,10 @@ +import './BatchExports.scss' + import { LemonTag } from '@posthog/lemon-ui' import clsx from 'clsx' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { BatchExportConfiguration, BatchExportRun } from '~/types' -import './BatchExports.scss' +import { BatchExportConfiguration, BatchExportRun } from '~/types' export function BatchExportTag({ batchExportConfig }: { batchExportConfig: BatchExportConfiguration }): JSX.Element { return ( diff --git a/frontend/src/scenes/batch_exports/utils.ts b/frontend/src/scenes/batch_exports/utils.ts index 16ebf9d3176ef..56c07e26a7ccf 100644 --- a/frontend/src/scenes/batch_exports/utils.ts +++ b/frontend/src/scenes/batch_exports/utils.ts @@ -25,6 +25,10 @@ export function humanizeDestination(destination: BatchExportDestination): string return `postgresql://${destination.config.user}:***@${destination.config.host}:${destination.config.port}/${destination.config.database}` } + if (destination.type === 'Redshift') { + return `redshift://${destination.config.user}:***@${destination.config.host}:${destination.config.port}/${destination.config.database}` + } + if (destination.type === 'BigQuery') { return `bigquery:${destination.config.project_id}:${destination.config.dataset_id}:${destination.config.table_id}` } diff --git a/frontend/src/scenes/billing/Billing.scss b/frontend/src/scenes/billing/Billing.scss index 6960ebc6e4a07..8088a048cf769 100644 --- a/frontend/src/scenes/billing/Billing.scss +++ b/frontend/src/scenes/billing/Billing.scss @@ -1,6 +1,7 @@ .BillingPlan { max-width: 500px; flex-grow: 1; + .BillingPlan__description { ol, ul { @@ -8,10 +9,12 @@ padding-left: 0; list-style: none; text-align: center; + li { line-height: 1.2rem; margin-bottom: 1rem; } + .disclaimer { font-size: 10px; font-weight: bold; diff --git a/frontend/src/scenes/billing/Billing.stories.tsx b/frontend/src/scenes/billing/Billing.stories.tsx index 3a43552e646c0..ab6cbae8ea895 100644 --- a/frontend/src/scenes/billing/Billing.stories.tsx +++ b/frontend/src/scenes/billing/Billing.stories.tsx @@ -1,9 +1,11 @@ import { Meta } from '@storybook/react' -import { Billing } from './Billing' -import { useStorybookMocks, mswDecorator } from '~/mocks/browser' -import preflightJson from '~/mocks/fixtures/_preflight.json' + +import { mswDecorator, useStorybookMocks } from '~/mocks/browser' import billingJson from '~/mocks/fixtures/_billing_v2.json' import billingJsonWithDiscount from '~/mocks/fixtures/_billing_v2_with_discount.json' +import preflightJson from '~/mocks/fixtures/_preflight.json' + +import { Billing } from './Billing' const meta: Meta = { title: 'Scenes-Other/Billing v2', diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index 5c02c43bb2dbf..63517aa1d99b4 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -1,23 +1,24 @@ -import { useEffect } from 'react' -import { billingLogic } from './billingLogic' import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { Field, Form } from 'kea-forms' +import { PageHeader } from 'lib/components/PageHeader' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { dayjs } from 'lib/dayjs' +import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { IconPlus } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { dayjs } from 'lib/dayjs' -import clsx from 'clsx' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { capitalizeFirstLetter } from 'lib/utils' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { useEffect } from 'react' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { SceneExport } from 'scenes/sceneTypes' + import { BillingHero } from './BillingHero' -import { PageHeader } from 'lib/components/PageHeader' +import { billingLogic } from './billingLogic' import { BillingProduct } from './BillingProduct' -import { IconPlus } from 'lib/lemon-ui/icons' -import { SceneExport } from 'scenes/sceneTypes' -import { supportLogic } from 'lib/components/Support/supportLogic' -import { Field, Form } from 'kea-forms' -import { Tooltip } from 'lib/lemon-ui/Tooltip' export const scene: SceneExport = { component: Billing, diff --git a/frontend/src/scenes/billing/BillingGauge.scss b/frontend/src/scenes/billing/BillingGauge.scss index 37ebdc11bbd20..b9c73a13291f6 100644 --- a/frontend/src/scenes/billing/BillingGauge.scss +++ b/frontend/src/scenes/billing/BillingGauge.scss @@ -1,5 +1,5 @@ .BillingGaugeItem { - animation: billing-gauge-item-expand 800ms cubic-bezier(0.15, 0.15, 0.2, 1) forwards; + animation: BillingGaugeItem__expand 800ms cubic-bezier(0.15, 0.15, 0.2, 1) forwards; .BillingGaugeItem__info { position: absolute; @@ -9,15 +9,14 @@ margin-left: -1px; font-size: 0.8rem; background: var(--bg-light); - bottom: 100%; - padding: 0 0.25rem 0.5rem 0.25rem; + padding: 0 0.25rem 0.5rem; line-height: 1rem; &--bottom { top: 100%; bottom: auto; - padding: 0.5rem 0.25rem 0 0.25rem; + padding: 0.5rem 0.25rem 0; } &:hover { @@ -26,10 +25,11 @@ } } -@keyframes billing-gauge-item-expand { +@keyframes BillingGaugeItem__expand { 0% { width: 0%; } + 100% { width: var(--billing-gauge-item-width); } diff --git a/frontend/src/scenes/billing/BillingGauge.tsx b/frontend/src/scenes/billing/BillingGauge.tsx index bdc61e4437a0c..cb0b5f480da50 100644 --- a/frontend/src/scenes/billing/BillingGauge.tsx +++ b/frontend/src/scenes/billing/BillingGauge.tsx @@ -1,8 +1,9 @@ +import './BillingGauge.scss' + import clsx from 'clsx' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { compactNumber } from 'lib/utils' import { useMemo } from 'react' -import './BillingGauge.scss' type BillingGaugeItemProps = { width: string diff --git a/frontend/src/scenes/billing/BillingHero.scss b/frontend/src/scenes/billing/BillingHero.scss index 0928d8d935c0c..3c30a1ff02d79 100644 --- a/frontend/src/scenes/billing/BillingHero.scss +++ b/frontend/src/scenes/billing/BillingHero.scss @@ -18,5 +18,5 @@ .BillingHero__hog__img { height: 200px; width: 200px; - margin: -20px -30px; + margin: -20px 0; } diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index 0f74dc168071a..ca8e8170a5832 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -1,9 +1,13 @@ -import { BlushingHog } from 'lib/components/hedgehogs' import './BillingHero.scss' +import { BlushingHog } from 'lib/components/hedgehogs' +import useResizeObserver from 'use-resize-observer' + export const BillingHero = (): JSX.Element => { + const { width, ref: billingHeroRef } = useResizeObserver() + return ( -
    +

    How pricing works

    Get the whole hog.

    @@ -13,9 +17,11 @@ export const BillingHero = (): JSX.Element => { limits as low as $0 to control spend.

    -
    - -
    + {width && width > 500 && ( +
    + +
    + )}
    ) } diff --git a/frontend/src/scenes/billing/BillingLimitInput.tsx b/frontend/src/scenes/billing/BillingLimitInput.tsx index 6f1dd652a4acb..db519a096265a 100644 --- a/frontend/src/scenes/billing/BillingLimitInput.tsx +++ b/frontend/src/scenes/billing/BillingLimitInput.tsx @@ -1,18 +1,22 @@ -import { BillingProductV2AddonType, BillingProductV2Type, BillingV2TierType } from '~/types' -import { billingLogic } from './billingLogic' -import { convertAmountToUsage } from './billing-utils' +import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { billingProductLogic } from './billingProductLogic' -import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import clsx from 'clsx' +import { useRef } from 'react' + +import { BillingProductV2AddonType, BillingProductV2Type, BillingV2TierType } from '~/types' + +import { convertAmountToUsage } from './billing-utils' +import { billingLogic } from './billingLogic' +import { billingProductLogic } from './billingProductLogic' export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => { + const limitInputRef = useRef(null) const { billing, billingLoading } = useValues(billingLogic) const { updateBillingLimits } = useActions(billingLogic) const { isEditingBillingLimit, showBillingLimitInput, billingLimitInput, customLimitUsd } = useValues( - billingProductLogic({ product }) + billingProductLogic({ product, billingLimitInputRef: limitInputRef }) ) const { setIsEditingBillingLimit, setBillingLimitInput } = useActions(billingProductLogic({ product })) @@ -78,7 +82,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type } return null } return ( -
    +
    {!isEditingBillingLimit ? ( @@ -104,6 +108,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type } <>
    ) : feature.limit ? ( <> - + {feature.limit && `${convertLargeNumberToWords(feature.limit, null)} ${feature.unit && feature.unit}${ timeDenominator ? `/${timeDenominator}` : '' @@ -36,7 +40,7 @@ export function PlanIcon({ ) : ( <> - + {feature.note} )} @@ -48,6 +52,7 @@ const getProductTiers = ( plan: BillingV2PlanType, product: BillingProductV2Type | BillingProductV2AddonType ): JSX.Element => { + const { width, ref: tiersRef } = useResizeObserver() const tiers = plan?.tiers const allTierPrices = tiers?.map((tier) => parseFloat(tier.unit_amount_usd)) @@ -59,7 +64,8 @@ const getProductTiers = ( tiers?.map((tier, i) => (
    {convertLargeNumberToWords(tier.up_to, tiers[i - 1]?.up_to, true, product.unit)} @@ -72,7 +78,11 @@ const getProductTiers = (
    )) ) : product?.free_allocation ? ( -
    +
    Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo @@ -97,6 +107,7 @@ export const PlanComparison = ({ const fullyFeaturedPlan = plans[plans.length - 1] const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const { redirectPath, billing } = useValues(billingLogic) + const { width, ref: planComparisonRef } = useResizeObserver() const upgradeButtons = plans?.map((plan) => { return ( @@ -132,7 +143,7 @@ export const PlanComparison = ({ }) return ( - +
    @@ -283,6 +294,10 @@ export const PlanComparison = ({ {plans?.map((plan) => ( - + ))} )) diff --git a/frontend/src/scenes/billing/ProductPricingModal.tsx b/frontend/src/scenes/billing/ProductPricingModal.tsx index 150a4be47f7e9..4e7879e80b98e 100644 --- a/frontend/src/scenes/billing/ProductPricingModal.tsx +++ b/frontend/src/scenes/billing/ProductPricingModal.tsx @@ -1,6 +1,8 @@ import { LemonModal } from '@posthog/lemon-ui' import { capitalizeFirstLetter } from 'lib/utils' + import { BillingProductV2AddonType, BillingProductV2Type, BillingV2PlanType } from '~/types' + import { getTierDescription } from './BillingProduct' export const ProductPricingModal = ({ diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx index 9a3b65fa24c03..f4bbb17473f7a 100644 --- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx +++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx @@ -1,15 +1,17 @@ import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui' -import { billingProductLogic } from './billingProductLogic' import { useActions, useValues } from 'kea' + import { BillingProductV2Type } from '~/types' + import { billingLogic } from './billingLogic' +import { billingProductLogic } from './billingProductLogic' export const UnsubscribeSurveyModal = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => { const { surveyID, surveyResponse } = useValues(billingProductLogic({ product })) const { setSurveyResponse, reportSurveySent, reportSurveyDismissed } = useActions(billingProductLogic({ product })) const { deactivateProduct } = useActions(billingLogic) - const textAreaNotEmpty = surveyResponse['$survey_repsonse']?.length > 0 + const textAreaNotEmpty = surveyResponse['$survey_response']?.length > 0 return ( { diff --git a/frontend/src/scenes/billing/billing-utils.spec.ts b/frontend/src/scenes/billing/billing-utils.spec.ts index a28188ba901a3..48c34f4da53ba 100644 --- a/frontend/src/scenes/billing/billing-utils.spec.ts +++ b/frontend/src/scenes/billing/billing-utils.spec.ts @@ -1,3 +1,9 @@ +import { dayjs } from 'lib/dayjs' +import tk from 'timekeeper' + +import billingJson from '~/mocks/fixtures/_billing_v2.json' +import billingJsonWithFlatFee from '~/mocks/fixtures/_billing_v2_with_flat_fee.json' + import { convertAmountToUsage, convertLargeNumberToWords, @@ -5,10 +11,6 @@ import { projectUsage, summarizeUsage, } from './billing-utils' -import tk from 'timekeeper' -import { dayjs } from 'lib/dayjs' -import billingJson from '~/mocks/fixtures/_billing_v2.json' -import billingJsonWithFlatFee from '~/mocks/fixtures/_billing_v2_with_flat_fee.json' describe('summarizeUsage', () => { it('should summarise usage', () => { diff --git a/frontend/src/scenes/billing/billing-utils.ts b/frontend/src/scenes/billing/billing-utils.ts index 4b55f893bf04e..b6b152c099fe4 100644 --- a/frontend/src/scenes/billing/billing-utils.ts +++ b/frontend/src/scenes/billing/billing-utils.ts @@ -1,4 +1,5 @@ import { dayjs } from 'lib/dayjs' + import { BillingProductV2Type, BillingV2TierType, BillingV2Type } from '~/types' export const summarizeUsage = (usage: number | null): string => { diff --git a/frontend/src/scenes/billing/billingLogic.ts b/frontend/src/scenes/billing/billingLogic.ts index 4e694d0d115b2..63c5c5222d9e8 100644 --- a/frontend/src/scenes/billing/billingLogic.ts +++ b/frontend/src/scenes/billing/billingLogic.ts @@ -1,19 +1,21 @@ -import { kea, path, actions, connect, afterMount, selectors, listeners, reducers } from 'kea' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' -import api from 'lib/api' -import { BillingProductV2Type, BillingV2Type } from '~/types' import { router, urlToAction } from 'kea-router' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import api from 'lib/api' import { dayjs } from 'lib/dayjs' -import { lemonToast } from '@posthog/lemon-ui' +import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { pluralize } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { userLogic } from 'scenes/userLogic' -import { pluralize } from 'lib/utils' + +import { BillingProductV2Type, BillingV2Type, ProductKey } from '~/types' + import type { billingLogicType } from './billingLogicType' -import { forms } from 'kea-forms' -import { urls } from 'scenes/urls' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export const ALLOCATION_THRESHOLD_ALERT = 0.85 // Threshold to show warning of event usage near limit export const ALLOCATION_THRESHOLD_BLOCK = 1.2 // Threshold to block usage @@ -25,6 +27,8 @@ export interface BillingAlertConfig { contactSupport?: boolean buttonCTA?: string dismissKey?: string + action?: LemonBannerAction + pathName?: string } const parseBillingResponse = (data: Partial): BillingV2Type => { @@ -54,6 +58,8 @@ const parseBillingResponse = (data: Partial): BillingV2Type => { export const billingLogic = kea([ path(['scenes', 'billing', 'billingLogic']), actions({ + setProductSpecificAlert: (productSpecificAlert: BillingAlertConfig | null) => ({ productSpecificAlert }), + setScrollToProductKey: (scrollToProductKey: ProductKey | null) => ({ scrollToProductKey }), setShowLicenseDirectInput: (show: boolean) => ({ show }), reportBillingAlertShown: (alertConfig: BillingAlertConfig) => ({ alertConfig }), reportBillingAlertActionClicked: (alertConfig: BillingAlertConfig) => ({ alertConfig }), @@ -67,6 +73,18 @@ export const billingLogic = kea([ actions: [userLogic, ['loadUser'], eventUsageLogic, ['reportProductUnsubscribed']], }), reducers({ + scrollToProductKey: [ + null as ProductKey | null, + { + setScrollToProductKey: (_, { scrollToProductKey }) => scrollToProductKey, + }, + ], + productSpecificAlert: [ + null as BillingAlertConfig | null, + { + setProductSpecificAlert: (_, { productSpecificAlert }) => productSpecificAlert, + }, + ], showLicenseDirectInput: [ false, { @@ -77,9 +95,7 @@ export const billingLogic = kea([ '' as string, { setRedirectPath: () => { - return window.location.pathname.includes('/ingestion') - ? urls.ingestion() + '/billing' - : window.location.pathname.includes('/onboarding') + return window.location.pathname.includes('/onboarding') ? window.location.pathname + window.location.search : '' }, @@ -88,7 +104,7 @@ export const billingLogic = kea([ isOnboarding: [ false, { - setIsOnboarding: () => window.location.pathname.includes('/ingestion'), + setIsOnboarding: () => window.location.pathname.includes('/onboarding'), }, ], }), @@ -147,8 +163,12 @@ export const billingLogic = kea([ }, ], billingAlert: [ - (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd], - (billing, preflight, projectedTotalAmountUsd): BillingAlertConfig | undefined => { + (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd, s.productSpecificAlert], + (billing, preflight, projectedTotalAmountUsd, productSpecificAlert): BillingAlertConfig | undefined => { + if (productSpecificAlert) { + return productSpecificAlert + } + if (!billing || !preflight?.cloud) { return } @@ -323,6 +343,10 @@ export const billingLogic = kea([ actions.setActivateLicenseValues({ license: hash.license }) actions.submitActivateLicense() } + if (_search.products) { + const products = _search.products.split(',') + actions.setScrollToProductKey(products[0]) + } actions.setRedirectPath() actions.setIsOnboarding() }, diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index aeb72f177c5be..5d78ef5ac7e81 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -1,21 +1,36 @@ -import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import posthog from 'posthog-js' +import React from 'react' + import { BillingProductV2AddonType, BillingProductV2Type, BillingV2PlanType, BillingV2TierType } from '~/types' + +import { convertAmountToUsage } from './billing-utils' import { billingLogic } from './billingLogic' import type { billingProductLogicType } from './billingProductLogicType' -import { convertAmountToUsage } from './billing-utils' -import posthog from 'posthog-js' const DEFAULT_BILLING_LIMIT = 500 +export interface BillingProductLogicProps { + product: BillingProductV2Type | BillingProductV2AddonType + billingLimitInputRef?: React.MutableRefObject +} + export const billingProductLogic = kea([ + props({} as BillingProductLogicProps), key((props) => props.product.type), path(['scenes', 'billing', 'billingProductLogic']), connect({ - values: [billingLogic, ['billing', 'isUnlicensedDebug']], - actions: [billingLogic, ['loadBillingSuccess', 'updateBillingLimitsSuccess', 'deactivateProduct']], - }), - props({ - product: {} as BillingProductV2Type | BillingProductV2AddonType, + values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey']], + actions: [ + billingLogic, + [ + 'loadBillingSuccess', + 'updateBillingLimitsSuccess', + 'deactivateProduct', + 'setProductSpecificAlert', + 'setScrollToProductKey', + ], + ], }), actions({ setIsEditingBillingLimit: (isEditingBillingLimit: boolean) => ({ isEditingBillingLimit }), @@ -215,5 +230,40 @@ export const billingProductLogic = kea([ }) actions.setSurveyID('') }, + setScrollToProductKey: ({ scrollToProductKey }) => { + if (scrollToProductKey && scrollToProductKey === props.product.type) { + const { currentPlan } = values.currentAndUpgradePlans + + if (currentPlan.initial_billing_limit) { + actions.setProductSpecificAlert({ + status: 'warning', + title: 'Billing Limit Automatically Applied', + pathName: '/organization/billing', + dismissKey: `auto-apply-billing-limit-${props.product.type}`, + message: `To protect your costs and ours, we've automatically applied a $${currentPlan?.initial_billing_limit} billing limit for ${props.product.name}.`, + action: { + onClick: () => { + actions.setIsEditingBillingLimit(true) + setTimeout(() => { + if (props.billingLimitInputRef?.current) { + props.billingLimitInputRef?.current.focus() + props.billingLimitInputRef?.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + }, 0) + }, + children: 'Update billing limit', + }, + }) + } + } + }, + })), + events(({ actions, values }) => ({ + afterMount: () => { + actions.setScrollToProductKey(values.scrollToProductKey) + }, })), ]) diff --git a/frontend/src/scenes/cohorts/Cohort.tsx b/frontend/src/scenes/cohorts/Cohort.tsx index d597f2a93964a..2286edee5e429 100644 --- a/frontend/src/scenes/cohorts/Cohort.tsx +++ b/frontend/src/scenes/cohorts/Cohort.tsx @@ -1,8 +1,10 @@ -import { cohortSceneLogic } from './cohortSceneLogic' import 'antd/lib/dropdown/style/index.css' -import { SceneExport } from 'scenes/sceneTypes' + import { CohortEdit } from 'scenes/cohorts/CohortEdit' +import { SceneExport } from 'scenes/sceneTypes' + import { CohortLogicProps } from './cohortEditLogic' +import { cohortSceneLogic } from './cohortSceneLogic' export const scene: SceneExport = { component: Cohort, diff --git a/frontend/src/scenes/cohorts/CohortEdit.tsx b/frontend/src/scenes/cohorts/CohortEdit.tsx index 72774547a2057..edbf5ddd46559 100644 --- a/frontend/src/scenes/cohorts/CohortEdit.tsx +++ b/frontend/src/scenes/cohorts/CohortEdit.tsx @@ -1,34 +1,36 @@ -import { CohortLogicProps, cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' +import { LemonDivider } from '@posthog/lemon-ui' +import { UploadFile } from 'antd/es/upload/interface' +import Dragger from 'antd/lib/upload/Dragger' import { useActions, useValues } from 'kea' -import { userLogic } from 'scenes/userLogic' -import { PageHeader } from 'lib/components/PageHeader' -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Form } from 'kea-forms' import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import { Divider } from 'antd' +import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' +import { CohortTypeEnum } from 'lib/constants' import { Field } from 'lib/forms/Field' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { IconUploadFile } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' -import { COHORT_TYPE_OPTIONS } from 'scenes/cohorts/CohortFilters/constants' -import { CohortTypeEnum } from 'lib/constants' -import { AvailableFeature, NotebookNodeType } from '~/types' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import Dragger from 'antd/lib/upload/Dragger' -import { UploadFile } from 'antd/es/upload/interface' -import { IconUploadFile } from 'lib/lemon-ui/icons' -import { CohortCriteriaGroups } from 'scenes/cohorts/CohortFilters/CohortCriteriaGroups' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { Form } from 'kea-forms' -import { NotFound } from 'lib/components/NotFound' -import { Query } from '~/queries/Query/Query' import { pluralize } from 'lib/utils' -import { LemonDivider } from '@posthog/lemon-ui' -import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect' -import { More } from 'lib/lemon-ui/LemonButton/More' +import { cohortEditLogic, CohortLogicProps } from 'scenes/cohorts/cohortEditLogic' +import { CohortCriteriaGroups } from 'scenes/cohorts/CohortFilters/CohortCriteriaGroups' +import { COHORT_TYPE_OPTIONS } from 'scenes/cohorts/CohortFilters/constants' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect' +import { Query } from '~/queries/Query/Query' +import { AvailableFeature, NotebookNodeType } from '~/types' export function CohortEdit({ id }: CohortLogicProps): JSX.Element { + const is3000 = useFeatureFlag('POSTHOG_3000') const logicProps = { id } const logic = cohortEditLogic(logicProps) const { deleteCohort, setOuterGroupsType, setQuery, duplicateCohort } = useActions(logic) @@ -126,8 +128,8 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { } /> - -
    + {!is3000 && } +
    @@ -211,7 +213,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element {
    ) : ( <> - +
    Matching criteria @@ -236,7 +238,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element { {/* The typeof here is needed to pass the cohort id to the query below. Using `isNewCohort` won't work */} {typeof cohort.id === 'number' && ( <> - +

    Persons in this cohort diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaGroups.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaGroups.tsx index 1535f6974e591..4a57b3dc2beed 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaGroups.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaGroups.tsx @@ -1,17 +1,19 @@ import './CohortCriteriaGroups.scss' -import { criteriaToBehavioralFilterType, isCohortCriteriaGroup } from 'scenes/cohorts/cohortUtils' + +import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { Group } from 'kea-forms' import { Field as KeaField } from 'kea-forms/lib/components' -import clsx from 'clsx' -import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' -import { alphabet } from 'lib/utils' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconCopy, IconDelete, IconPlusMini } from 'lib/lemon-ui/icons' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { useActions, useValues } from 'kea' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' +import { alphabet } from 'lib/utils' +import { cohortEditLogic, CohortLogicProps } from 'scenes/cohorts/cohortEditLogic' import { CohortCriteriaRowBuilder } from 'scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder' -import { CohortLogicProps, cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' +import { criteriaToBehavioralFilterType, isCohortCriteriaGroup } from 'scenes/cohorts/cohortUtils' + import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect' export function CohortCriteriaGroups(logicProps: CohortLogicProps): JSX.Element { diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.stories.tsx index 684a4a78a529a..e87ba2481f9c2 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.stories.tsx @@ -1,17 +1,18 @@ -import { useState } from 'react' import { Meta } from '@storybook/react' +import { useMountedLogic } from 'kea' +import { Form } from 'kea-forms' +import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' +import { useState } from 'react' +import { cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' import { CohortCriteriaRowBuilder, CohortCriteriaRowBuilderProps, } from 'scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder' -import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' -import { useMountedLogic } from 'kea' +import { BehavioralFilterType } from 'scenes/cohorts/CohortFilters/types' + import { actionsModel } from '~/models/actionsModel' import { cohortsModel } from '~/models/cohortsModel' -import { BehavioralFilterType } from 'scenes/cohorts/CohortFilters/types' import { BehavioralEventType } from '~/types' -import { Form } from 'kea-forms' -import { cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' const meta: Meta = { title: 'Filters/Cohort Filters/Row Builder', diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx index 4f7209a7d5583..9b5968364a232 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx @@ -1,16 +1,18 @@ import './CohortCriteriaRowBuilder.scss' -import { BehavioralFilterType, CohortFieldProps, Field, FilterType } from 'scenes/cohorts/CohortFilters/types' -import { renderField, ROWS } from 'scenes/cohorts/CohortFilters/constants' -import { Col, Divider } from 'antd' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconCopy, IconDelete } from 'lib/lemon-ui/icons' -import { AnyCohortCriteriaType, BehavioralEventType, FilterLogicalOperator } from '~/types' + +import { Divider } from 'antd' import clsx from 'clsx' +import { useActions } from 'kea' import { Field as KeaField } from 'kea-forms' +import { IconCopy, IconDelete } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { useActions } from 'kea' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { cohortEditLogic, CohortLogicProps } from 'scenes/cohorts/cohortEditLogic' +import { renderField, ROWS } from 'scenes/cohorts/CohortFilters/constants' +import { BehavioralFilterType, CohortFieldProps, Field, FilterType } from 'scenes/cohorts/CohortFilters/types' import { cleanCriteria } from 'scenes/cohorts/cohortUtils' -import { CohortLogicProps, cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' + +import { AnyCohortCriteriaType, BehavioralEventType, FilterLogicalOperator } from '~/types' export interface CohortCriteriaRowBuilderProps { id: CohortLogicProps['id'] @@ -38,7 +40,7 @@ export function CohortCriteriaRowBuilder({ const renderFieldComponent = (_field: Field, i: number): JSX.Element => { return ( -

    +
    {renderField[_field.type]({ fieldKey: _field.fieldKey, criteria, @@ -46,7 +48,7 @@ export function CohortCriteriaRowBuilder({ ...(_field.groupTypeFieldKey ? { groupTypeFieldKey: _field.groupTypeFieldKey } : {}), onChange: (newCriteria) => setCriteria(newCriteria, groupIndex, index), } as CohortFieldProps)} - +
    ) } @@ -95,7 +97,7 @@ export function CohortCriteriaRowBuilder({ }} > <> - +
    {renderField[FilterType.Behavioral]({ fieldKey: 'value', criteria, @@ -104,7 +106,7 @@ export function CohortCriteriaRowBuilder({ onChangeType?.(newCriteria['value'] ?? BehavioralEventType.PerformEvent) }, })} - +
    diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortField.scss b/frontend/src/scenes/cohorts/CohortFilters/CohortField.scss index fd1b5d9585fbe..075dda92dc0d9 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortField.scss +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortField.scss @@ -47,7 +47,7 @@ background-color: inherit; .ant-select-selection-search { - padding-left: 0px !important; + padding-left: 0 !important; } .ant-select-selection-placeholder { diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortField.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortField.tsx index 31da065eb5cde..ed2a8474d5a9b 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortField.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortField.tsx @@ -1,11 +1,15 @@ import './CohortField.scss' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { useMemo } from 'react' -import { cohortFieldLogic } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' + +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { PropertyValue } from 'lib/components/PropertyFilters/components/PropertyValue' import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' import { TaxonomicPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { useMemo } from 'react' +import { cohortFieldLogic } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' import { CohortFieldBaseProps, CohortNumberFieldProps, @@ -14,9 +18,7 @@ import { CohortTaxonomicFieldProps, CohortTextFieldProps, } from 'scenes/cohorts/CohortFilters/types' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import clsx from 'clsx' -import { PropertyValue } from 'lib/components/PropertyFilters/components/PropertyValue' + import { PropertyFilterType, PropertyFilterValue, PropertyOperator } from '~/types' let uniqueMemoizedIndex = 0 diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortNumberField.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortNumberField.stories.tsx index 3a671f06c009c..3b61333f8914a 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortNumberField.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortNumberField.stories.tsx @@ -1,10 +1,11 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { CohortNumberField } from './CohortField' -import { renderField } from 'scenes/cohorts/CohortFilters/constants' -import { CohortNumberFieldProps, FilterType } from 'scenes/cohorts/CohortFilters/types' import { useMountedLogic } from 'kea' +import { useState } from 'react' import { cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' +import { renderField } from 'scenes/cohorts/CohortFilters/constants' +import { CohortNumberFieldProps, FilterType } from 'scenes/cohorts/CohortFilters/types' + +import { CohortNumberField } from './CohortField' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortPersonPropertiesValuesField.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortPersonPropertiesValuesField.stories.tsx index e76421655908c..7ff9795e439f4 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortPersonPropertiesValuesField.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortPersonPropertiesValuesField.stories.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { CohortPersonPropertiesValuesField } from './CohortField' +import { useState } from 'react' import { renderField } from 'scenes/cohorts/CohortFilters/constants' import { CohortPersonPropertiesValuesFieldProps, FilterType } from 'scenes/cohorts/CohortFilters/types' + import { PropertyOperator } from '~/types' +import { CohortPersonPropertiesValuesField } from './CohortField' + type Story = StoryObj const meta: Meta = { title: 'Filters/Cohort Filters/Fields/Person Properties', diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortSelectorField.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortSelectorField.stories.tsx index 32a18b3219beb..566d691eced0e 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortSelectorField.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortSelectorField.stories.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { CohortSelectorField } from './CohortField' +import { useState } from 'react' import { CohortSelectorFieldProps, FieldOptionsType } from 'scenes/cohorts/CohortFilters/types' +import { CohortSelectorField } from './CohortField' + type Story = StoryObj const meta: Meta = { title: 'Filters/Cohort Filters/Fields/Select', diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortTaxonomicField.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortTaxonomicField.stories.tsx index e80b150292d33..69e9421c43aa1 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortTaxonomicField.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortTaxonomicField.stories.tsx @@ -1,13 +1,15 @@ -import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { CohortTaxonomicField } from './CohortField' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' import { useMountedLogic } from 'kea' -import { actionsModel } from '~/models/actionsModel' +import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { useState } from 'react' import { renderField } from 'scenes/cohorts/CohortFilters/constants' import { CohortTaxonomicFieldProps, FilterType } from 'scenes/cohorts/CohortFilters/types' +import { actionsModel } from '~/models/actionsModel' + +import { CohortTaxonomicField } from './CohortField' + type Story = StoryObj const meta: Meta = { title: 'Filters/Cohort Filters/Fields/Taxonomic', diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortTextField.stories.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortTextField.stories.tsx index e119a31370cf5..95044ccf51cad 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortTextField.stories.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortTextField.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { CohortTextField } from './CohortField' import { renderField } from 'scenes/cohorts/CohortFilters/constants' import { CohortTextFieldProps, FilterType } from 'scenes/cohorts/CohortFilters/types' +import { CohortTextField } from './CohortField' + type Story = StoryObj const meta: Meta = { title: 'Filters/Cohort Filters/Fields/Text', diff --git a/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.test.ts b/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.test.ts index c6647cb5a6b7a..f5d9933748cd2 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.test.ts +++ b/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.test.ts @@ -1,11 +1,12 @@ -import { cohortFieldLogic, CohortFieldLogicProps } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' -import { useMocks } from '~/mocks/jest' -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' -import { groupsModel } from '~/models/groupsModel' import { MOCK_GROUP_TYPES } from 'lib/api.mock' -import { FieldOptionsType } from 'scenes/cohorts/CohortFilters/types' +import { cohortFieldLogic, CohortFieldLogicProps } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' import { FIELD_VALUES } from 'scenes/cohorts/CohortFilters/constants' +import { FieldOptionsType } from 'scenes/cohorts/CohortFilters/types' + +import { useMocks } from '~/mocks/jest' +import { groupsModel } from '~/models/groupsModel' +import { initKeaTests } from '~/test/init' describe('cohortFieldLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.ts b/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.ts index 7ab38207877e3..fd304edb571f8 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.ts +++ b/frontend/src/scenes/cohorts/CohortFilters/cohortFieldLogic.ts @@ -1,15 +1,17 @@ -import { actions, kea, key, connect, propsChanged, listeners, path, props, reducers, selectors } from 'kea' -import { BehavioralFilterKey, FieldOptionsType, FieldValues } from 'scenes/cohorts/CohortFilters/types' +import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { objectsEqual } from 'lib/utils' import { FIELD_VALUES, SCALE_FIELD_VALUES } from 'scenes/cohorts/CohortFilters/constants' +import { BehavioralFilterKey, FieldOptionsType, FieldValues } from 'scenes/cohorts/CohortFilters/types' +import { cleanBehavioralTypeCriteria, resolveCohortFieldValue } from 'scenes/cohorts/cohortUtils' +import { userLogic } from 'scenes/userLogic' + +import { actionsModel } from '~/models/actionsModel' +import { cohortsModel } from '~/models/cohortsModel' import { groupsModel } from '~/models/groupsModel' import { ActorGroupType, AnyCohortCriteriaType, AvailableFeature } from '~/types' + import type { cohortFieldLogicType } from './cohortFieldLogicType' -import { cleanBehavioralTypeCriteria, resolveCohortFieldValue } from 'scenes/cohorts/cohortUtils' -import { cohortsModel } from '~/models/cohortsModel' -import { actionsModel } from '~/models/actionsModel' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { objectsEqual } from 'lib/utils' -import { userLogic } from 'scenes/userLogic' export interface CohortFieldLogicProps { cohortFilterLogicKey: string diff --git a/frontend/src/scenes/cohorts/CohortFilters/constants.tsx b/frontend/src/scenes/cohorts/CohortFilters/constants.tsx index d0ed52f9f26f7..dc17d1b7883ea 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/constants.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/constants.tsx @@ -1,3 +1,13 @@ +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { CohortTypeEnum, PROPERTY_MATCH_TYPE } from 'lib/constants' +import { LemonSelectOptions } from 'lib/lemon-ui/LemonSelect' +import { + CohortNumberField, + CohortPersonPropertiesValuesField, + CohortSelectorField, + CohortTaxonomicField, + CohortTextField, +} from 'scenes/cohorts/CohortFilters/CohortField' import { BehavioralFilterKey, BehavioralFilterType, @@ -12,6 +22,7 @@ import { FilterType, Row, } from 'scenes/cohorts/CohortFilters/types' + import { ActorGroupType, BaseMathType, @@ -27,16 +38,6 @@ import { TimeUnitType, ValueOptionType, } from '~/types' -import { - CohortNumberField, - CohortPersonPropertiesValuesField, - CohortSelectorField, - CohortTaxonomicField, - CohortTextField, -} from 'scenes/cohorts/CohortFilters/CohortField' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { LemonSelectOptions } from 'lib/lemon-ui/LemonSelect' -import { CohortTypeEnum, PROPERTY_MATCH_TYPE } from 'lib/constants' /* * Cohort filters are broken down into 3 layers of components. diff --git a/frontend/src/scenes/cohorts/CohortFilters/types.ts b/frontend/src/scenes/cohorts/CohortFilters/types.ts index feb320cd340ce..f66717385bd6b 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/types.ts +++ b/frontend/src/scenes/cohorts/CohortFilters/types.ts @@ -1,3 +1,6 @@ +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { CohortFieldLogicProps } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' + import { AnyCohortCriteriaType, BehavioralCohortType, @@ -6,8 +9,6 @@ import { PropertyFilterValue, PropertyOperator, } from '~/types' -import { CohortFieldLogicProps } from 'scenes/cohorts/CohortFilters/cohortFieldLogic' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' export enum FilterType { Behavioral = 'behavioral', diff --git a/frontend/src/scenes/cohorts/Cohorts.scss b/frontend/src/scenes/cohorts/Cohorts.scss index a5e797a1d45eb..ba03ee05269f4 100644 --- a/frontend/src/scenes/cohorts/Cohorts.scss +++ b/frontend/src/scenes/cohorts/Cohorts.scss @@ -8,6 +8,7 @@ padding: 0.5rem 1rem; margin-top: 1rem; border-radius: var(--radius); + .ant-spin-spinning { margin-right: 0.375rem; } @@ -21,7 +22,7 @@ border-radius: 4px !important; &:hover { - border-color: var(--primary-light) !important; + border-color: var(--primary-3000-hover) !important; } .ant-upload-drag-container { @@ -35,7 +36,7 @@ .ant-upload-text { font-weight: 600; font-size: 1rem !important; - margin: 8px 0 0 0 !important; + margin: 8px 0 0 !important; } } } diff --git a/frontend/src/scenes/cohorts/Cohorts.tsx b/frontend/src/scenes/cohorts/Cohorts.tsx index 5966ef782dfcd..0ed8f9efe59d0 100644 --- a/frontend/src/scenes/cohorts/Cohorts.tsx +++ b/frontend/src/scenes/cohorts/Cohorts.tsx @@ -1,24 +1,27 @@ -import { useState } from 'react' -import { cohortsModel } from '../../models/cohortsModel' -import { useValues, useActions } from 'kea' -import { AvailableFeature, CohortType, ProductKey } from '~/types' import './Cohorts.scss' + +import { LemonInput } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { combineUrl, router } from 'kea-router' +import { ListHog } from 'lib/components/hedgehogs' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { FEATURE_FLAGS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { Link } from 'lib/lemon-ui/Link' -import { dayjs } from 'lib/dayjs' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useState } from 'react' import { urls } from 'scenes/urls' -import { LemonTable, LemonTableColumns, LemonTableColumn } from 'lib/lemon-ui/LemonTable' import { userLogic } from 'scenes/userLogic' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { combineUrl, router } from 'kea-router' -import { LemonInput } from '@posthog/lemon-ui' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { ListHog } from 'lib/components/hedgehogs' + +import { AvailableFeature, CohortType, ProductKey } from '~/types' + +import { cohortsModel } from '../../models/cohortsModel' export function Cohorts(): JSX.Element { const { cohorts, cohortsSearch, cohortsLoading } = useValues(cohortsModel) diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.test.ts b/frontend/src/scenes/cohorts/cohortEditLogic.test.ts index 37061f8217024..94f6910e1b99b 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.test.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.test.ts @@ -1,12 +1,17 @@ -import { initKeaTests } from '~/test/init' +import { router } from 'kea-router' import { expectLogic, partial } from 'kea-test-utils' -import { useMocks } from '~/mocks/jest' -import { mockCohort } from '~/test/mocks' -import { teamLogic } from 'scenes/teamLogic' import { api } from 'lib/api.mock' -import { cohortsModel } from '~/models/cohortsModel' -import { router } from 'kea-router' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { cohortEditLogic, CohortLogicProps } from 'scenes/cohorts/cohortEditLogic' +import { CRITERIA_VALIDATIONS, NEW_CRITERIA, ROWS } from 'scenes/cohorts/CohortFilters/constants' +import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' + +import { useMocks } from '~/mocks/jest' +import { cohortsModel } from '~/models/cohortsModel' +import { initKeaTests } from '~/test/init' +import { mockCohort } from '~/test/mocks' import { BehavioralEventType, BehavioralLifecycleType, @@ -15,10 +20,6 @@ import { PropertyOperator, TimeUnitType, } from '~/types' -import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { CRITERIA_VALIDATIONS, NEW_CRITERIA, ROWS } from 'scenes/cohorts/CohortFilters/constants' -import { CohortLogicProps, cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' describe('cohortEditLogic', () => { let logic: ReturnType @@ -77,8 +78,8 @@ describe('cohortEditLogic', () => { it('delete cohort', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort(mockCohort) - await logic.actions.deleteCohort() + logic.actions.setCohort(mockCohort) + logic.actions.deleteCohort() }) .toFinishAllListeners() .toDispatchActions(['setCohort', 'deleteCohort', router.actionCreators.push(urls.cohorts())]) @@ -92,7 +93,7 @@ describe('cohortEditLogic', () => { it('save with valid cohort', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -116,7 +117,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortSuccess']) expect(api.update).toBeCalledTimes(1) }) @@ -124,11 +125,11 @@ describe('cohortEditLogic', () => { it('do not save with invalid name', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, name: '', }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) expect(api.update).toBeCalledTimes(0) }) @@ -137,7 +138,7 @@ describe('cohortEditLogic', () => { it('do not save on OR operator', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -171,7 +172,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -199,7 +200,7 @@ describe('cohortEditLogic', () => { it('do not save on less than one positive matching criteria', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -225,7 +226,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -252,7 +253,7 @@ describe('cohortEditLogic', () => { it('do not save on criteria cancelling each other out', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -286,7 +287,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -317,7 +318,7 @@ describe('cohortEditLogic', () => { it('do not save on invalid lower and upper bound period values - perform event regularly', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -346,7 +347,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -376,7 +377,7 @@ describe('cohortEditLogic', () => { it('do not save on invalid lower and upper bound period values - perform events in sequence', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -403,7 +404,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -435,7 +436,7 @@ describe('cohortEditLogic', () => { it(`${key} row missing all required fields`, async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -461,7 +462,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -496,13 +497,13 @@ describe('cohortEditLogic', () => { it('can save existing static cohort with empty csv', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, is_static: true, groups: [], csv: undefined, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortSuccess']) expect(api.update).toBeCalledTimes(1) }) @@ -510,14 +511,14 @@ describe('cohortEditLogic', () => { it('do not save static cohort with empty csv', async () => { await initCohortLogic({ id: 'new' }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, is_static: true, groups: [], csv: undefined, id: 'new', }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) expect(api.update).toBeCalledTimes(0) }) diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.ts b/frontend/src/scenes/cohorts/cohortEditLogic.ts index 0b53d5ce25e6a..15794de4b6d24 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.ts @@ -1,22 +1,12 @@ import { actions, afterMount, beforeUnmount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import { actionToUrl, router } from 'kea-router' import api from 'lib/api' -import { cohortsModel } from '~/models/cohortsModel' import { ENTITY_MATCH_TYPE, FEATURE_FLAGS } from 'lib/constants' -import { - AnyCohortCriteriaType, - AnyCohortGroupType, - CohortCriteriaGroupFilter, - CohortGroupType, - CohortType, - FilterLogicalOperator, - PropertyFilterType, -} from '~/types' -import { personsLogic } from 'scenes/persons/personsLogic' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { urls } from 'scenes/urls' -import { actionToUrl, router } from 'kea-router' -import { loaders } from 'kea-loaders' -import { forms } from 'kea-forms' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { NEW_COHORT, NEW_CRITERIA, NEW_CRITERIA_GROUP } from 'scenes/cohorts/CohortFilters/constants' import { applyAllCriteriaGroup, applyAllNestedCriteria, @@ -25,12 +15,23 @@ import { isCohortCriteriaGroup, validateGroup, } from 'scenes/cohorts/cohortUtils' -import { NEW_COHORT, NEW_CRITERIA, NEW_CRITERIA_GROUP } from 'scenes/cohorts/CohortFilters/constants' -import type { cohortEditLogicType } from './cohortEditLogicType' -import { processCohort } from 'lib/utils' +import { personsLogic } from 'scenes/persons/personsLogic' +import { urls } from 'scenes/urls' + +import { cohortsModel, processCohort } from '~/models/cohortsModel' import { DataTableNode, Node, NodeKind } from '~/queries/schema' import { isDataTableNode } from '~/queries/utils' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { + AnyCohortCriteriaType, + AnyCohortGroupType, + CohortCriteriaGroupFilter, + CohortGroupType, + CohortType, + FilterLogicalOperator, + PropertyFilterType, +} from '~/types' + +import type { cohortEditLogicType } from './cohortEditLogicType' export type CohortLogicProps = { id?: CohortType['id'] @@ -74,7 +75,7 @@ export const cohortEditLogic = kea([ reducers(({ props, selectors }) => ({ cohort: [ - NEW_COHORT as CohortType, + NEW_COHORT, { setOuterGroupsType: (state, { type }) => ({ ...state, @@ -212,7 +213,7 @@ export const cohortEditLogic = kea([ loaders(({ actions, values, key }) => ({ cohort: [ - NEW_COHORT as CohortType, + NEW_COHORT, { setCohort: ({ cohort }) => processCohort(cohort), fetchCohort: async ({ id }, breakpoint) => { @@ -316,9 +317,15 @@ export const cohortEditLogic = kea([ cohortsModel.findMounted()?.actions.deleteCohort({ id: values.cohort.id, name: values.cohort.name }) router.actions.push(urls.cohorts()) }, + submitCohort: () => { + if (values.cohortHasErrors) { + lemonToast.error('There was an error submiting this cohort. Make sure the cohort filters are correct.') + } + }, checkIfFinishedCalculating: async ({ cohort }, breakpoint) => { if (cohort.is_calculating) { actions.setPollTimeout( + // eslint-disable-next-line @typescript-eslint/no-misused-promises window.setTimeout(async () => { const newCohort = await api.cohorts.get(cohort.id) breakpoint() diff --git a/frontend/src/scenes/cohorts/cohortSceneLogic.ts b/frontend/src/scenes/cohorts/cohortSceneLogic.ts index 52ea69b500b72..a1b604ffb484e 100644 --- a/frontend/src/scenes/cohorts/cohortSceneLogic.ts +++ b/frontend/src/scenes/cohorts/cohortSceneLogic.ts @@ -1,9 +1,11 @@ import { kea, key, path, props, selectors } from 'kea' -import { Breadcrumb } from '~/types' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + import { cohortsModel } from '~/models/cohortsModel' -import { CohortLogicProps } from './cohortEditLogic' +import { Breadcrumb } from '~/types' +import { CohortLogicProps } from './cohortEditLogic' import type { cohortSceneLogicType } from './cohortSceneLogicType' export const cohortSceneLogic = kea([ @@ -13,15 +15,22 @@ export const cohortSceneLogic = kea([ selectors({ breadcrumbs: [ - () => [cohortsModel.selectors.cohortsById, (_, props) => props.id], + () => [cohortsModel.selectors.cohortsById, (_, props) => props.id as CohortLogicProps['id']], (cohortsById, cohortId): Breadcrumb[] => { return [ { + key: Scene.PersonsManagement, + name: 'People', + path: urls.persons(), + }, + { + key: 'cohorts', name: 'Cohorts', path: urls.cohorts(), }, { - name: cohortId !== 'new' ? cohortsById[cohortId]?.name || 'Untitled' : 'Untitled', + key: cohortId || 'loading', + name: cohortId && cohortId !== 'new' ? cohortsById[cohortId]?.name || 'Untitled' : 'Untitled', }, ] }, diff --git a/frontend/src/scenes/cohorts/cohortUtils.tsx b/frontend/src/scenes/cohorts/cohortUtils.tsx index 6c13ba3e5adc6..d8d701daeb13a 100644 --- a/frontend/src/scenes/cohorts/cohortUtils.tsx +++ b/frontend/src/scenes/cohorts/cohortUtils.tsx @@ -1,3 +1,16 @@ +import equal from 'fast-deep-equal' +import { DeepPartialMap, ValidationErrorType } from 'kea-forms' +import { ENTITY_MATCH_TYPE, PROPERTY_MATCH_TYPE } from 'lib/constants' +import { areObjectValuesEmpty, calculateDays, isNumeric } from 'lib/utils' +import { BEHAVIORAL_TYPE_TO_LABEL, CRITERIA_VALIDATIONS, ROWS } from 'scenes/cohorts/CohortFilters/constants' +import { + BehavioralFilterKey, + BehavioralFilterType, + CohortClientErrors, + FieldWithFieldKey, + FilterType, +} from 'scenes/cohorts/CohortFilters/types' + import { ActionType, AnyCohortCriteriaType, @@ -12,18 +25,6 @@ import { PropertyOperator, TimeUnitType, } from '~/types' -import { ENTITY_MATCH_TYPE, PROPERTY_MATCH_TYPE } from 'lib/constants' -import { - BehavioralFilterKey, - BehavioralFilterType, - CohortClientErrors, - FieldWithFieldKey, - FilterType, -} from 'scenes/cohorts/CohortFilters/types' -import { areObjectValuesEmpty, calculateDays, isNumeric } from 'lib/utils' -import { DeepPartialMap, ValidationErrorType } from 'kea-forms' -import equal from 'fast-deep-equal' -import { BEHAVIORAL_TYPE_TO_LABEL, CRITERIA_VALIDATIONS, ROWS } from 'scenes/cohorts/CohortFilters/constants' export function cleanBehavioralTypeCriteria(criteria: AnyCohortCriteriaType): AnyCohortCriteriaType { let type = undefined @@ -89,7 +90,7 @@ export function isValidCohortGroup(criteria: AnyCohortGroupType): boolean { export function createCohortFormData(cohort: CohortType): FormData { const rawCohort = { ...(cohort.name ? { name: cohort.name } : {}), - ...(cohort.description ? { description: cohort.description } : {}), + ...{ description: cohort.description ?? '' }, ...(cohort.csv ? { csv: cohort.csv } : {}), ...(cohort.is_static ? { is_static: cohort.is_static } : {}), filters: JSON.stringify( diff --git a/frontend/src/scenes/dashboard/Dashboard.scss b/frontend/src/scenes/dashboard/Dashboard.scss index 8af88d8d63b37..fe4c80e270a4a 100644 --- a/frontend/src/scenes/dashboard/Dashboard.scss +++ b/frontend/src/scenes/dashboard/Dashboard.scss @@ -14,11 +14,14 @@ white-space: nowrap; display: flex; align-items: center; + .ant-btn { .anticon { vertical-align: baseline; } + margin-left: 10px; + &.button-box { padding: 4px 8px; } @@ -27,9 +30,18 @@ @media (max-width: 768px) { flex-direction: column; + .dashboard-meta { padding-top: 1rem; justify-content: flex-end; } } } + +.DashboardTemplates__option { + border: 1px solid var(--border); + + &:hover { + border-color: var(--primary-3000-hover); + } +} diff --git a/frontend/src/scenes/dashboard/Dashboard.tsx b/frontend/src/scenes/dashboard/Dashboard.tsx index 03418ed2e140c..80310d43f6f2c 100644 --- a/frontend/src/scenes/dashboard/Dashboard.tsx +++ b/frontend/src/scenes/dashboard/Dashboard.tsx @@ -1,26 +1,29 @@ -import { useEffect } from 'react' +import './Dashboard.scss' + +import { IconCalendar } from '@posthog/icons' +import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' -import { dashboardLogic, DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' -import { DashboardItems } from 'scenes/dashboard/DashboardItems' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import './Dashboard.scss' -import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' -import { DashboardMode, DashboardPlacement, DashboardType } from '~/types' -import { DashboardEventSource } from 'lib/utils/eventUsageLogic' -import { EmptyDashboardComponent } from './EmptyDashboardComponent' import { NotFound } from 'lib/components/NotFound' -import { DashboardReloadAction, LastRefreshText } from 'scenes/dashboard/DashboardReloadAction' -import { SceneExport } from 'scenes/sceneTypes' -import { InsightErrorState } from 'scenes/insights/EmptyStates' -import { DashboardHeader } from './DashboardHeader' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { groupsModel } from '../../models/groupsModel' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' +import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { DashboardEventSource } from 'lib/utils/eventUsageLogic' +import { useEffect } from 'react' +import { DashboardItems } from 'scenes/dashboard/DashboardItems' +import { dashboardLogic, DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' +import { DashboardReloadAction, LastRefreshText } from 'scenes/dashboard/DashboardReloadAction' +import { InsightErrorState } from 'scenes/insights/EmptyStates' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { IconCalendar } from '@posthog/icons' + +import { DashboardMode, DashboardPlacement, DashboardType } from '~/types' + +import { groupsModel } from '../../models/groupsModel' +import { DashboardHeader } from './DashboardHeader' +import { EmptyDashboardComponent } from './EmptyDashboardComponent' interface DashboardProps { id?: string @@ -118,22 +121,20 @@ function DashboardScene(): JSX.Element { DashboardPlacement.Export, DashboardPlacement.FeatureFlag, ].includes(placement) && ( -
    -
    - ( - <> - - {key} - - )} - /> -
    +
    + ( + <> + + {key} + + )} + /> = [ { diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 806b6a3ada25d..6d94e94830fe6 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -1,38 +1,40 @@ import { useActions, useValues } from 'kea' +import { router } from 'kea-router' +import { TextCardModal } from 'lib/components/Cards/TextCard/TextCardModal' import { EditableField } from 'lib/components/EditableField/EditableField' +import { ExportButton, ExportButtonItem } from 'lib/components/ExportButton/ExportButton' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FullScreen } from 'lib/components/FullScreen' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { PageHeader } from 'lib/components/PageHeader' +import { SharingModal } from 'lib/components/Sharing/SharingModal' +import { SubscribeButton, SubscriptionsModal } from 'lib/components/Subscriptions/SubscriptionsModal' +import { privilegeLevelToName } from 'lib/constants' +import { IconLock } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonWithSideAction } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { PageHeader } from 'lib/components/PageHeader' +import { isLemonSelectSection } from 'lib/lemon-ui/LemonSelect' +import { ProfileBubbles } from 'lib/lemon-ui/ProfilePicture/ProfileBubbles' import { humanFriendlyDetailedTime, slugify } from 'lib/utils' import { DashboardEventSource } from 'lib/utils/eventUsageLogic' +import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' +import { DeleteDashboardModal } from 'scenes/dashboard/DeleteDashboardModal' +import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' +import { DuplicateDashboardModal } from 'scenes/dashboard/DuplicateDashboardModal' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + import { dashboardsModel } from '~/models/dashboardsModel' +import { notebooksModel } from '~/models/notebooksModel' +import { tagsModel } from '~/models/tagsModel' import { AvailableFeature, DashboardMode, DashboardType, ExporterFormat } from '~/types' -import { dashboardLogic } from './dashboardLogic' + import { DASHBOARD_RESTRICTION_OPTIONS } from './DashboardCollaborators' -import { userLogic } from 'scenes/userLogic' -import { privilegeLevelToName } from 'lib/constants' -import { ProfileBubbles } from 'lib/lemon-ui/ProfilePicture/ProfileBubbles' import { dashboardCollaboratorsLogic } from './dashboardCollaboratorsLogic' -import { IconLock } from 'lib/lemon-ui/icons' -import { urls } from 'scenes/urls' -import { ExportButton, ExportButtonItem } from 'lib/components/ExportButton/ExportButton' -import { SubscribeButton, SubscriptionsModal } from 'lib/components/Subscriptions/SubscriptionsModal' -import { router } from 'kea-router' -import { SharingModal } from 'lib/components/Sharing/SharingModal' -import { isLemonSelectSection } from 'lib/lemon-ui/LemonSelect' -import { TextCardModal } from 'lib/components/Cards/TextCard/TextCardModal' -import { DeleteDashboardModal } from 'scenes/dashboard/DeleteDashboardModal' -import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' -import { DuplicateDashboardModal } from 'scenes/dashboard/DuplicateDashboardModal' -import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' -import { tagsModel } from '~/models/tagsModel' +import { dashboardLogic } from './dashboardLogic' import { DashboardTemplateEditor } from './DashboardTemplateEditor' import { dashboardTemplateEditorLogic } from './dashboardTemplateEditorLogic' -import { notebooksModel } from '~/models/notebooksModel' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' export const DASHBOARD_CANNOT_EDIT_MESSAGE = "You don't have edit permissions for this dashboard. Ask a dashboard collaborator with edit access to add you." @@ -364,14 +366,14 @@ export function DashboardHeader(): JSX.Element | null { onChange={(_, tags) => triggerDashboardUpdate({ tags })} saving={dashboardLoading} tagsAvailable={tags.filter((tag) => !dashboard.tags?.includes(tag))} - className="insight-metadata-tags" + className="mt-2" /> ) : dashboard.tags.length ? ( ) : null} diff --git a/frontend/src/scenes/dashboard/DashboardItems.scss b/frontend/src/scenes/dashboard/DashboardItems.scss index cac3b9299bac4..8bd848c3b2705 100644 --- a/frontend/src/scenes/dashboard/DashboardItems.scss +++ b/frontend/src/scenes/dashboard/DashboardItems.scss @@ -13,13 +13,11 @@ transition: border-color 100ms ease; } } -.react-grid-item { - transition: all 100ms ease; - transition-property: left, top; -} + .react-grid-item.cssTransforms { transition-property: transform; } + .react-grid-item.resizing { z-index: 1; will-change: width, height; @@ -43,16 +41,21 @@ transition: 100ms ease; max-width: 100%; position: relative; - border: 1px solid var(--primary); - outline: 1px solid var(--primary); + border: 1px solid var(--primary-3000); + outline: 1px solid var(--primary-3000); border-radius: var(--radius); z-index: 2; user-select: none; } + .react-resizable-hide > .react-resizable-handle { display: none; } + .react-grid-item { + transition: all 100ms ease; + transition-property: left, top; + & > .react-resizable-handle { position: absolute; width: 2rem; @@ -62,15 +65,18 @@ cursor: se-resize; z-index: 10; } + & > .react-resizable-handle.react-resizable-handle-se { cursor: se-resize; } + & > .react-resizable-handle.react-resizable-handle-e { top: 0; bottom: 2rem; height: auto; cursor: ew-resize; } + & > .react-resizable-handle.react-resizable-handle-s { left: 0; right: 2rem; diff --git a/frontend/src/scenes/dashboard/DashboardItems.tsx b/frontend/src/scenes/dashboard/DashboardItems.tsx index 92eac03b10ba5..797f9db8dfc72 100644 --- a/frontend/src/scenes/dashboard/DashboardItems.tsx +++ b/frontend/src/scenes/dashboard/DashboardItems.tsx @@ -1,18 +1,18 @@ import './DashboardItems.scss' -import { useRef, useState } from 'react' -import { useActions, useValues } from 'kea' -import { Responsive as ReactGridLayout } from 'react-grid-layout' - -import { DashboardMode, DashboardType, DashboardPlacement, DashboardTile } from '~/types' -import { insightsModel } from '~/models/insightsModel' -import { dashboardLogic, BREAKPOINT_COLUMN_COUNTS, BREAKPOINTS } from 'scenes/dashboard/dashboardLogic' import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { InsightCard } from 'lib/components/Cards/InsightCard' +import { TextCard } from 'lib/components/Cards/TextCard/TextCard' import { useResizeObserver } from 'lib/hooks/useResizeObserver' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { DashboardEventSource } from 'lib/utils/eventUsageLogic' -import { TextCard } from 'lib/components/Cards/TextCard/TextCard' +import { useRef, useState } from 'react' +import { Responsive as ReactGridLayout } from 'react-grid-layout' +import { BREAKPOINT_COLUMN_COUNTS, BREAKPOINTS, dashboardLogic } from 'scenes/dashboard/dashboardLogic' + +import { insightsModel } from '~/models/insightsModel' +import { DashboardMode, DashboardPlacement, DashboardTile, DashboardType } from '~/types' export function DashboardItems(): JSX.Element { const { @@ -99,80 +99,59 @@ export function DashboardItems(): JSX.Element { > {tiles?.map((tile: DashboardTile) => { const { insight, text } = tile + + const commonTileProps = { + dashboardId: dashboard?.id, + showResizeHandles: dashboardMode === DashboardMode.Edit, + canResizeWidth: canResizeWidth, + showEditingControls: [ + DashboardPlacement.Dashboard, + DashboardPlacement.ProjectHomepage, + ].includes(placement), + moreButtons: canEditDashboard ? ( + setDashboardMode(DashboardMode.Edit, DashboardEventSource.MoreDropdown)} + status="stealth" + fullWidth + > + Edit layout (E) + + ) : null, + moveToDashboard: ({ id, name }: Pick) => { + if (!dashboard) { + throw new Error('must be on a dashboard to move this tile') + } + moveToDashboard(tile, dashboard.id, id, name) + }, + removeFromDashboard: () => removeTile(tile), + } + if (insight) { return ( updateTileColor(tile.id, color)} ribbonColor={tile.color} - removeFromDashboard={() => removeTile(tile)} refresh={() => refreshAllDashboardItems({ tiles: [tile], action: 'refresh_manual' })} rename={() => renameInsight(insight)} duplicate={() => duplicateInsight(insight)} - moveToDashboard={({ id, name }: Pick) => { - if (!dashboard) { - throw new Error('must be on a dashboard to move an insight') - } - moveToDashboard(tile, dashboard.id, id, name) - }} - showEditingControls={[ - DashboardPlacement.Dashboard, - DashboardPlacement.ProjectHomepage, - ].includes(placement)} showDetailsControls={placement != DashboardPlacement.Export} - moreButtons={ - canEditDashboard ? ( - - setDashboardMode(DashboardMode.Edit, DashboardEventSource.MoreDropdown) - } - status="stealth" - fullWidth - > - Edit layout (E) - - ) : null - } placement={placement} + {...commonTileProps} /> ) } if (text) { return ( removeTile(tile)} + textTile={tile} duplicate={() => duplicateTile(tile)} - moveToDashboard={({ id, name }: Pick) => { - if (!dashboard) { - throw new Error('must be on a dashboard to move a text tile') - } - moveToDashboard(tile, dashboard.id, id, name) - }} - moreButtons={ - canEditDashboard ? ( - - setDashboardMode(DashboardMode.Edit, DashboardEventSource.MoreDropdown) - } - status="stealth" - fullWidth - > - Edit layout (E) - - ) : null - } + {...commonTileProps} /> ) } diff --git a/frontend/src/scenes/dashboard/DashboardReloadAction.tsx b/frontend/src/scenes/dashboard/DashboardReloadAction.tsx index 72c9d2bedebe8..0c66d8e71df4e 100644 --- a/frontend/src/scenes/dashboard/DashboardReloadAction.tsx +++ b/frontend/src/scenes/dashboard/DashboardReloadAction.tsx @@ -1,13 +1,13 @@ +import { LemonButtonWithSideAction, LemonDivider, LemonSwitch } from '@posthog/lemon-ui' import { Radio, RadioChangeEvent } from 'antd' -import { dashboardLogic, DASHBOARD_MIN_REFRESH_INTERVAL_MINUTES } from 'scenes/dashboard/dashboardLogic' -import { useActions, useValues } from 'kea' -import { humanFriendlyDuration } from 'lib/utils' import clsx from 'clsx' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useActions, useValues } from 'kea' import { dayjs } from 'lib/dayjs' -import { LemonButtonWithSideAction, LemonDivider, LemonSwitch } from '@posthog/lemon-ui' import { IconRefresh } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { humanFriendlyDuration } from 'lib/utils' +import { DASHBOARD_MIN_REFRESH_INTERVAL_MINUTES, dashboardLogic } from 'scenes/dashboard/dashboardLogic' export const LastRefreshText = (): JSX.Element => { const { lastRefreshed } = useValues(dashboardLogic) diff --git a/frontend/src/scenes/dashboard/DashboardTemplateEditor.stories.tsx b/frontend/src/scenes/dashboard/DashboardTemplateEditor.stories.tsx index c7c13ec44bc8b..58611ed1d6b76 100644 --- a/frontend/src/scenes/dashboard/DashboardTemplateEditor.stories.tsx +++ b/frontend/src/scenes/dashboard/DashboardTemplateEditor.stories.tsx @@ -1,4 +1,5 @@ import { Meta } from '@storybook/react' + import { DashboardTemplateEditor } from './DashboardTemplateEditor' import { dashboardTemplateEditorLogic } from './dashboardTemplateEditorLogic' diff --git a/frontend/src/scenes/dashboard/DashboardTemplateEditor.tsx b/frontend/src/scenes/dashboard/DashboardTemplateEditor.tsx index d145d26cb396a..bd7dff72006ec 100644 --- a/frontend/src/scenes/dashboard/DashboardTemplateEditor.tsx +++ b/frontend/src/scenes/dashboard/DashboardTemplateEditor.tsx @@ -1,9 +1,10 @@ -import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { useMonaco } from '@monaco-editor/react' -import { useEffect } from 'react' +import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { dashboardTemplateEditorLogic } from './dashboardTemplateEditorLogic' import { CodeEditor } from 'lib/components/CodeEditors' +import { useEffect } from 'react' + +import { dashboardTemplateEditorLogic } from './dashboardTemplateEditorLogic' export function DashboardTemplateEditor({ inline = false }: { inline?: boolean }): JSX.Element { const monaco = useMonaco() diff --git a/frontend/src/scenes/dashboard/DashboardTemplateVariables.tsx b/frontend/src/scenes/dashboard/DashboardTemplateVariables.tsx index 13440778b192a..a2fe844aab338 100644 --- a/frontend/src/scenes/dashboard/DashboardTemplateVariables.tsx +++ b/frontend/src/scenes/dashboard/DashboardTemplateVariables.tsx @@ -1,7 +1,9 @@ import { LemonLabel } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' + import { FilterType, InsightType } from '~/types' + import { dashboardTemplateVariablesLogic } from './dashboardTemplateVariablesLogic' import { newDashboardLogic } from './newDashboardLogic' diff --git a/frontend/src/scenes/dashboard/Dashboards.stories.tsx b/frontend/src/scenes/dashboard/Dashboards.stories.tsx index 1794572670043..93286b38d2bb7 100644 --- a/frontend/src/scenes/dashboard/Dashboards.stories.tsx +++ b/frontend/src/scenes/dashboard/Dashboards.stories.tsx @@ -1,14 +1,16 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' -import { mswDecorator } from '~/mocks/browser' -import { App } from 'scenes/App' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { DashboardEventSource } from 'lib/utils/eventUsageLogic' +import { useEffect } from 'react' +import { App } from 'scenes/App' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' import { useAvailableFeatures } from '~/mocks/features' import { DashboardMode } from '~/types' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { DashboardEventSource } from 'lib/utils/eventUsageLogic' + import { dashboardTemplatesLogic } from './dashboards/templates/dashboardTemplatesLogic' const meta: Meta = { diff --git a/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx b/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx index 0ef1911307f96..035ef4e0e94ab 100644 --- a/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/DeleteDashboardModal.tsx @@ -1,12 +1,10 @@ import { useActions, useValues } from 'kea' - -import { LemonButton } from 'lib/lemon-ui/LemonButton' - -import { LemonModal } from 'lib/lemon-ui/LemonModal' import { Form } from 'kea-forms' -import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { Field } from 'lib/forms/Field' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' export function DeleteDashboardModal(): JSX.Element { const { hideDeleteDashboardModal } = useActions(deleteDashboardLogic) diff --git a/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx b/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx index 61306cfc59705..46beee57edbee 100644 --- a/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/DuplicateDashboardModal.tsx @@ -1,9 +1,9 @@ import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' import { Field } from 'lib/forms/Field' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { Form } from 'kea-forms' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' +import { LemonModal } from 'lib/lemon-ui/LemonModal' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' export function DuplicateDashboardModal(): JSX.Element { diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss b/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss index 8f5e80394144f..bf4a7f6f3db78 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.scss @@ -14,7 +14,9 @@ .posthog-3000 & { --bg-light: var(--bg-3000); // Make the fade blend in with the 3000 background smoothly } + @extend %mixin-gradient-overlay; + width: 100%; height: 150px; } diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx index fc290a9219b1b..f7965f7ba1519 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx @@ -1,12 +1,14 @@ -import { dashboardLogic } from './dashboardLogic' +import './EmptyDashboardComponent.scss' + import { useValues } from 'kea' -import { urls } from 'scenes/urls' +import { IconPlus } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { IconPlus } from 'lib/lemon-ui/icons' -import './EmptyDashboardComponent.scss' import React from 'react' +import { urls } from 'scenes/urls' + import { DASHBOARD_CANNOT_EDIT_MESSAGE } from './DashboardHeader' +import { dashboardLogic } from './dashboardLogic' function SkeletonCard({ children, active }: { children: React.ReactNode; active: boolean }): JSX.Element { return ( diff --git a/frontend/src/scenes/dashboard/NewDashboardModal.scss b/frontend/src/scenes/dashboard/NewDashboardModal.scss index 76aaa50b48d80..b14c650aecaa0 100644 --- a/frontend/src/scenes/dashboard/NewDashboardModal.scss +++ b/frontend/src/scenes/dashboard/NewDashboardModal.scss @@ -2,8 +2,7 @@ .DashboardTemplateChooser { max-width: 780px; grid-template-columns: repeat(3, 1fr); - column-gap: 0.5em; - row-gap: 0.5em; + gap: 0.5em 0.5em; display: grid; } diff --git a/frontend/src/scenes/dashboard/NewDashboardModal.tsx b/frontend/src/scenes/dashboard/NewDashboardModal.tsx index d9b819806bc38..82dc1d2dd8011 100644 --- a/frontend/src/scenes/dashboard/NewDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/NewDashboardModal.tsx @@ -1,19 +1,20 @@ +import './NewDashboardModal.scss' + +import { LemonButton } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useMountedLogic, useValues } from 'kea' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { FallbackCoverImage } from 'lib/components/FallbackCoverImage/FallbackCoverImage' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { dashboardTemplatesLogic } from 'scenes/dashboard/dashboards/templates/dashboardTemplatesLogic' -import { DashboardTemplateVariables } from './DashboardTemplateVariables' -import { LemonButton } from '@posthog/lemon-ui' -import { dashboardTemplateVariablesLogic } from './dashboardTemplateVariablesLogic' -import { DashboardTemplateScope, DashboardTemplateType } from '~/types' +import { pluralize } from 'lib/utils' +import BlankDashboardHog from 'public/blank-dashboard-hog.png' import { useState } from 'react' +import { dashboardTemplatesLogic } from 'scenes/dashboard/dashboards/templates/dashboardTemplatesLogic' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' -import { pluralize } from 'lib/utils' +import { DashboardTemplateScope, DashboardTemplateType } from '~/types' -import BlankDashboardHog from 'public/blank-dashboard-hog.png' -import './NewDashboardModal.scss' -import { FallbackCoverImage } from 'lib/components/FallbackCoverImage/FallbackCoverImage' -import clsx from 'clsx' +import { DashboardTemplateVariables } from './DashboardTemplateVariables' +import { dashboardTemplateVariablesLogic } from './dashboardTemplateVariablesLogic' function TemplateItem({ template, diff --git a/frontend/src/scenes/dashboard/dashboardCollaboratorsLogic.ts b/frontend/src/scenes/dashboard/dashboardCollaboratorsLogic.ts index 1ff801b17ead3..b1ede643920a2 100644 --- a/frontend/src/scenes/dashboard/dashboardCollaboratorsLogic.ts +++ b/frontend/src/scenes/dashboard/dashboardCollaboratorsLogic.ts @@ -1,17 +1,19 @@ +import { actions, connect, events, kea, key, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, events } from 'kea' import api from 'lib/api' import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants' +import { teamMembersLogic } from 'scenes/settings/project/teamMembersLogic' + import { - DashboardType, DashboardCollaboratorType, - UserType, + DashboardType, FusedDashboardCollaboratorType, UserBasicType, + UserType, } from '~/types' + import type { dashboardCollaboratorsLogicType } from './dashboardCollaboratorsLogicType' import { dashboardLogic } from './dashboardLogic' -import { teamMembersLogic } from 'scenes/settings/project/teamMembersLogic' export interface DashboardCollaboratorsLogicProps { dashboardId: DashboardType['id'] diff --git a/frontend/src/scenes/dashboard/dashboardLogic.queryCancellation.test.ts b/frontend/src/scenes/dashboard/dashboardLogic.queryCancellation.test.ts index dce00c6637c2a..ef026526841fa 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.queryCancellation.test.ts +++ b/frontend/src/scenes/dashboard/dashboardLogic.queryCancellation.test.ts @@ -1,15 +1,16 @@ import { expectLogic, partial } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import api from 'lib/api' +import { MOCK_TEAM_ID } from 'lib/api.mock' +import { now } from 'lib/dayjs' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { boxToString, dashboardResult, insightOnDashboard, tileFromInsight } from 'scenes/dashboard/dashboardLogic.test' + +import { useMocks } from '~/mocks/jest' import { dashboardsModel } from '~/models/dashboardsModel' import { insightsModel } from '~/models/insightsModel' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { initKeaTests } from '~/test/init' import { DashboardType, InsightModel, InsightShortId } from '~/types' -import { useMocks } from '~/mocks/jest' -import { now } from 'lib/dayjs' -import { MOCK_TEAM_ID } from 'lib/api.mock' -import api from 'lib/api' -import { boxToString, dashboardResult, insightOnDashboard, tileFromInsight } from 'scenes/dashboard/dashboardLogic.test' const seenQueryIDs: string[] = [] diff --git a/frontend/src/scenes/dashboard/dashboardLogic.test.ts b/frontend/src/scenes/dashboard/dashboardLogic.test.ts index eb04138096c8f..baf761f3e98f9 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.test.ts +++ b/frontend/src/scenes/dashboard/dashboardLogic.test.ts @@ -1,12 +1,18 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ // let tiles assert an insight is present in tests i.e. `tile!.insight` when it must be present for tests to pass import { expectLogic, truth } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import api from 'lib/api' +import { MOCK_TEAM_ID } from 'lib/api.mock' +import { dayjs, now } from 'lib/dayjs' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import _dashboardJson from './__mocks__/dashboard.json' +import { teamLogic } from 'scenes/teamLogic' + +import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' +import { useMocks } from '~/mocks/jest' import { dashboardsModel } from '~/models/dashboardsModel' import { insightsModel } from '~/models/insightsModel' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { DashboardFilter } from '~/queries/schema' +import { initKeaTests } from '~/test/init' import { DashboardTile, DashboardType, @@ -17,13 +23,8 @@ import { TextModel, TileLayout, } from '~/types' -import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' -import { useMocks } from '~/mocks/jest' -import { dayjs, now } from 'lib/dayjs' -import { teamLogic } from 'scenes/teamLogic' -import { MOCK_TEAM_ID } from 'lib/api.mock' -import api from 'lib/api' -import { DashboardFilter } from '~/queries/schema' + +import _dashboardJson from './__mocks__/dashboard.json' const dashboardJson = _dashboardJson as any as DashboardType @@ -33,9 +34,9 @@ export function insightOnDashboard( insight: Partial = {} ): InsightModel { const tiles = dashboardJson.tiles.filter((tile) => !!tile.insight && tile.insight?.id === insightId) - let tile = dashboardJson.tiles[0] as DashboardTile + let tile = dashboardJson.tiles[0] if (tiles.length) { - tile = tiles[0] as DashboardTile + tile = tiles[0] } if (!tile.insight) { throw new Error('tile has no insight') @@ -218,7 +219,7 @@ describe('dashboardLogic', () => { const fromIndex = from.tiles.findIndex( (tile) => !!tile.insight && tile.insight.id === tileToUpdate.insight.id ) - const removedTile = from.tiles.splice(fromIndex, 1)[0] as DashboardTile + const removedTile = from.tiles.splice(fromIndex, 1)[0] // update the insight const insightId = tileToUpdate.insight.id @@ -354,7 +355,7 @@ describe('dashboardLogic', () => { const startingDashboard = dashboards['9'] const tiles = startingDashboard.tiles - const sourceTile = tiles[0] as DashboardTile + const sourceTile = tiles[0] await expectLogic(logic) .toFinishAllListeners() @@ -530,11 +531,11 @@ describe('dashboardLogic', () => { ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { loading: true, timer: expect.anything(), }, - [(dashboards['5'].tiles[1] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[1].insight!.short_id]: { loading: true, timer: expect.anything(), }, @@ -548,29 +549,21 @@ describe('dashboardLogic', () => { // and updates the action in the model (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[1] as DashboardTile).insight!.short_id, + a.payload.insight.short_id === dashboards['5'].tiles[1].insight!.short_id, (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, + a.payload.insight.short_id === dashboards['5'].tiles[0].insight!.short_id, // no longer reloading - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - false - ), - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[1] as DashboardTile).insight!.short_id, - false - ), + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[0].insight!.short_id, false), + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[1].insight!.short_id, false), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, - [(dashboards['5'].tiles[1] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[1].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, @@ -585,21 +578,18 @@ describe('dashboardLogic', () => { it('reloads selected items', async () => { await expectLogic(logic, () => { logic.actions.refreshAllDashboardItems({ - tiles: [dashboards['5'].tiles[0] as DashboardTile], + tiles: [dashboards['5'].tiles[0]], action: 'refresh_manual', }) }) .toFinishAllListeners() .toDispatchActions([ 'refreshAllDashboardItems', - logic.actionCreators.setRefreshStatuses( - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id], - true - ), + logic.actionCreators.setRefreshStatuses([dashboards['5'].tiles[0].insight!.short_id], true), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { loading: true, timer: expect.anything(), }, @@ -612,16 +602,12 @@ describe('dashboardLogic', () => { .toDispatchActionsInAnyOrder([ (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - false - ), + a.payload.insight.short_id === dashboards['5'].tiles[0].insight!.short_id, + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[0].insight!.short_id, false), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, @@ -859,4 +845,3 @@ describe('dashboardLogic', () => { ).toEqual([]) }) }) -/* eslint-enable @typescript-eslint/no-non-null-assertion */ diff --git a/frontend/src/scenes/dashboard/dashboardLogic.tsx b/frontend/src/scenes/dashboard/dashboardLogic.tsx index 5b05ba3edf664..a35ac8752f90e 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboardLogic.tsx @@ -12,17 +12,30 @@ import { selectors, sharedListeners, } from 'kea' -import api, { ApiMethodOptions, getJSONOrThrow } from 'lib/api' -import { dashboardsModel } from '~/models/dashboardsModel' +import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' -import { clearDOMTextSelection, isUserLoggedIn, shouldCancelQuery, toParams, uuid } from 'lib/utils' -import { insightsModel } from '~/models/insightsModel' +import api, { ApiMethodOptions, getJSONOrThrow } from 'lib/api' import { AUTO_REFRESH_DASHBOARD_THRESHOLD_HOURS, DashboardPrivilegeLevel, OrganizationMembershipLevel, } from 'lib/constants' +import { Dayjs, dayjs, now } from 'lib/dayjs' +import { captureTimeToSeeData, currentSessionId, TimeToSeeDataPayload } from 'lib/internalMetrics' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { Link } from 'lib/lemon-ui/Link' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { clearDOMTextSelection, isUserLoggedIn, shouldCancelQuery, toParams, uuid } from 'lib/utils' import { DashboardEventSource, eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { Layout, Layouts } from 'react-grid-layout' +import { calculateLayouts } from 'scenes/dashboard/tileLayouts' +import { insightLogic } from 'scenes/insights/insightLogic' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { insightsModel } from '~/models/insightsModel' import { AnyPropertyFilter, Breadcrumb, @@ -39,20 +52,10 @@ import { TextModel, TileLayout, } from '~/types' -import type { dashboardLogicType } from './dashboardLogicType' -import { Layout, Layouts } from 'react-grid-layout' -import { insightLogic } from 'scenes/insights/insightLogic' -import { teamLogic } from '../teamLogic' -import { urls } from 'scenes/urls' -import { userLogic } from 'scenes/userLogic' -import { dayjs, now, Dayjs } from 'lib/dayjs' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { Link } from 'lib/lemon-ui/Link' -import { captureTimeToSeeData, currentSessionId, TimeToSeeDataPayload } from 'lib/internalMetrics' + import { getResponseBytes, sortDates } from '../insights/utils' -import { loaders } from 'kea-loaders' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { calculateLayouts } from 'scenes/dashboard/tileLayouts' +import { teamLogic } from '../teamLogic' +import type { dashboardLogicType } from './dashboardLogicType' export const BREAKPOINTS: Record = { sm: 1024, @@ -231,7 +234,7 @@ export const dashboardLogic = kea([ filters: values.filters, }) } catch (e) { - lemonToast.error('Could not update dashboardFilters: ' + e) + lemonToast.error('Could not update dashboardFilters: ' + String(e)) return values.dashboard } }, @@ -265,7 +268,7 @@ export const dashboardLogic = kea([ tiles: values.tiles.filter((t) => t.id !== tile.id), } as DashboardType } catch (e) { - lemonToast.error('Could not remove tile from dashboard: ' + e) + lemonToast.error('Could not remove tile from dashboard: ' + String(e)) return values.dashboard } }, @@ -280,7 +283,7 @@ export const dashboardLogic = kea([ tiles: [newTile], } as Partial) } catch (e) { - lemonToast.error('Could not duplicate tile: ' + e) + lemonToast.error('Could not duplicate tile: ' + String(e)) return values.dashboard } }, @@ -464,7 +467,7 @@ export const dashboardLogic = kea([ tiles[tileIndex] = { ...tiles[tileIndex], insight: { - ...((tiles[tileIndex] as DashboardTile).insight as InsightModel), + ...(tiles[tileIndex].insight as InsightModel), name: item.name, last_modified_at: item.last_modified_at, }, @@ -734,11 +737,22 @@ export const dashboardLogic = kea([ (s) => [s.dashboard], (dashboard): Breadcrumb[] => [ { + key: Scene.Dashboards, name: 'Dashboards', path: urls.dashboards(), }, { + key: dashboard?.id || 'new', name: dashboard?.id ? dashboard.name || 'Unnamed' : null, + onRename: async (name) => { + if (dashboard) { + await dashboardsModel.asyncActions.updateDashboard({ + id: dashboard.id, + name, + allowUndo: true, + }) + } + }, }, ], ], @@ -961,7 +975,7 @@ export const dashboardLogic = kea([ ) actions.setRefreshStatus(insight.short_id) - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'dashboard', primary_interaction_id: dashboardQueryId, @@ -1000,13 +1014,13 @@ export const dashboardLogic = kea([ insights_fetched: insights.length, insights_fetched_cached: 0, } - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, is_primary_interaction: !initialLoad, }) if (initialLoad) { const { startTime, responseBytes } = values.dashboardLoadTimerData - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, action: 'initial_load_full', time_to_see_data_ms: Math.floor(performance.now() - startTime), @@ -1017,9 +1031,13 @@ export const dashboardLogic = kea([ } }) - function loadNextPromise(): void { + async function loadNextPromise(): Promise { if (!cancelled && fetchItemFunctions.length > 0) { - fetchItemFunctions.shift()?.().then(loadNextPromise) + const nextPromise = fetchItemFunctions.shift() + if (nextPromise) { + await nextPromise() + await loadNextPromise() + } } } @@ -1066,7 +1084,7 @@ export const dashboardLogic = kea([ } }, loadDashboardItemsSuccess: function (...args) { - sharedListeners.reportLoadTiming(...args) + void sharedListeners.reportLoadTiming(...args) const dashboard = values.dashboard as DashboardType const { action, dashboardQueryId, startTime, responseBytes } = values.dashboardLoadTimerData @@ -1123,9 +1141,9 @@ export const dashboardLogic = kea([ is_primary_interaction: !initialLoad, } - captureTimeToSeeData(values.currentTeamId, payload) + void captureTimeToSeeData(values.currentTeamId, payload) if (initialLoad && allLoaded) { - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, action: 'initial_load_full', is_primary_interaction: true, diff --git a/frontend/src/scenes/dashboard/dashboardTemplateEditorLogic.ts b/frontend/src/scenes/dashboard/dashboardTemplateEditorLogic.ts index 70c81ec1200a9..60a039058f4dc 100644 --- a/frontend/src/scenes/dashboard/dashboardTemplateEditorLogic.ts +++ b/frontend/src/scenes/dashboard/dashboardTemplateEditorLogic.ts @@ -2,11 +2,12 @@ import { lemonToast } from '@posthog/lemon-ui' import { actions, afterMount, connect, kea, listeners, path, reducers } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + import { DashboardTemplateEditorType, DashboardTemplateType, MonacoMarker } from '~/types' -import { dashboardTemplatesLogic } from './dashboards/templates/dashboardTemplatesLogic' +import { dashboardTemplatesLogic } from './dashboards/templates/dashboardTemplatesLogic' import type { dashboardTemplateEditorLogicType } from './dashboardTemplateEditorLogicType' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' export const dashboardTemplateEditorLogic = kea([ path(['scenes', 'dashboard', 'dashboardTemplateEditorLogic']), diff --git a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts index 96f865e8377e8..39b45617e66ac 100644 --- a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts +++ b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts @@ -1,8 +1,9 @@ import { actions, kea, path, props, propsChanged, reducers } from 'kea' +import { isEmptyObject } from 'lib/utils' + import { DashboardTemplateVariableType, FilterType, Optional } from '~/types' import type { dashboardTemplateVariablesLogicType } from './dashboardTemplateVariablesLogicType' -import { isEmptyObject } from 'lib/utils' export interface DashboardTemplateVariablesLogicProps { variables: DashboardTemplateVariableType[] diff --git a/frontend/src/scenes/dashboard/dashboards/Dashboards.tsx b/frontend/src/scenes/dashboard/dashboards/Dashboards.tsx index 6c70731560a5a..f83556b385ec2 100644 --- a/frontend/src/scenes/dashboard/dashboards/Dashboards.tsx +++ b/frontend/src/scenes/dashboard/dashboards/Dashboards.tsx @@ -1,18 +1,19 @@ import { useActions, useValues } from 'kea' -import { dashboardsModel } from '~/models/dashboardsModel' -import { dashboardsLogic, DashboardsTab } from 'scenes/dashboard/dashboards/dashboardsLogic' -import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' import { inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' -import { DeleteDashboardModal } from 'scenes/dashboard/DeleteDashboardModal' -import { DuplicateDashboardModal } from 'scenes/dashboard/DuplicateDashboardModal' -import { NoDashboards } from 'scenes/dashboard/dashboards/NoDashboards' +import { dashboardsLogic, DashboardsTab } from 'scenes/dashboard/dashboards/dashboardsLogic' import { DashboardsTableContainer } from 'scenes/dashboard/dashboards/DashboardsTable' +import { NoDashboards } from 'scenes/dashboard/dashboards/NoDashboards' import { DashboardTemplatesTable } from 'scenes/dashboard/dashboards/templates/DashboardTemplatesTable' -import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { DeleteDashboardModal } from 'scenes/dashboard/DeleteDashboardModal' +import { DuplicateDashboardModal } from 'scenes/dashboard/DuplicateDashboardModal' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' +import { SceneExport } from 'scenes/sceneTypes' + +import { dashboardsModel } from '~/models/dashboardsModel' export const scene: SceneExport = { component: Dashboards, diff --git a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx index 2f0fa2f4aa926..00889a9e1e5aa 100644 --- a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx +++ b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx @@ -1,29 +1,32 @@ +import { IconPin, IconPinFilled, IconShare } from '@posthog/icons' +import { LemonInput, LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { dashboardsModel, nameCompareFunction } from '~/models/dashboardsModel' -import { DashboardsFilters, dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' -import { userLogic } from 'scenes/userLogic' -import { teamLogic } from 'scenes/teamLogic' -import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' -import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { AvailableFeature, DashboardBasicType, DashboardMode, DashboardType } from '~/types' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { DashboardEventSource } from 'lib/utils/eventUsageLogic' -import { DashboardPrivilegeLevel } from 'lib/constants' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconCottage, IconLock, IconPinOutline, IconPinFilled, IconShare } from 'lib/lemon-ui/icons' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { DashboardPrivilegeLevel } from 'lib/constants' +import { IconCottage, IconLock } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import { LemonRow } from 'lib/lemon-ui/LemonRow' -import { DASHBOARD_CANNOT_EDIT_MESSAGE } from '../DashboardHeader' -import { LemonInput, LemonSelect } from '@posthog/lemon-ui' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { DashboardEventSource } from 'lib/utils/eventUsageLogic' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { DashboardsFilters, dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' +import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' +import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' import { membersLogic } from 'scenes/organization/membersLogic' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { dashboardsModel, nameCompareFunction } from '~/models/dashboardsModel' +import { AvailableFeature, DashboardBasicType, DashboardMode, DashboardType } from '~/types' + +import { DASHBOARD_CANNOT_EDIT_MESSAGE } from '../DashboardHeader' export function DashboardsTableContainer(): JSX.Element { const { dashboardsLoading } = useValues(dashboardsModel) @@ -70,7 +73,7 @@ export function DashboardsTable({ : () => pinDashboard(id, DashboardEventSource.DashboardsList) } tooltip={pinned ? 'Unpin dashboard' : 'Pin dashboard'} - icon={pinned ? : } + icon={pinned ? : } /> ) }, @@ -215,28 +218,31 @@ export function DashboardsTable({ />
    - setFilters({ pinned: !filters.pinned })} - icon={} - > - Pinned - -
    -
    - setFilters({ shared: !filters.shared })} - icon={} - > - Shared - + Filter to: +
    + setFilters({ pinned: !filters.pinned })} + icon={} + > + Pinned + +
    +
    + setFilters({ shared: !filters.shared })} + icon={} + > + Shared + +
    Created by: diff --git a/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx b/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx index 0b3dc1735d610..f8797c2e0b6da 100644 --- a/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx +++ b/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx @@ -1,47 +1,51 @@ import { useActions } from 'kea' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' -import { Card } from 'antd' -// eslint-disable-next-line no-restricted-imports -import { AppstoreAddOutlined } from '@ant-design/icons' export const NoDashboards = (): JSX.Element => { - const { addDashboard } = useActions(newDashboardLogic) - return (

    Create your first dashboard:

    - - addDashboard({ - name: 'New Dashboard', - useTemplate: '', - }) - } - > -
    - -
    -
    - + + description="Start with recommended metrics for a web app" + template={{ name: 'Web App Dashboard', template: 'DEFAULT_APP' }} + />
    ) } + +const Option = ({ + title, + description, + template, +}: { + title: string + description: string + template: { name: string; template: string } +}): JSX.Element => { + const { addDashboard } = useActions(newDashboardLogic) + + const onClick = (): void => { + addDashboard({ + name: template.name, + useTemplate: template.template, + }) + } + + return ( +
    +
    {title}
    + {description} +
    + ) +} diff --git a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts index a85500aadf514..61d98a07a914f 100644 --- a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts +++ b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.test.ts @@ -1,10 +1,12 @@ +import { expectLogic, truth } from 'kea-test-utils' import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' -import { initKeaTests } from '~/test/init' + import { useMocks } from '~/mocks/jest' +import { dashboardsModel } from '~/models/dashboardsModel' +import { initKeaTests } from '~/test/init' import { DashboardType, UserBasicType } from '~/types' + import dashboardJson from '../__mocks__/dashboard.json' -import { dashboardsModel } from '~/models/dashboardsModel' -import { expectLogic, truth } from 'kea-test-utils' let dashboardId = 1234 const dashboard = (extras: Partial): DashboardType => { diff --git a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts index 015a792e1b3af..3d9ee6e1969c3 100644 --- a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts +++ b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts @@ -1,13 +1,15 @@ -import { actions, connect, kea, path, reducers, selectors } from 'kea' import Fuse from 'fuse.js' -import { dashboardsModel } from '~/models/dashboardsModel' -import type { dashboardsLogicType } from './dashboardsLogicType' -import { userLogic } from 'scenes/userLogic' +import { actions, connect, kea, path, reducers, selectors } from 'kea' import { actionToUrl, router, urlToAction } from 'kea-router' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { objectClean } from 'lib/utils' +import { userLogic } from 'scenes/userLogic' + +import { dashboardsModel } from '~/models/dashboardsModel' import { DashboardBasicType } from '~/types' +import type { dashboardsLogicType } from './dashboardsLogicType' + export enum DashboardsTab { Dashboards = 'dashboards', Templates = 'templates', @@ -47,7 +49,7 @@ export const dashboardsLogic = kea([ ], filters: [ - DEFAULT_FILTERS as DashboardsFilters, + DEFAULT_FILTERS, { setFilters: (state, { filters }) => objectClean({ diff --git a/frontend/src/scenes/dashboard/dashboards/templates/DashboardTemplatesTable.tsx b/frontend/src/scenes/dashboard/dashboards/templates/DashboardTemplatesTable.tsx index 471d9da1117a5..d260363e63674 100644 --- a/frontend/src/scenes/dashboard/dashboards/templates/DashboardTemplatesTable.tsx +++ b/frontend/src/scenes/dashboard/dashboards/templates/DashboardTemplatesTable.tsx @@ -1,14 +1,15 @@ -import { dashboardTemplatesLogic } from 'scenes/dashboard/dashboards/templates/dashboardTemplatesLogic' -import { useActions, useValues } from 'kea' -import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' -import { DashboardTemplateType } from '~/types' import { LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { More } from 'lib/lemon-ui/LemonButton/More' -import { dashboardTemplateEditorLogic } from 'scenes/dashboard/dashboardTemplateEditorLogic' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { dashboardTemplatesLogic } from 'scenes/dashboard/dashboards/templates/dashboardTemplatesLogic' import { DashboardTemplateEditor } from 'scenes/dashboard/DashboardTemplateEditor' +import { dashboardTemplateEditorLogic } from 'scenes/dashboard/dashboardTemplateEditorLogic' import { userLogic } from 'scenes/userLogic' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' + +import { DashboardTemplateType } from '~/types' export const DashboardTemplatesTable = (): JSX.Element | null => { const { allTemplates, allTemplatesLoading } = useValues(dashboardTemplatesLogic) diff --git a/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx b/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx index df662a9b7d1ef..eeceeeb28d23a 100644 --- a/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx @@ -1,9 +1,9 @@ import { actions, afterMount, connect, kea, key, path, props } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { DashboardTemplateScope, DashboardTemplateType } from '~/types' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import type { dashboardTemplatesLogicType } from './dashboardTemplatesLogicType' diff --git a/frontend/src/scenes/dashboard/deleteDashboardLogic.ts b/frontend/src/scenes/dashboard/deleteDashboardLogic.ts index d6f59e17acc38..65a0beb798c14 100644 --- a/frontend/src/scenes/dashboard/deleteDashboardLogic.ts +++ b/frontend/src/scenes/dashboard/deleteDashboardLogic.ts @@ -1,8 +1,9 @@ import { actions, connect, kea, listeners, path, reducers } from 'kea' +import { forms } from 'kea-forms' import { router } from 'kea-router' import { urls } from 'scenes/urls' + import { dashboardsModel } from '~/models/dashboardsModel' -import { forms } from 'kea-forms' import type { deleteDashboardLogicType } from './deleteDashboardLogicType' diff --git a/frontend/src/scenes/dashboard/duplicateDashboardLogic.ts b/frontend/src/scenes/dashboard/duplicateDashboardLogic.ts index fec8d5a09bb58..108407e9671ab 100644 --- a/frontend/src/scenes/dashboard/duplicateDashboardLogic.ts +++ b/frontend/src/scenes/dashboard/duplicateDashboardLogic.ts @@ -1,9 +1,9 @@ import { actions, connect, kea, listeners, path, reducers } from 'kea' +import { forms } from 'kea-forms' import { router } from 'kea-router' import { urls } from 'scenes/urls' -import { dashboardsModel } from '~/models/dashboardsModel' -import { forms } from 'kea-forms' +import { dashboardsModel } from '~/models/dashboardsModel' import { insightsModel } from '~/models/insightsModel' import type { duplicateDashboardLogicType } from './duplicateDashboardLogicType' diff --git a/frontend/src/scenes/dashboard/newDashboardLogic.ts b/frontend/src/scenes/dashboard/newDashboardLogic.ts index 7c963244ec947..455bfa884cdd7 100644 --- a/frontend/src/scenes/dashboard/newDashboardLogic.ts +++ b/frontend/src/scenes/dashboard/newDashboardLogic.ts @@ -1,15 +1,17 @@ import { actions, connect, isBreakpoint, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import type { newDashboardLogicType } from './newDashboardLogicType' -import { DashboardRestrictionLevel } from 'lib/constants' -import { DashboardTemplateType, DashboardType, DashboardTemplateVariableType, DashboardTile, JsonType } from '~/types' +import { forms } from 'kea-forms' +import { router } from 'kea-router' import api from 'lib/api' +import { DashboardRestrictionLevel } from 'lib/constants' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { teamLogic } from 'scenes/teamLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' + import { dashboardsModel } from '~/models/dashboardsModel' -import { forms } from 'kea-forms' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { DashboardTemplateType, DashboardTemplateVariableType, DashboardTile, DashboardType, JsonType } from '~/types' + +import type { newDashboardLogicType } from './newDashboardLogicType' export interface NewDashboardForm { name: string diff --git a/frontend/src/scenes/dashboard/tileLayouts.test.ts b/frontend/src/scenes/dashboard/tileLayouts.test.ts index 63794cf6395b1..20a6d78ae0d24 100644 --- a/frontend/src/scenes/dashboard/tileLayouts.test.ts +++ b/frontend/src/scenes/dashboard/tileLayouts.test.ts @@ -1,4 +1,5 @@ import { calculateLayouts } from 'scenes/dashboard/tileLayouts' + import { DashboardLayoutSize, DashboardTile, TileLayout } from '~/types' function tileWithLayout(layouts: Record, tileId: number = 1): DashboardTile { diff --git a/frontend/src/scenes/dashboard/tileLayouts.ts b/frontend/src/scenes/dashboard/tileLayouts.ts index 77fdfa268aeeb..90d3481c464c9 100644 --- a/frontend/src/scenes/dashboard/tileLayouts.ts +++ b/frontend/src/scenes/dashboard/tileLayouts.ts @@ -1,7 +1,8 @@ import { Layout } from 'react-grid-layout' -import { ChartDisplayType, DashboardLayoutSize, DashboardTile, FilterType } from '~/types' -import { isPathsFilter, isRetentionFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' import { BREAKPOINT_COLUMN_COUNTS, MIN_ITEM_HEIGHT_UNITS, MIN_ITEM_WIDTH_UNITS } from 'scenes/dashboard/dashboardLogic' +import { isPathsFilter, isRetentionFilter, isTrendsFilter } from 'scenes/insights/sharedUtils' + +import { ChartDisplayType, DashboardLayoutSize, DashboardTile, FilterType } from '~/types' export const sortTilesByLayout = (tiles: Array, col: DashboardLayoutSize): Array => { return [...tiles].sort((a: DashboardTile, b: DashboardTile) => { diff --git a/frontend/src/scenes/data-management/DataManagementScene.stories.tsx b/frontend/src/scenes/data-management/DataManagementScene.stories.tsx index 9f2492a913ce4..606c7a97afff5 100644 --- a/frontend/src/scenes/data-management/DataManagementScene.stories.tsx +++ b/frontend/src/scenes/data-management/DataManagementScene.stories.tsx @@ -1,15 +1,17 @@ -import { mswDecorator, setFeatureFlags } from '~/mocks/browser' import { Meta } from '@storybook/react' -import { useAvailableFeatures } from '~/mocks/features' -import { AvailableFeature } from '~/types' -import { useEffect } from 'react' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { FEATURE_FLAGS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + +import { mswDecorator, setFeatureFlags } from '~/mocks/browser' +import { useAvailableFeatures } from '~/mocks/features' import { DatabaseSchemaQueryResponse } from '~/queries/schema' +import { AvailableFeature } from '~/types' + import { ingestionWarningsResponse } from './ingestion-warnings/__mocks__/ingestion-warnings-response' -import { dayjs } from 'lib/dayjs' -import { FEATURE_FLAGS } from 'lib/constants' const MOCK_DATABASE: DatabaseSchemaQueryResponse = { events: [ diff --git a/frontend/src/scenes/data-management/DataManagementScene.tsx b/frontend/src/scenes/data-management/DataManagementScene.tsx index 0513de6e11e5e..7fec05a325f81 100644 --- a/frontend/src/scenes/data-management/DataManagementScene.tsx +++ b/frontend/src/scenes/data-management/DataManagementScene.tsx @@ -1,30 +1,31 @@ import { actions, connect, kea, path, reducers, selectors, useActions, useValues } from 'kea' import { actionToUrl, urlToAction } from 'kea-router' -import { urls } from 'scenes/urls' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconInfo } from 'lib/lemon-ui/icons' +import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { PageHeader } from 'lib/components/PageHeader' import { TitleWithIcon } from 'lib/components/TitleWithIcon' import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { IconInfo } from 'lib/lemon-ui/icons' import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { capitalizeFirstLetter } from 'lib/utils' import React from 'react' -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' import { NewActionButton } from 'scenes/actions/NewActionButton' import { Annotations } from 'scenes/annotations' +import { NewAnnotationButton } from 'scenes/annotations/AnnotationModal' +import { Scene, SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb } from '~/types' +import { ActionsTable } from './actions/ActionsTable' +import { DatabaseTableList } from './database/DatabaseTableList' import type { dataManagementSceneLogicType } from './DataManagementSceneType' -import { NewAnnotationButton } from 'scenes/annotations/AnnotationModal' import { EventDefinitionsTable } from './events/EventDefinitionsTable' -import { ActionsTable } from './actions/ActionsTable' -import { PropertyDefinitionsTable } from './properties/PropertyDefinitionsTable' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' import { IngestionWarningsView } from './ingestion-warnings/IngestionWarningsView' -import { DatabaseTableList } from './database/DatabaseTableList' -import { Breadcrumb } from '~/types' -import { capitalizeFirstLetter } from 'lib/utils' +import { PropertyDefinitionsTable } from './properties/PropertyDefinitionsTable' export enum DataManagementTab { Actions = 'actions', @@ -96,7 +97,7 @@ const tabs: Record< }, [DataManagementTab.IngestionWarnings]: { url: urls.ingestionWarnings(), - label: 'Ingestion Warnings', + label: 'Ingestion warnings', content: , }, [DataManagementTab.Database]: { @@ -135,10 +136,12 @@ const dataManagementSceneLogic = kea([ (tab): Breadcrumb[] => { return [ { + key: Scene.DataManagement, name: `Data Management`, path: tabs.events.url, }, { + key: tab, name: capitalizeFirstLetter(tab), path: tabs[tab].url, }, diff --git a/frontend/src/scenes/data-management/actions/ActionsTable.tsx b/frontend/src/scenes/data-management/actions/ActionsTable.tsx index aa9bf6aad9876..8efad967b31b3 100644 --- a/frontend/src/scenes/data-management/actions/ActionsTable.tsx +++ b/frontend/src/scenes/data-management/actions/ActionsTable.tsx @@ -1,36 +1,38 @@ -import { Link } from 'lib/lemon-ui/Link' -import { Radio } from 'antd' -import { deleteWithUndo, stripHTTP } from 'lib/utils' +import { LemonInput, LemonSegmentedButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { actionsModel } from '~/models/actionsModel' -import { NewActionButton } from '../../actions/NewActionButton' -import { ActionType, AvailableFeature, ChartDisplayType, InsightType, ProductKey } from '~/types' -import { userLogic } from 'scenes/userLogic' -import { teamLogic } from '../../teamLogic' -import api from 'lib/api' -import { urls } from '../../urls' -import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable/types' -import { LemonTable } from 'lib/lemon-ui/LemonTable' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { More } from 'lib/lemon-ui/LemonButton/More' import { combineUrl } from 'kea-router' +import api from 'lib/api' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { LemonInput } from '@posthog/lemon-ui' -import { actionsLogic } from 'scenes/actions/actionsLogic' -import { IconCheckmark, IconPlayCircle } from 'lib/lemon-ui/icons' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { IconCheckmark, IconPlayCircle } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable/types' +import { Link } from 'lib/lemon-ui/Link' +import { stripHTTP } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { actionsLogic } from 'scenes/actions/actionsLogic' +import { userLogic } from 'scenes/userLogic' + +import { actionsModel } from '~/models/actionsModel' +import { ActionType, AvailableFeature, ChartDisplayType, InsightType, ProductKey } from '~/types' + +import { NewActionButton } from '../../actions/NewActionButton' +import { teamLogic } from '../../teamLogic' +import { urls } from '../../urls' export function ActionsTable(): JSX.Element { const { currentTeam } = useValues(teamLogic) const { actionsLoading } = useValues(actionsModel({ params: 'include_count=1' })) const { loadActions } = useActions(actionsModel) - const { filterByMe, searchTerm, actionsFiltered, shouldShowProductIntroduction, shouldShowEmptyState } = + const { filterType, searchTerm, actionsFiltered, shouldShowProductIntroduction, shouldShowEmptyState } = useValues(actionsLogic) - const { setFilterByMe, setSearchTerm } = useActions(actionsLogic) + const { setFilterType, setSearchTerm } = useActions(actionsLogic) const { hasAvailableFeature } = useValues(userLogic) const { updateHasSeenProductIntroFor } = useActions(userLogic) @@ -202,7 +204,7 @@ export function ActionsTable(): JSX.Element { - deleteWithUndo({ + void deleteWithUndo({ endpoint: api.actions.determineDeleteEndpoint(), object: action, callback: loadActions, @@ -237,7 +239,7 @@ export function ActionsTable(): JSX.Element { } /> )} - {(shouldShowEmptyState && filterByMe) || !shouldShowEmptyState ? ( + {(shouldShowEmptyState && filterType === 'me') || !shouldShowEmptyState ? (
    - setFilterByMe(e.target.value)}> - All actions - My actions - +
    ) : null} - {(!shouldShowEmptyState || filterByMe) && ( + {(!shouldShowEmptyState || filterType === 'me') && ( <> } /> - + {isEvent && ( <> @@ -214,11 +216,11 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { /> )} - + {isEvent && definition.id !== 'new' && ( <> - +
    Matching events

    diff --git a/frontend/src/scenes/data-management/definition/definitionEditLogic.test.ts b/frontend/src/scenes/data-management/definition/definitionEditLogic.test.ts index 1f43f33f8113b..4399c959fd98f 100644 --- a/frontend/src/scenes/data-management/definition/definitionEditLogic.test.ts +++ b/frontend/src/scenes/data-management/definition/definitionEditLogic.test.ts @@ -1,14 +1,15 @@ -import { definitionLogic } from 'scenes/data-management/definition/definitionLogic' -import { useMocks } from '~/mocks/jest' -import { mockEventDefinitions, mockEventPropertyDefinition } from '~/test/mocks' -import { initKeaTests } from '~/test/init' -import { definitionEditLogic } from 'scenes/data-management/definition/definitionEditLogic' +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' +import { definitionEditLogic } from 'scenes/data-management/definition/definitionEditLogic' +import { definitionLogic } from 'scenes/data-management/definition/definitionLogic' import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { mockEventDefinitions, mockEventPropertyDefinition } from '~/test/mocks' + describe('definitionEditLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/data-management/definition/definitionEditLogic.ts b/frontend/src/scenes/data-management/definition/definitionEditLogic.ts index e77625178a9da..8b14c0f51d2a9 100644 --- a/frontend/src/scenes/data-management/definition/definitionEditLogic.ts +++ b/frontend/src/scenes/data-management/definition/definitionEditLogic.ts @@ -1,20 +1,22 @@ import { beforeUnmount, connect, kea, key, path, props } from 'kea' -import { Definition, EventDefinition, PropertyDefinition } from '~/types' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' +import { capitalizeFirstLetter } from 'lib/utils' import { definitionLogic, DefinitionLogicProps, DefinitionPageMode, } from 'scenes/data-management/definition/definitionLogic' -import type { definitionEditLogicType } from './definitionEditLogicType' -import { capitalizeFirstLetter } from 'lib/utils' import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' + +import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' import { tagsModel } from '~/models/tagsModel' +import { Definition, EventDefinition, PropertyDefinition } from '~/types' + +import type { definitionEditLogicType } from './definitionEditLogicType' export interface DefinitionEditLogicProps extends DefinitionLogicProps { definition: Definition diff --git a/frontend/src/scenes/data-management/definition/definitionLogic.test.ts b/frontend/src/scenes/data-management/definition/definitionLogic.test.ts index 9b3ef5531dcaf..faea186096541 100644 --- a/frontend/src/scenes/data-management/definition/definitionLogic.test.ts +++ b/frontend/src/scenes/data-management/definition/definitionLogic.test.ts @@ -1,10 +1,11 @@ -import { createNewDefinition, definitionLogic } from 'scenes/data-management/definition/definitionLogic' -import { initKeaTests } from '~/test/init' +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' +import { createNewDefinition, definitionLogic } from 'scenes/data-management/definition/definitionLogic' +import { urls } from 'scenes/urls' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' import { mockEventDefinitions, mockEventPropertyDefinition } from '~/test/mocks' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' describe('definitionLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/data-management/definition/definitionLogic.ts b/frontend/src/scenes/data-management/definition/definitionLogic.ts index 80fae17926149..0c9a97067499c 100644 --- a/frontend/src/scenes/data-management/definition/definitionLogic.ts +++ b/frontend/src/scenes/data-management/definition/definitionLogic.ts @@ -1,15 +1,19 @@ -import { actions, afterMount, kea, key, props, path, selectors, reducers, connect } from 'kea' -import { AvailableFeature, Breadcrumb, Definition, PropertyDefinition } from '~/types' +import { actions, afterMount, connect, kea, key, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import api from 'lib/api' -import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import type { definitionLogicType } from './definitionLogicType' +import api from 'lib/api' import { getPropertyLabel } from 'lib/taxonomy' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + +import { updatePropertyDefinitions } from '~/models/propertyDefinitionsModel' +import { AvailableFeature, Breadcrumb, Definition, PropertyDefinition } from '~/types' + +import { DataManagementTab } from '../DataManagementScene' import { eventDefinitionsTableLogic } from '../events/eventDefinitionsTableLogic' import { propertyDefinitionsTableLogic } from '../properties/propertyDefinitionsTableLogic' +import type { definitionLogicType } from './definitionLogicType' export enum DefinitionPageMode { View = 'view', @@ -119,14 +123,17 @@ export const definitionLogic = kea([ (definition, isEvent): Breadcrumb[] => { return [ { + key: Scene.DataManagement, name: `Data Management`, path: isEvent ? urls.eventDefinitions() : urls.propertyDefinitions(), }, { + key: isEvent ? DataManagementTab.EventDefinitions : DataManagementTab.PropertyDefinitions, name: isEvent ? 'Events' : 'Properties', path: isEvent ? urls.eventDefinitions() : urls.propertyDefinitions(), }, { + key: definition?.id || 'new', name: definition?.id !== 'new' ? getPropertyLabel(definition?.name) || 'Untitled' : 'Untitled', }, ] diff --git a/frontend/src/scenes/data-management/events/DefinitionHeader.tsx b/frontend/src/scenes/data-management/events/DefinitionHeader.tsx index cc34e2f364f9e..cab89632b62f1 100644 --- a/frontend/src/scenes/data-management/events/DefinitionHeader.tsx +++ b/frontend/src/scenes/data-management/events/DefinitionHeader.tsx @@ -1,44 +1,36 @@ -import { EventDefinition, PropertyDefinition } from '~/types' -import { - IconAutocapture, - IconPageleave, - IconPreview, - PropertyIcon, - IconUnverifiedEvent, - IconVerifiedEvent, - VerifiedPropertyIcon, - IconSelectAll, -} from 'lib/lemon-ui/icons' -import { getKeyMapping, KEY_MAPPING } from 'lib/taxonomy' +import { IconBadge, IconBolt, IconCursor, IconEye, IconLeave, IconList, IconLogomark } from '@posthog/icons' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { TaxonomicFilterGroup, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import clsx from 'clsx' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' import { eventTaxonomicGroupProps, propertyTaxonomicGroupProps, } from 'lib/components/TaxonomicFilter/taxonomicFilterLogic' +import { TaxonomicFilterGroup, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { IconSelectAll } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { getKeyMapping, KEY_MAPPING } from 'lib/taxonomy' +import { urls } from 'scenes/urls' + +import { EventDefinition, PropertyDefinition } from '~/types' export function getPropertyDefinitionIcon(definition: PropertyDefinition): JSX.Element { if (KEY_MAPPING.event[definition.name]) { return ( - + ) } if (definition.verified) { return ( - + ) } return ( - + ) } @@ -47,29 +39,32 @@ export function getEventDefinitionIcon(definition: EventDefinition & { value: st // Rest are events if (definition.name === '$pageview' || definition.name === '$screen') { return ( - - + + ) } if (definition.name === '$pageleave') { return ( - + ) } if (definition.name === '$autocapture') { + return + } + if (definition.name && definition.verified) { return ( - - + + ) } - if (definition.name && (definition.verified || !!KEY_MAPPING.event[definition.name])) { + if (definition.name && !!KEY_MAPPING.event[definition.name]) { return ( - - + + ) } @@ -81,8 +76,8 @@ export function getEventDefinitionIcon(definition: EventDefinition & { value: st ) } return ( - - + + ) } @@ -108,7 +103,7 @@ function RawDefinitionHeader({ const isLink = asLink && fullDetailUrl const innerContent = ( - + ) @@ -127,7 +122,27 @@ function RawDefinitionHeader({ {!hideIcon && icon &&

    {icon}
    } {!hideText && (
    -
    {linkedInnerContent}
    +
    + {linkedInnerContent} + {definition.verified && ( + <> + + + + + )} + {!!KEY_MAPPING.event[definition.name] && ( + + + + )} +
    {description ?
    {description}
    : null}
    )} diff --git a/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx b/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx index 70a5b6a88a8e8..109afbe5a2c78 100644 --- a/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx +++ b/frontend/src/scenes/data-management/events/EventDefinitionProperties.tsx @@ -1,14 +1,13 @@ import { useActions, useValues } from 'kea' -import { useEffect } from 'react' -import { - eventDefinitionsTableLogic, - PROPERTY_DEFINITIONS_PER_EVENT, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' -import { EventDefinition, PropertyDefinition } from '~/types' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { organizationLogic } from 'scenes/organizationLogic' +import { PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { useEffect } from 'react' import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { organizationLogic } from 'scenes/organizationLogic' + +import { EventDefinition, PropertyDefinition } from '~/types' export function EventDefinitionProperties({ definition }: { definition: EventDefinition }): JSX.Element { const { loadPropertiesForEvent } = useActions(eventDefinitionsTableLogic) diff --git a/frontend/src/scenes/data-management/events/EventDefinitionsTable.scss b/frontend/src/scenes/data-management/events/EventDefinitionsTable.scss index b5054393c9116..9cc22ad4acc2a 100644 --- a/frontend/src/scenes/data-management/events/EventDefinitionsTable.scss +++ b/frontend/src/scenes/data-management/events/EventDefinitionsTable.scss @@ -1,6 +1,7 @@ .events-definition-table { .LemonTable__content > table > tbody { td.definition-column-icon { + padding-right: 0.5rem; width: 36px; .definition-column-name-icon { @@ -8,7 +9,7 @@ align-items: center; justify-content: center; width: 30px; - font-size: 1.5rem; + font-size: 1.2rem; svg.taxonomy-icon { flex-shrink: 0; @@ -37,19 +38,15 @@ justify-content: center; .definition-column-name-content-title { + align-items: center; + display: flex; font-weight: 600; - cursor: pointer; - position: relative; + gap: 0.25rem; overflow: visible; + position: relative; - &:before { - content: ''; - position: absolute; - top: -5px; - bottom: -5px; - left: -10px; - right: -50px; - height: 22px; + svg { + color: var(--success); } } } @@ -81,6 +78,7 @@ .LemonTable__expansion { .event-properties-wrapper { padding: 1rem 0 1rem 1rem; + .event-properties-header { font-weight: 600; } diff --git a/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx b/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx index 7571d91bf5731..4b89be432b41a 100644 --- a/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx +++ b/frontend/src/scenes/data-management/events/EventDefinitionsTable.tsx @@ -1,22 +1,22 @@ import './EventDefinitionsTable.scss' + +import { LemonButton, LemonInput, LemonSelect, LemonSelectOptions, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { EventDefinition, EventDefinitionType } from '~/types' -import { - EVENT_DEFINITIONS_PER_PAGE, - eventDefinitionsTableLogic, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { combineUrl } from 'kea-router' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { organizationLogic } from 'scenes/organizationLogic' +import { TZLabel } from 'lib/components/TZLabel' +import { EVENT_DEFINITIONS_PER_PAGE } from 'lib/constants' +import { IconPlayCircle } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { EventDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' import { EventDefinitionProperties } from 'scenes/data-management/events/EventDefinitionProperties' -import { LemonButton, LemonInput, LemonSelect, LemonSelectOptions, Link } from '@posthog/lemon-ui' -import { More } from 'lib/lemon-ui/LemonButton/More' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { organizationLogic } from 'scenes/organizationLogic' import { urls } from 'scenes/urls' -import { combineUrl } from 'kea-router' -import { IconPlayCircle } from 'lib/lemon-ui/icons' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { TZLabel } from 'lib/components/TZLabel' + +import { EventDefinition, EventDefinitionType } from '~/types' const eventTypeOptions: LemonSelectOptions = [ { value: EventDefinitionType.Event, label: 'All events', 'data-attr': 'event-type-option-event' }, diff --git a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts index 991d4c0b7d5ff..edceacb2d38d4 100644 --- a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts +++ b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.test.ts @@ -1,17 +1,15 @@ -import { initKeaTests } from '~/test/init' -import { - EVENT_DEFINITIONS_PER_PAGE, - eventDefinitionsTableLogic, - PROPERTY_DEFINITIONS_PER_EVENT, -} from 'scenes/data-management/events/eventDefinitionsTableLogic' -import { api, MOCK_TEAM_ID } from 'lib/api.mock' -import { expectLogic, partial } from 'kea-test-utils' -import { mockEvent, mockEventDefinitions, mockEventPropertyDefinitions } from '~/test/mocks' -import { useMocks } from '~/mocks/jest' -import { organizationLogic } from 'scenes/organizationLogic' import { combineUrl, router } from 'kea-router' +import { expectLogic, partial } from 'kea-test-utils' +import { api, MOCK_TEAM_ID } from 'lib/api.mock' +import { EVENT_DEFINITIONS_PER_PAGE, PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' import { keyMappingKeys } from 'lib/taxonomy' +import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic' +import { organizationLogic } from 'scenes/organizationLogic' import { urls } from 'scenes/urls' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { mockEvent, mockEventDefinitions, mockEventPropertyDefinitions } from '~/test/mocks' import { EventDefinitionType } from '~/types' describe('eventDefinitionsTableLogic', () => { diff --git a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts index 58ce1a766852c..4c7fd59120ae3 100644 --- a/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts +++ b/frontend/src/scenes/data-management/events/eventDefinitionsTableLogic.ts @@ -1,12 +1,16 @@ import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { AnyPropertyFilter, EventDefinitionType, EventDefinition, PropertyDefinition } from '~/types' -import type { eventDefinitionsTableLogicType } from './eventDefinitionsTableLogicType' +import { loaders } from 'kea-loaders' +import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' import api, { PaginatedResponse } from 'lib/api' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' +import { EVENT_DEFINITIONS_PER_PAGE, PROPERTY_DEFINITIONS_PER_EVENT } from 'lib/constants' import { keyMappingKeys } from 'lib/taxonomy' -import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' -import { convertPropertyGroupToProperties, objectsEqual } from 'lib/utils' +import { objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { loaders } from 'kea-loaders' + +import { AnyPropertyFilter, EventDefinition, EventDefinitionType, PropertyDefinition } from '~/types' + +import type { eventDefinitionsTableLogicType } from './eventDefinitionsTableLogicType' export interface EventDefinitionsPaginatedResponse extends PaginatedResponse { current?: string @@ -37,9 +41,6 @@ function cleanFilters(filter: Partial): Filters { } } -export const EVENT_DEFINITIONS_PER_PAGE = 50 -export const PROPERTY_DEFINITIONS_PER_EVENT = 5 - export function createDefinitionKey(event?: EventDefinition, property?: PropertyDefinition): string { return `${event?.id ?? 'event'}-${property?.id ?? 'property'}` } @@ -103,7 +104,7 @@ export const eventDefinitionsTableLogic = kea([ }), reducers({ filters: [ - cleanFilters({}) as Filters, + cleanFilters({}), { setFilters: (state, { filters }) => ({ ...state, diff --git a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx index 1150d65f52f36..9f7d337b8db58 100644 --- a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx +++ b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx @@ -1,13 +1,15 @@ import { useValues } from 'kea' -import { urls } from 'scenes/urls' -import { IngestionWarning, ingestionWarningsLogic, IngestionWarningSummary } from './ingestionWarningsLogic' -import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { ReadingHog } from 'lib/components/hedgehogs' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' import { TZLabel } from 'lib/components/TZLabel' -import { Link } from 'lib/lemon-ui/Link' +import { LemonTable } from 'lib/lemon-ui/LemonTable' import { TableCellSparkline } from 'lib/lemon-ui/LemonTable/TableCellSparkline' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { Link } from 'lib/lemon-ui/Link' +import { urls } from 'scenes/urls' + import { ProductKey } from '~/types' -import { ReadingHog } from 'lib/components/hedgehogs' + +import { IngestionWarning, ingestionWarningsLogic, IngestionWarningSummary } from './ingestionWarningsLogic' const WARNING_TYPE_TO_DESCRIPTION = { cannot_merge_already_identified: 'Refused to merge an already identified user', diff --git a/frontend/src/scenes/data-management/ingestion-warnings/ingestionWarningsLogic.ts b/frontend/src/scenes/data-management/ingestion-warnings/ingestionWarningsLogic.ts index c3509561e1a42..587305ff6d482 100644 --- a/frontend/src/scenes/data-management/ingestion-warnings/ingestionWarningsLogic.ts +++ b/frontend/src/scenes/data-management/ingestion-warnings/ingestionWarningsLogic.ts @@ -1,13 +1,16 @@ -import { kea, connect, path, selectors, afterMount } from 'kea' +import { afterMount, connect, kea, path, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { Breadcrumb } from '~/types' -import { urls } from 'scenes/urls' import api from 'lib/api' +import { dayjs, dayjsUtcToTimezone } from 'lib/dayjs' +import { range } from 'lib/utils' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb } from '~/types' -import type { ingestionWarningsLogicType } from './ingestionWarningsLogicType' import { teamLogic } from '../../teamLogic' -import { range } from 'lib/utils' -import { dayjs, dayjsUtcToTimezone } from 'lib/dayjs' +import { DataManagementTab } from '../DataManagementScene' +import type { ingestionWarningsLogicType } from './ingestionWarningsLogicType' export interface IngestionWarningSummary { type: string @@ -47,11 +50,13 @@ export const ingestionWarningsLogic = kea([ (): Breadcrumb[] => { return [ { - name: `Data Management`, + key: Scene.DataManagement, + name: `Data management`, path: urls.eventDefinitions(), }, { - name: 'Ingestion Warnings', + key: DataManagementTab.IngestionWarnings, + name: 'Ingestion warnings', path: urls.ingestionWarnings(), }, ] diff --git a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.scss b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.scss index 5a697e0c2c1ca..68dfef351b3c0 100644 --- a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.scss +++ b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.scss @@ -1,6 +1,7 @@ .event-properties-definition-table { .LemonTable__content > table > tbody { td.definition-column-icon { + padding-right: 0.5rem; width: 36px; .definition-column-name-icon { @@ -8,6 +9,7 @@ align-items: center; justify-content: center; width: 30px; + font-size: 1.2rem; svg.taxonomy-icon { flex-shrink: 0; @@ -32,17 +34,14 @@ justify-content: center; .definition-column-name-content-title { - font-weight: 600; + align-items: center; cursor: pointer; + display: flex; + font-weight: 600; + gap: 0.25rem; - &:before { - content: ''; - position: absolute; - top: -5px; - //bottom: -5px; - left: -10px; - right: -50px; - height: 22px; + svg { + color: var(--success); } } } diff --git a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx index 19a9d8429a0db..74396c36e63f7 100644 --- a/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx +++ b/frontend/src/scenes/data-management/properties/PropertyDefinitionsTable.tsx @@ -1,18 +1,18 @@ import './PropertyDefinitionsTable.scss' + +import { LemonInput, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { PropertyDefinition } from '~/types' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { organizationLogic } from 'scenes/organizationLogic' -import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' -import { - EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - propertyDefinitionsTableLogic, -} from 'scenes/data-management/properties/propertyDefinitionsTableLogic' -import { LemonInput, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader' +import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' +import { organizationLogic } from 'scenes/organizationLogic' import { urls } from 'scenes/urls' +import { PropertyDefinition } from '~/types' + export function PropertyDefinitionsTable(): JSX.Element { const { propertyDefinitions, propertyDefinitionsLoading, filters, propertyTypeOptions } = useValues(propertyDefinitionsTableLogic) diff --git a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts index 97f6abf271d0b..1e50b74a467b3 100644 --- a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts +++ b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.test.ts @@ -1,16 +1,15 @@ -import { initKeaTests } from '~/test/init' -import { api, MOCK_GROUP_TYPES, MOCK_TEAM_ID } from 'lib/api.mock' +import { combineUrl, router } from 'kea-router' import { expectLogic, partial } from 'kea-test-utils' -import { mockEventPropertyDefinitions } from '~/test/mocks' -import { useMocks } from '~/mocks/jest' +import { api, MOCK_GROUP_TYPES, MOCK_TEAM_ID } from 'lib/api.mock' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' +import { propertyDefinitionsTableLogic } from 'scenes/data-management/properties/propertyDefinitionsTableLogic' import { organizationLogic } from 'scenes/organizationLogic' -import { combineUrl, router } from 'kea-router' -import { - EVENT_PROPERTY_DEFINITIONS_PER_PAGE, - propertyDefinitionsTableLogic, -} from 'scenes/data-management/properties/propertyDefinitionsTableLogic' import { urls } from 'scenes/urls' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { mockEventPropertyDefinitions } from '~/test/mocks' + describe('propertyDefinitionsTableLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts index f83419217e84b..a9a1541df21f8 100644 --- a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts +++ b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts @@ -1,19 +1,21 @@ import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { PropertyDefinition } from '~/types' -import api from 'lib/api' +import { loaders } from 'kea-loaders' import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' +import api from 'lib/api' +import { EVENT_PROPERTY_DEFINITIONS_PER_PAGE } from 'lib/constants' +import { LemonSelectOption } from 'lib/lemon-ui/LemonSelect' +import { capitalizeFirstLetter, objectsEqual } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { normalizePropertyDefinitionEndpointUrl, PropertyDefinitionsPaginatedResponse, } from 'scenes/data-management/events/eventDefinitionsTableLogic' -import { capitalizeFirstLetter, objectsEqual } from 'lib/utils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { loaders } from 'kea-loaders' import { urls } from 'scenes/urls' -import type { propertyDefinitionsTableLogicType } from './propertyDefinitionsTableLogicType' +import { PropertyDefinition } from '~/types' + import { groupsModel } from '../../../models/groupsModel' -import { LemonSelectOption } from 'lib/lemon-ui/LemonSelect' +import type { propertyDefinitionsTableLogicType } from './propertyDefinitionsTableLogicType' export interface Filters { property: string @@ -38,8 +40,6 @@ function removeDefaults(filter: Filters): Partial { } } -export const EVENT_PROPERTY_DEFINITIONS_PER_PAGE = 50 - export interface PropertyDefinitionsTableLogicProps { key: string } @@ -193,7 +193,7 @@ export const propertyDefinitionsTableLogic = kea { const [type, index] = propertyType.split('::') actions.setFilters({ - type: type as string, + type: type, group_type_index: index ? +index : null, }) }, diff --git a/frontend/src/scenes/data-warehouse/DataWarehousePageTabs.tsx b/frontend/src/scenes/data-warehouse/DataWarehousePageTabs.tsx index 06aaec8915831..cba833ea548ab 100644 --- a/frontend/src/scenes/data-warehouse/DataWarehousePageTabs.tsx +++ b/frontend/src/scenes/data-warehouse/DataWarehousePageTabs.tsx @@ -1,11 +1,11 @@ +import { actions, kea, path, reducers, useActions, useValues } from 'kea' import { actionToUrl, urlToAction } from 'kea-router' -import { kea, useActions, useValues, path, actions, reducers } from 'kea' -import { urls } from 'scenes/urls' +import { FEATURE_FLAGS } from 'lib/constants' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { urls } from 'scenes/urls' import type { dataWarehouseTabsLogicType } from './DataWarehousePageTabsType' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' export enum DataWarehouseTab { Posthog = 'posthog', diff --git a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx index 9d6c253a1cb78..291eaf6836d6e 100644 --- a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx +++ b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx @@ -1,10 +1,11 @@ import './ViewLinkModal.scss' import { LemonButton, LemonDivider, LemonModal, LemonSelect, LemonTag } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { Field, Form } from 'kea-forms' import { IconDelete, IconSwapHoriz } from 'lib/lemon-ui/icons' import { viewLinkLogic } from 'scenes/data-warehouse/viewLinkLogic' -import { Form, Field } from 'kea-forms' -import { useActions, useValues } from 'kea' + import { DatabaseSchemaQueryResponseField } from '~/queries/schema' export function ViewLinkModal({ tableSelectable }: { tableSelectable: boolean }): JSX.Element { diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx index 4f7133fff9e4e..fd5889be6fd6d 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx @@ -1,18 +1,20 @@ -import { LemonTag, Link, LemonButtonWithSideAction, LemonButton } from '@posthog/lemon-ui' -import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' +import { LemonButton, LemonButtonWithSideAction, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { router } from 'kea-router' +import { PageHeader } from 'lib/components/PageHeader' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { FEATURE_FLAGS } from 'lib/constants' +import { IconSettings } from 'lib/lemon-ui/icons' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { ProductKey } from '~/types' -import { DataWarehouseTablesContainer } from './DataWarehouseTables' -import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' + import { DataWarehousePageTabs, DataWarehouseTab } from '../DataWarehousePageTabs' +import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' +import { DataWarehouseTablesContainer } from './DataWarehouseTables' import SourceModal from './SourceModal' -import { IconSettings } from 'lib/lemon-ui/icons' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' export const scene: SceneExport = { component: DataWarehouseExternalScene, diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx index 7f0a7044b150c..35b3270e71516 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx @@ -1,12 +1,13 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { DatabaseTables } from 'scenes/data-management/database/DatabaseTables' -import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' -import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonButton } from '@posthog/lemon-ui' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' +import { DatabaseTables } from 'scenes/data-management/database/DatabaseTables' import { teamLogic } from 'scenes/teamLogic' + import { DataWarehouseSceneRow } from '../types' +import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' export function DataWarehouseTablesContainer(): JSX.Element { const { tables, dataWarehouseLoading } = useValues(dataWarehouseSceneLogic) @@ -45,7 +46,7 @@ export function DataWarehouseTablesContainer(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/warehouse_tables`, object: { name: warehouseTable.name, id: warehouseTable.id }, callback: loadDataWarehouse, diff --git a/frontend/src/scenes/data-warehouse/external/SourceModal.tsx b/frontend/src/scenes/data-warehouse/external/SourceModal.tsx index 2308ec31b3140..4b8a98e04cc29 100644 --- a/frontend/src/scenes/data-warehouse/external/SourceModal.tsx +++ b/frontend/src/scenes/data-warehouse/external/SourceModal.tsx @@ -1,11 +1,12 @@ import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonModalProps } from '@posthog/lemon-ui' -import { Form } from 'kea-forms' -import { ConnectorConfigType, sourceModalLogic } from './sourceModalLogic' import { useActions, useValues } from 'kea' -import { DatawarehouseTableForm } from '../new_table/DataWarehouseTableForm' +import { Form } from 'kea-forms' import { Field } from 'lib/forms/Field' import stripeLogo from 'public/stripe-logo.svg' +import { DatawarehouseTableForm } from '../new_table/DataWarehouseTableForm' +import { ConnectorConfigType, sourceModalLogic } from './sourceModalLogic' + interface SourceModalProps extends LemonModalProps {} export default function SourceModal(props: SourceModalProps): JSX.Element { diff --git a/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.tsx b/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.tsx index 238945ac9f5b9..13ebc2dda9f32 100644 --- a/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.tsx +++ b/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.tsx @@ -1,11 +1,12 @@ import { actions, afterMount, connect, kea, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api, { PaginatedResponse } from 'lib/api' -import { DataWarehouseTable, ProductKey } from '~/types' import { userLogic } from 'scenes/userLogic' -import type { dataWarehouseSceneLogicType } from './dataWarehouseSceneLogicType' +import { DataWarehouseTable, ProductKey } from '~/types' + import { DataWarehouseSceneRow } from '../types' +import type { dataWarehouseSceneLogicType } from './dataWarehouseSceneLogicType' export const dataWarehouseSceneLogic = kea([ path(['scenes', 'warehouse', 'dataWarehouseSceneLogic']), diff --git a/frontend/src/scenes/data-warehouse/external/sourceModalLogic.ts b/frontend/src/scenes/data-warehouse/external/sourceModalLogic.ts index 398db7f8247a3..2f616fe91c1af 100644 --- a/frontend/src/scenes/data-warehouse/external/sourceModalLogic.ts +++ b/frontend/src/scenes/data-warehouse/external/sourceModalLogic.ts @@ -1,15 +1,16 @@ -import { actions, connect, kea, path, reducers, selectors, listeners } from 'kea' - -import type { sourceModalLogicType } from './sourceModalLogicType' -import { forms } from 'kea-forms' -import { ExternalDataStripeSourceCreatePayload } from '~/types' -import api from 'lib/api' import { lemonToast } from '@posthog/lemon-ui' -import { dataWarehouseTableLogic } from '../new_table/dataWarehouseTableLogic' -import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' import { router } from 'kea-router' +import api from 'lib/api' import { urls } from 'scenes/urls' + +import { ExternalDataStripeSourceCreatePayload } from '~/types' + +import { dataWarehouseTableLogic } from '../new_table/dataWarehouseTableLogic' import { dataWarehouseSettingsLogic } from '../settings/dataWarehouseSettingsLogic' +import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic' +import type { sourceModalLogicType } from './sourceModalLogicType' export interface ConnectorConfigType { name: string diff --git a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx index 25f593df020db..ae6d5120d8316 100644 --- a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx +++ b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableForm.tsx @@ -1,8 +1,9 @@ -import { dataWarehouseTableLogic } from './dataWarehouseTableLogic' -import { Form } from 'kea-forms' import { LemonInput, LemonSelect } from '@posthog/lemon-ui' +import { Form } from 'kea-forms' import { Field } from 'lib/forms/Field' +import { dataWarehouseTableLogic } from './dataWarehouseTableLogic' + interface DataWarehouseTableFormProps { footer?: JSX.Element } diff --git a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableScene.tsx b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableScene.tsx index e7c2fbf15e94a..f547d46cd7d1e 100644 --- a/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableScene.tsx +++ b/frontend/src/scenes/data-warehouse/new_table/DataWarehouseTableScene.tsx @@ -1,11 +1,12 @@ -import { dataWarehouseTableLogic } from './dataWarehouseTableLogic' import { LemonButton, Link } from '@posthog/lemon-ui' -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' import { useActions, useValues } from 'kea' import { router } from 'kea-router' +import { PageHeader } from 'lib/components/PageHeader' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + import { DatawarehouseTableForm } from './DataWarehouseTableForm' +import { dataWarehouseTableLogic } from './dataWarehouseTableLogic' export const scene: SceneExport = { component: DataWarehouseTable, diff --git a/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx b/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx index 910336cef0de6..88ab7dfa92962 100644 --- a/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx +++ b/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx @@ -1,18 +1,22 @@ import { lemonToast } from '@posthog/lemon-ui' -import { kea, path, props, listeners, reducers, actions, selectors, connect } from 'kea' +import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { router } from 'kea-router' import api from 'lib/api' +import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { AnyPropertyFilter, Breadcrumb, DataWarehouseTable } from '~/types' + import { DataTableNode } from '~/queries/schema' -import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' -import type { dataWarehouseTableLogicType } from './dataWarehouseTableLogicType' +import { AnyPropertyFilter, Breadcrumb, DataWarehouseTable } from '~/types' + import { dataWarehouseSceneLogic } from '../external/dataWarehouseSceneLogic' +import type { dataWarehouseTableLogicType } from './dataWarehouseTableLogicType' export interface TableLogicProps { - id: string | 'new' + /** A UUID or 'new'. */ + id: string } const NEW_WAREHOUSE_TABLE: DataWarehouseTable = { @@ -99,10 +103,12 @@ export const dataWarehouseTableLogic = kea([ () => [], (): Breadcrumb[] => [ { - name: `Data Warehouse`, + key: Scene.DataWarehouse, + name: `Data warehouse`, path: urls.dataWarehouseExternal(), }, { + key: 'new', name: 'New', }, ], diff --git a/frontend/src/scenes/data-warehouse/posthog/DataWarehousePosthogScene.tsx b/frontend/src/scenes/data-warehouse/posthog/DataWarehousePosthogScene.tsx index 6e91637178af1..8d181bf41b915 100644 --- a/frontend/src/scenes/data-warehouse/posthog/DataWarehousePosthogScene.tsx +++ b/frontend/src/scenes/data-warehouse/posthog/DataWarehousePosthogScene.tsx @@ -1,14 +1,15 @@ import { LemonButton, LemonTag, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' -import { DataWarehousePageTabs, DataWarehouseTab } from '../DataWarehousePageTabs' import { DatabaseTablesContainer } from 'scenes/data-management/database/DatabaseTables' -import { ViewLinkModal } from '../ViewLinkModal' -import { useActions, useValues } from 'kea' +import { SceneExport } from 'scenes/sceneTypes' + +import { DataWarehousePageTabs, DataWarehouseTab } from '../DataWarehousePageTabs' import { viewLinkLogic } from '../viewLinkLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import { ViewLinkModal } from '../ViewLinkModal' export const scene: SceneExport = { component: DataWarehousePosthogScene, diff --git a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx index 236486c58dd6d..75141f82ec1c1 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx @@ -1,15 +1,17 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { DatabaseTables } from 'scenes/data-management/database/DatabaseTables' -import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' +import { router } from 'kea-router' import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonButton, Link } from '@posthog/lemon-ui' -import { deleteWithUndo } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable' +import { DatabaseTables } from 'scenes/data-management/database/DatabaseTables' import { teamLogic } from 'scenes/teamLogic' -import { DataWarehouseSceneRow } from '../types' -import { dataWarehouseSavedQueriesLogic } from './dataWarehouseSavedQueriesLogic' import { urls } from 'scenes/urls' + import { DataTableNode, HogQLQuery, NodeKind } from '~/queries/schema' -import { router } from 'kea-router' + +import { DataWarehouseSceneRow } from '../types' +import { dataWarehouseSavedQueriesLogic } from './dataWarehouseSavedQueriesLogic' export function DataWarehouseSavedQueriesContainer(): JSX.Element { const { savedQueries, dataWarehouseSavedQueriesLoading } = useValues(dataWarehouseSavedQueriesLogic) @@ -67,7 +69,7 @@ export function DataWarehouseSavedQueriesContainer(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/warehouse_saved_queries`, object: { name: warehouseView.name, id: warehouseView.id }, callback: loadDataWarehouseSavedQueries, diff --git a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesScene.tsx b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesScene.tsx index eb78288c85efb..0b630fd12efa8 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesScene.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesScene.tsx @@ -1,17 +1,19 @@ import { LemonButton, LemonTag, Link } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { router } from 'kea-router' import { PageHeader } from 'lib/components/PageHeader' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + +import { Error404 } from '~/layout/Error404' +import { ProductKey } from '~/types' + import { DataWarehousePageTabs, DataWarehouseTab } from '../DataWarehousePageTabs' -import { dataWarehouseSavedQueriesLogic } from './dataWarehouseSavedQueriesLogic' import { DataWarehouseSavedQueriesContainer } from './DataWarehouseSavedQueriesContainer' -import { useValues } from 'kea' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' -import { router } from 'kea-router' -import { ProductKey } from '~/types' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { Error404 } from '~/layout/Error404' +import { dataWarehouseSavedQueriesLogic } from './dataWarehouseSavedQueriesLogic' export const scene: SceneExport = { component: DataWarehouseSavedQueriesScene, diff --git a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx index f8776ffbd0e7b..1fe221605f0c0 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx @@ -1,14 +1,14 @@ import { afterMount, connect, kea, listeners, path, selectors } from 'kea' import { loaders } from 'kea-loaders' +import { router } from 'kea-router' import api, { PaginatedResponse } from 'lib/api' -import { DataWarehouseSavedQuery, ProductKey } from '~/types' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { DataWarehouseSceneRow } from '../types' +import { DataWarehouseSavedQuery, ProductKey } from '~/types' +import { DataWarehouseSceneRow } from '../types' import type { dataWarehouseSavedQueriesLogicType } from './dataWarehouseSavedQueriesLogicType' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' export const dataWarehouseSavedQueriesLogic = kea([ path(['scenes', 'warehouse', 'dataWarehouseSavedQueriesLogic']), diff --git a/frontend/src/scenes/data-warehouse/settings/DataWarehouseSettingsScene.tsx b/frontend/src/scenes/data-warehouse/settings/DataWarehouseSettingsScene.tsx index eb158c2f97c52..45f8888958773 100644 --- a/frontend/src/scenes/data-warehouse/settings/DataWarehouseSettingsScene.tsx +++ b/frontend/src/scenes/data-warehouse/settings/DataWarehouseSettingsScene.tsx @@ -1,13 +1,14 @@ import { LemonButton, LemonTable, LemonTag, Spinner } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' +import { FEATURE_FLAGS } from 'lib/constants' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { SceneExport } from 'scenes/sceneTypes' -import { dataWarehouseSettingsLogic } from './dataWarehouseSettingsLogic' -import { useActions, useValues } from 'kea' + import { dataWarehouseSceneLogic } from '../external/dataWarehouseSceneLogic' import SourceModal from '../external/SourceModal' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { More } from 'lib/lemon-ui/LemonButton/More' +import { dataWarehouseSettingsLogic } from './dataWarehouseSettingsLogic' export const scene: SceneExport = { component: DataWarehouseSettingsScene, diff --git a/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts b/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts index a7c40b36401b3..00c4db0b08f30 100644 --- a/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts +++ b/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts @@ -1,11 +1,13 @@ import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' - -import type { dataWarehouseSettingsLogicType } from './dataWarehouseSettingsLogicType' import { loaders } from 'kea-loaders' import api, { PaginatedResponse } from 'lib/api' -import { ExternalDataStripeSource, Breadcrumb } from '~/types' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { Breadcrumb, ExternalDataStripeSource } from '~/types' + +import type { dataWarehouseSettingsLogicType } from './dataWarehouseSettingsLogicType' + export interface DataWarehouseSource {} export const dataWarehouseSettingsLogic = kea([ @@ -49,10 +51,12 @@ export const dataWarehouseSettingsLogic = kea([ () => [], (): Breadcrumb[] => [ { + key: Scene.DataWarehouse, name: `Data Warehouse`, path: urls.dataWarehouseExternal(), }, { + key: Scene.DataWarehouseSettings, name: 'Data Warehouse Settings', path: urls.dataWarehouseSettings(), }, diff --git a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx index 9e867642eed89..c8f4bafc4999d 100644 --- a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx +++ b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx @@ -1,12 +1,14 @@ -import { actions, connect, kea, selectors, listeners, reducers, path, afterMount } from 'kea' -import { dataWarehouseSavedQueriesLogic } from './saved_queries/dataWarehouseSavedQueriesLogic' -import { DataWarehouseSceneRow } from './types' -import { DataWarehouseViewLink } from '~/types' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { forms } from 'kea-forms' -import api from 'lib/api' -import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' import { loaders } from 'kea-loaders' +import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' + +import { DataWarehouseViewLink } from '~/types' + +import { dataWarehouseSavedQueriesLogic } from './saved_queries/dataWarehouseSavedQueriesLogic' +import { DataWarehouseSceneRow } from './types' import type { viewLinkLogicType } from './viewLinkLogicType' import { ViewLinkKeyLabel } from './ViewLinkModal' diff --git a/frontend/src/scenes/debug/DebugScene.tsx b/frontend/src/scenes/debug/DebugScene.tsx index 19cba38cb9d5e..018f72067e2e2 100644 --- a/frontend/src/scenes/debug/DebugScene.tsx +++ b/frontend/src/scenes/debug/DebugScene.tsx @@ -1,14 +1,16 @@ -import { debugSceneLogic } from './debugSceneLogic' -import { SceneExport } from 'scenes/sceneTypes' -import { PageHeader } from 'lib/components/PageHeader' -import { Query } from '~/queries/Query/Query' import { useActions, useValues } from 'kea' -import { stringifiedExamples } from '~/queries/examples' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { PageHeader } from 'lib/components/PageHeader' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { HogQLQuery } from '~/queries/schema' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { HogQLDebug } from 'scenes/debug/HogQLDebug' +import { SceneExport } from 'scenes/sceneTypes' + +import { stringifiedExamples } from '~/queries/examples' +import { Query } from '~/queries/Query/Query' +import { HogQLQuery } from '~/queries/schema' + +import { debugSceneLogic } from './debugSceneLogic' interface QueryDebugProps { queryKey: string diff --git a/frontend/src/scenes/debug/HogQLDebug.tsx b/frontend/src/scenes/debug/HogQLDebug.tsx index 21cc7b0b13104..c32a4328cb3b8 100644 --- a/frontend/src/scenes/debug/HogQLDebug.tsx +++ b/frontend/src/scenes/debug/HogQLDebug.tsx @@ -1,15 +1,16 @@ -import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor' -import { DataNode, HogQLQuery, HogQLQueryResponse } from '~/queries/schema' -import { DateRange } from '~/queries/nodes/DataNode/DateRange' -import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' import { BindLogic, useValues } from 'kea' -import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' -import { ElapsedTime, Timings } from '~/queries/nodes/DataNode/ElapsedTime' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { CodeEditor } from 'lib/components/CodeEditors' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { LemonLabel } from 'lib/lemon-ui/LemonLabel' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' + +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' +import { DateRange } from '~/queries/nodes/DataNode/DateRange' +import { ElapsedTime, Timings } from '~/queries/nodes/DataNode/ElapsedTime' import { Reload } from '~/queries/nodes/DataNode/Reload' +import { EventPropertyFilters } from '~/queries/nodes/EventsNode/EventPropertyFilters' +import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor' +import { DataNode, HogQLQuery, HogQLQueryResponse } from '~/queries/schema' interface HogQLDebugProps { queryKey: string @@ -80,7 +81,25 @@ export function HogQLDebug({ query, setQuery, queryKey }: HogQLDebugProps): JSX. } value={query.modifiers?.inCohortVia ?? response?.modifiers?.inCohortVia} /> - {' '} + + + Materialization Mode: + + setQuery({ + ...query, + modifiers: { ...query.modifiers, materializationMode: value }, + } as HogQLQuery) + } + value={query.modifiers?.materializationMode ?? response?.modifiers?.materializationMode} + /> +
    {dataLoading ? ( <> diff --git a/frontend/src/scenes/debug/debugSceneLogic.ts b/frontend/src/scenes/debug/debugSceneLogic.ts index 912474eac6907..4191584f69402 100644 --- a/frontend/src/scenes/debug/debugSceneLogic.ts +++ b/frontend/src/scenes/debug/debugSceneLogic.ts @@ -1,10 +1,11 @@ import { actions, kea, path, reducers } from 'kea' - -import type { debugSceneLogicType } from './debugSceneLogicType' import { actionToUrl, urlToAction } from 'kea-router' import { urls } from 'scenes/urls' + import { stringifiedExamples } from '~/queries/examples' +import type { debugSceneLogicType } from './debugSceneLogicType' + const DEFAULT_QUERY: string = stringifiedExamples['HogQLRaw'] export const debugSceneLogic = kea([ diff --git a/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx b/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx index 959d57825aae5..adfaae1895f33 100644 --- a/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx +++ b/frontend/src/scenes/early-access-features/EarlyAccessFeature.tsx @@ -1,10 +1,24 @@ import { LemonButton, LemonDivider, LemonInput, LemonSkeleton, LemonTag, LemonTextArea, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' +import { Form } from 'kea-forms' +import { router } from 'kea-router' +import { FlagSelector } from 'lib/components/FlagSelector' +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 { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { personsLogic, PersonsLogicProps } from 'scenes/persons/personsLogic' +import { PersonsSearch } from 'scenes/persons/PersonsSearch' +import { PersonsTable } from 'scenes/persons/PersonsTable' import { SceneExport } from 'scenes/sceneTypes' -import { earlyAccessFeatureLogic } from './earlyAccessFeatureLogic' -import { Form } from 'kea-forms' +import { urls } from 'scenes/urls' + import { EarlyAccessFeatureStage, EarlyAccessFeatureTabs, @@ -13,21 +27,9 @@ import { PropertyFilterType, PropertyOperator, } from '~/types' -import { urls } from 'scenes/urls' -import { IconClose, IconFlag, IconHelpOutline } from 'lib/lemon-ui/icons' -import { router } from 'kea-router' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' -import { personsLogic, PersonsLogicProps } from 'scenes/persons/personsLogic' -import clsx from 'clsx' + +import { earlyAccessFeatureLogic } from './earlyAccessFeatureLogic' import { InstructionsModal } from './InstructionsModal' -import { PersonsTable } from 'scenes/persons/PersonsTable' -import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { PersonsSearch } from 'scenes/persons/PersonsSearch' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { NotFound } from 'lib/components/NotFound' -import { FlagSelector } from 'lib/components/FlagSelector' export const scene: SceneExport = { component: EarlyAccessFeature, diff --git a/frontend/src/scenes/early-access-features/EarlyAccessFeatures.stories.tsx b/frontend/src/scenes/early-access-features/EarlyAccessFeatures.stories.tsx index b340f584e6cce..81fb329a5f72d 100644 --- a/frontend/src/scenes/early-access-features/EarlyAccessFeatures.stories.tsx +++ b/frontend/src/scenes/early-access-features/EarlyAccessFeatures.stories.tsx @@ -1,8 +1,9 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' import { EarlyAccessFeatureType } from '~/types' diff --git a/frontend/src/scenes/early-access-features/EarlyAccessFeatures.tsx b/frontend/src/scenes/early-access-features/EarlyAccessFeatures.tsx index 594a986400042..79b03f9da0e5a 100644 --- a/frontend/src/scenes/early-access-features/EarlyAccessFeatures.tsx +++ b/frontend/src/scenes/early-access-features/EarlyAccessFeatures.tsx @@ -1,13 +1,15 @@ import { LemonButton, LemonTable, LemonTag, Link } from '@posthog/lemon-ui' import { useValues } from 'kea' +import { router } from 'kea-router' import { PageHeader } from 'lib/components/PageHeader' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + import { EarlyAccessFeatureType, ProductKey } from '~/types' + import { earlyAccessFeaturesLogic } from './earlyAccessFeaturesLogic' -import { userLogic } from 'scenes/userLogic' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' -import { router } from 'kea-router' export const scene: SceneExport = { component: EarlyAccessFeatures, diff --git a/frontend/src/scenes/early-access-features/InstructionsModal.tsx b/frontend/src/scenes/early-access-features/InstructionsModal.tsx index 1e76f9ba5f06e..d3cda50184d25 100644 --- a/frontend/src/scenes/early-access-features/InstructionsModal.tsx +++ b/frontend/src/scenes/early-access-features/InstructionsModal.tsx @@ -1,9 +1,10 @@ import { LemonCollapse, LemonModal, Link } from '@posthog/lemon-ui' +import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { FeatureFlagType } from '~/types' import EarlyAccessFeatureImage from 'public/early-access-feature-demo.png' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { useValues } from 'kea' + +import { FeatureFlagType } from '~/types' interface InstructionsModalProps { featureFlag: FeatureFlagType diff --git a/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts b/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts index df5aabcc583b9..cfc21f71a21fe 100644 --- a/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts +++ b/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts @@ -1,9 +1,13 @@ +import { lemonToast } from '@posthog/lemon-ui' import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' import api from 'lib/api' +import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' + import { Breadcrumb, EarlyAccessFeatureStage, @@ -11,10 +15,9 @@ import { EarlyAccessFeatureType, NewEarlyAccessFeatureType, } from '~/types' + import type { earlyAccessFeatureLogicType } from './earlyAccessFeatureLogicType' import { earlyAccessFeaturesLogic } from './earlyAccessFeaturesLogic' -import { teamLogic } from 'scenes/teamLogic' -import { lemonToast } from '@posthog/lemon-ui' export const NEW_EARLY_ACCESS_FEATURE: NewEarlyAccessFeatureType = { name: '', @@ -121,10 +124,14 @@ export const earlyAccessFeatureLogic = kea([ (s) => [s.earlyAccessFeature], (earlyAccessFeature: EarlyAccessFeatureType): Breadcrumb[] => [ { + key: Scene.EarlyAccessFeatures, name: 'Early Access Management', path: urls.earlyAccessFeatures(), }, - ...(earlyAccessFeature?.name ? [{ name: earlyAccessFeature.name }] : []), + { + key: earlyAccessFeature.id || 'new', + name: earlyAccessFeature.name, + }, ], ], }), @@ -170,9 +177,9 @@ export const earlyAccessFeatureLogic = kea([ } }, })), - afterMount(async ({ props, actions }) => { + afterMount(({ props, actions }) => { if (props.id !== 'new') { - await actions.loadEarlyAccessFeature() + actions.loadEarlyAccessFeature() } }), ]) diff --git a/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts b/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts index 707a5df337acc..25fc3741bb9f6 100644 --- a/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts +++ b/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts @@ -1,10 +1,12 @@ import { afterMount, kea, path, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { Breadcrumb, EarlyAccessFeatureType } from '~/types' import type { earlyAccessFeaturesLogicType } from './earlyAccessFeaturesLogicType' -import { urls } from 'scenes/urls' export const earlyAccessFeaturesLogic = kea([ path(['scenes', 'features', 'featuresLogic']), @@ -22,13 +24,14 @@ export const earlyAccessFeaturesLogic = kea([ () => [], (): Breadcrumb[] => [ { - name: 'Early Access Management', + key: Scene.EarlyAccessFeatures, + name: 'Early access features', path: urls.earlyAccessFeatures(), }, ], ], }), - afterMount(async ({ actions }) => { - await actions.loadEarlyAccessFeatures() + afterMount(({ actions }) => { + actions.loadEarlyAccessFeatures() }), ]) diff --git a/frontend/src/scenes/events/EventDetails.scss b/frontend/src/scenes/events/EventDetails.scss index 2e54eda04b0d1..ac55e6ab55136 100644 --- a/frontend/src/scenes/events/EventDetails.scss +++ b/frontend/src/scenes/events/EventDetails.scss @@ -1,3 +1,3 @@ .LemonTabs[data-attr='event-details'] ul { - padding: 0px 0.75rem; + padding: 0 0.75rem; } diff --git a/frontend/src/scenes/events/EventDetails.tsx b/frontend/src/scenes/events/EventDetails.tsx index 9bf9efcf96648..59e5ed057bafe 100644 --- a/frontend/src/scenes/events/EventDetails.tsx +++ b/frontend/src/scenes/events/EventDetails.tsx @@ -1,19 +1,20 @@ -import { useState } from 'react' -import { KEY_MAPPING } from 'lib/taxonomy' -import { PropertiesTable } from 'lib/components/PropertiesTable' -import { HTMLElementsDisplay } from 'lib/components/HTMLElementsDisplay/HTMLElementsDisplay' -import { EventJSON } from 'scenes/events/EventJSON' -import { EventType, PropertyDefinitionType } from '~/types' +import './EventDetails.scss' + +import ReactJson from '@microlink/react-json-view' import { Properties } from '@posthog/plugin-scaffold' +import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' +import { HTMLElementsDisplay } from 'lib/components/HTMLElementsDisplay/HTMLElementsDisplay' +import { PropertiesTable } from 'lib/components/PropertiesTable' import { dayjs } from 'lib/dayjs' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { pluralize } from 'lib/utils' import { LemonTableProps } from 'lib/lemon-ui/LemonTable' -import ReactJson from '@microlink/react-json-view' -import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { KEY_MAPPING } from 'lib/taxonomy' +import { pluralize } from 'lib/utils' +import { useState } from 'react' +import { EventJSON } from 'scenes/events/EventJSON' -import './EventDetails.scss' +import { EventType, PropertyDefinitionType } from '~/types' interface EventDetailsProps { event: EventType diff --git a/frontend/src/scenes/events/Events.stories.tsx b/frontend/src/scenes/events/Events.stories.tsx index 8ee043b5c500c..f650449e755ab 100644 --- a/frontend/src/scenes/events/Events.stories.tsx +++ b/frontend/src/scenes/events/Events.stories.tsx @@ -1,11 +1,12 @@ import { Meta } from '@storybook/react' - +import { router } from 'kea-router' import { useEffect } from 'react' +import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' + import eventsQuery from './__mocks__/eventsQuery.json' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import { App } from 'scenes/App' const meta: Meta = { title: 'Scenes-App/Events', diff --git a/frontend/src/scenes/events/Events.tsx b/frontend/src/scenes/events/Events.tsx index 164363a7b35cc..0a2e94cd634ae 100644 --- a/frontend/src/scenes/events/Events.tsx +++ b/frontend/src/scenes/events/Events.tsx @@ -1,6 +1,6 @@ -import { SceneExport } from 'scenes/sceneTypes' import { PageHeader } from 'lib/components/PageHeader' import { EventsScene } from 'scenes/events/EventsScene' +import { SceneExport } from 'scenes/sceneTypes' export const scene: SceneExport = { component: Events, @@ -14,7 +14,7 @@ export const scene: SceneExport = { export function Events(): JSX.Element { return ( <> - +
    diff --git a/frontend/src/scenes/events/EventsScene.tsx b/frontend/src/scenes/events/EventsScene.tsx index 6b450242da5a3..d727a2371d3a8 100644 --- a/frontend/src/scenes/events/EventsScene.tsx +++ b/frontend/src/scenes/events/EventsScene.tsx @@ -1,5 +1,6 @@ import { useActions, useValues } from 'kea' import { eventsSceneLogic } from 'scenes/events/eventsSceneLogic' + import { Query } from '~/queries/Query/Query' export function EventsScene(): JSX.Element { diff --git a/frontend/src/scenes/events/Owner.tsx b/frontend/src/scenes/events/Owner.tsx index 600be23efbe12..308912d41d455 100644 --- a/frontend/src/scenes/events/Owner.tsx +++ b/frontend/src/scenes/events/Owner.tsx @@ -1,5 +1,6 @@ import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { CSSProperties } from 'react' + import { UserBasicType } from '~/types' export function Owner({ user, style = {} }: { user?: UserBasicType | null; style?: CSSProperties }): JSX.Element { diff --git a/frontend/src/scenes/events/createActionFromEvent.test.js b/frontend/src/scenes/events/createActionFromEvent.test.js index 3ea1f6a2330c2..e98e446466a17 100644 --- a/frontend/src/scenes/events/createActionFromEvent.test.js +++ b/frontend/src/scenes/events/createActionFromEvent.test.js @@ -1,8 +1,10 @@ -import { api } from 'lib/api.mock' import { router } from 'kea-router' -import { createActionFromEvent } from './createActionFromEvent' +import { api } from 'lib/api.mock' + import { initKeaTests } from '~/test/init' +import { createActionFromEvent } from './createActionFromEvent' + describe('createActionFromEvent()', () => { given( 'subject', diff --git a/frontend/src/scenes/events/createActionFromEvent.tsx b/frontend/src/scenes/events/createActionFromEvent.tsx index 0e0f584ad12d7..d12f365efb3a7 100644 --- a/frontend/src/scenes/events/createActionFromEvent.tsx +++ b/frontend/src/scenes/events/createActionFromEvent.tsx @@ -1,20 +1,21 @@ import { router } from 'kea-router' +import { CLICK_TARGETS, elementToSelector, matchesDataAttribute } from 'lib/actionUtils' import api from 'lib/api' -import { autoCaptureEventToDescription } from 'lib/utils' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { Link } from 'lib/lemon-ui/Link' +import { autoCaptureEventToDescription } from 'lib/utils' +import { urls } from 'scenes/urls' + import { ActionStepType, - StringMatching, ActionType, ElementType, EventType, PropertyFilterType, PropertyOperator, + StringMatching, TeamType, } from '~/types' -import { CLICK_TARGETS, elementToSelector, matchesDataAttribute } from 'lib/actionUtils' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { urls } from 'scenes/urls' export function recurseSelector(elements: ElementType[], parts: string, index: number): string { const element = elements[index] diff --git a/frontend/src/scenes/events/defaults.ts b/frontend/src/scenes/events/defaults.ts index eb62b5ce22c80..97fd86203ec4c 100644 --- a/frontend/src/scenes/events/defaults.ts +++ b/frontend/src/scenes/events/defaults.ts @@ -1,5 +1,5 @@ -import { DataTableNode, NodeKind } from '~/queries/schema' import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' +import { DataTableNode, NodeKind } from '~/queries/schema' import { AnyPropertyFilter } from '~/types' export const getDefaultEventsSceneQuery = (properties?: AnyPropertyFilter[]): DataTableNode => ({ diff --git a/frontend/src/scenes/events/eventsSceneLogic.tsx b/frontend/src/scenes/events/eventsSceneLogic.tsx index 8fff224f4ea5f..08912e5666d76 100644 --- a/frontend/src/scenes/events/eventsSceneLogic.tsx +++ b/frontend/src/scenes/events/eventsSceneLogic.tsx @@ -1,15 +1,16 @@ +import equal from 'fast-deep-equal' import { actions, connect, kea, path, reducers, selectors } from 'kea' - -import type { eventsSceneLogicType } from './eventsSceneLogicType' import { actionToUrl, urlToAction } from 'kea-router' -import equal from 'fast-deep-equal' -import { Node } from '~/queries/schema' -import { urls } from 'scenes/urls' -import { objectsEqual } from 'lib/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { objectsEqual } from 'lib/utils' import { getDefaultEventsSceneQuery } from 'scenes/events/defaults' import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + import { getDefaultEventsQueryForTeam } from '~/queries/nodes/DataTable/defaultEventsQuery' +import { Node } from '~/queries/schema' + +import type { eventsSceneLogicType } from './eventsSceneLogicType' export const eventsSceneLogic = kea([ path(['scenes', 'events', 'eventsSceneLogic']), diff --git a/frontend/src/scenes/experiments/Experiment.scss b/frontend/src/scenes/experiments/Experiment.scss index dd965da7de551..e8f1d3c61391e 100644 --- a/frontend/src/scenes/experiments/Experiment.scss +++ b/frontend/src/scenes/experiments/Experiment.scss @@ -2,7 +2,6 @@ .metrics-selection { border-top: 1px solid var(--border); padding-top: 1rem; - width: 100%; } @@ -14,49 +13,6 @@ justify-content: space-between; } - .insights-graph-container { - margin-bottom: 1rem; - - .display-config-inner { - display: flex; - align-items: center; - justify-content: space-between; - overflow-x: auto; - } - - .ant-card-body { - padding: 0; - } - - .insights-graph-container-row { - .insights-graph-container-row-left { - width: 100%; - } - - .insights-graph-container-row-right { - width: 100%; - height: min(calc(90vh - 16rem), 36rem); // same as .trends-insights-container - max-width: 300px; - padding: 0 1rem 1rem 0; - display: flex; - align-items: center; - } - } - - .LineGraph { - width: calc(100% - 2rem); - height: calc(100% - 2rem) !important; - margin-top: 1rem; - } - } - - .insights-graph-header { - margin-top: 0 !important; - padding-left: 1rem; - padding-right: 1rem; - min-height: 48px; - } - .experiment-preview { margin-bottom: 1rem; border-bottom: 1px solid var(--border); @@ -75,6 +31,7 @@ .variants { margin-top: 0.5rem; padding-bottom: 1rem; + .ant-form-horizontal { min-height: 32px; } @@ -92,7 +49,7 @@ .feature-flag-variant { display: flex; align-items: center; - padding: 0.5rem 0.5rem; + padding: 0.5rem; background: var(--bg-light); border-width: 1px; border-color: var(--border); @@ -111,7 +68,7 @@ justify-content: center; align-items: center; border-radius: 4px; - color: #ffffff; + color: #fff; min-width: 52px; padding: 2px 6px; margin-right: 8px; @@ -151,6 +108,7 @@ .participants { background-color: white; + .ant-collapse-header { padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -162,10 +120,12 @@ li { display: inline; } - li:after { + + li::after { content: ', '; } - li:last-child:after { + + li:last-child::after { content: ''; } } @@ -209,9 +169,7 @@ min-height: 320px; font-size: 24px; } -} -.view-experiment { .computation-time-and-sampling-notice { margin-top: 8px; } @@ -231,6 +189,7 @@ border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 1rem; + &:last-child { border-bottom: none; padding-bottom: 0; diff --git a/frontend/src/scenes/experiments/Experiment.stories.tsx b/frontend/src/scenes/experiments/Experiment.stories.tsx index a80c329c6e478..041dab5a4ad78 100644 --- a/frontend/src/scenes/experiments/Experiment.stories.tsx +++ b/frontend/src/scenes/experiments/Experiment.stories.tsx @@ -1,9 +1,11 @@ -import { useEffect } from 'react' import { Meta, StoryFn } from '@storybook/react' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' +import { useAvailableFeatures } from '~/mocks/features' import { toPaginatedResponse } from '~/mocks/handlers' import { AvailableFeature, @@ -20,7 +22,6 @@ import { SignificanceCode, TrendsExperimentResults, } from '~/types' -import { useAvailableFeatures } from '~/mocks/features' const MOCK_FUNNEL_EXPERIMENT: Experiment = { id: 1, diff --git a/frontend/src/scenes/experiments/Experiment.tsx b/frontend/src/scenes/experiments/Experiment.tsx index fdc5fe554faee..641ab21598c20 100644 --- a/frontend/src/scenes/experiments/Experiment.tsx +++ b/frontend/src/scenes/experiments/Experiment.tsx @@ -1,24 +1,5 @@ -import { Popconfirm, Progress } from 'antd' -import { BindLogic, useActions, useValues } from 'kea' -import { PageHeader } from 'lib/components/PageHeader' -import { useEffect, useState } from 'react' -import { insightLogic } from 'scenes/insights/insightLogic' -import { SceneExport } from 'scenes/sceneTypes' -import { AvailableFeature, Experiment as ExperimentType, FunnelStep, InsightType } from '~/types' import './Experiment.scss' -import { experimentLogic, ExperimentLogicProps } from './experimentLogic' -import { IconDelete, IconPlusMini } from 'lib/lemon-ui/icons' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { dayjs } from 'lib/dayjs' -import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' -import { SecondaryMetrics } from './SecondaryMetrics' -import { EditableField } from 'lib/components/EditableField/EditableField' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' -import { ExperimentPreview } from './ExperimentPreview' -import { ExperimentImplementationDetails } from './ExperimentImplementationDetails' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { router } from 'kea-router' + import { LemonDivider, LemonInput, @@ -29,23 +10,45 @@ import { LemonTextArea, Tooltip, } from '@posthog/lemon-ui' -import { NotFound } from 'lib/components/NotFound' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Popconfirm, Progress } from 'antd' +import clsx from 'clsx' +import { BindLogic, useActions, useValues } from 'kea' import { Form, Group } from 'kea-forms' +import { router } from 'kea-router' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +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 { userLogic } from 'scenes/userLogic' -import { ExperimentsPayGate } from './ExperimentsPayGate' +import { IconDelete, IconPlusMini } 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 { EXPERIMENT_INSIGHT_ID } from './constants' +import { Link } from 'lib/lemon-ui/Link' +import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' +import { useEffect, useState } from 'react' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { Query } from '~/queries/Query/Query' import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' +import { SceneExport } from 'scenes/sceneTypes' import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' -import { ExperimentInsightCreator } from './MetricSelector' -import { More } from 'lib/lemon-ui/LemonButton/More' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { Query } from '~/queries/Query/Query' +import { AvailableFeature, Experiment as ExperimentType, FunnelStep, InsightType } from '~/types' + +import { EXPERIMENT_INSIGHT_ID } from './constants' +import { ExperimentImplementationDetails } from './ExperimentImplementationDetails' +import { experimentLogic, ExperimentLogicProps } from './experimentLogic' +import { ExperimentPreview } from './ExperimentPreview' import { ExperimentResult } from './ExperimentResult' -import clsx from 'clsx' import { getExperimentStatus, getExperimentStatusColor } from './experimentsLogic' +import { ExperimentsPayGate } from './ExperimentsPayGate' +import { ExperimentInsightCreator } from './MetricSelector' +import { SecondaryMetrics } from './SecondaryMetrics' export const scene: SceneExport = { component: Experiment, @@ -244,13 +247,13 @@ export function Experiment(): JSX.Element { numbers, hyphens, and underscores.
    - {experiment.parameters.feature_flag_variants?.map((variant, index) => ( + {experiment.parameters.feature_flag_variants?.map((_, index) => (
    - - {experiment.feature_flag?.key} - + {experiment.feature_flag && ( + + {experiment.feature_flag.key} + + )} @@ -855,10 +860,10 @@ export function ResultsTag(): JSX.Element { export function LoadingState(): JSX.Element { return (
    - + - +
    ) } diff --git a/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx b/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx index 65c0651a65159..5dfdd792335a7 100644 --- a/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx +++ b/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx @@ -1,6 +1,9 @@ +import { LemonSelect, Link } from '@posthog/lemon-ui' import { IconGolang, IconJavascript, IconNodeJS, IconPHP, IconPython, IconRuby } from 'lib/lemon-ui/icons' import { useState } from 'react' + import { Experiment, MultivariateFlagVariant } from '~/types' + import { GolangSnippet, JSSnippet, @@ -10,7 +13,6 @@ import { RNSnippet, RubySnippet, } from './ExperimentCodeSnippets' -import { LemonSelect, Link } from '@posthog/lemon-ui' interface ExperimentImplementationDetailsProps { experiment: Partial | null diff --git a/frontend/src/scenes/experiments/ExperimentPreview.tsx b/frontend/src/scenes/experiments/ExperimentPreview.tsx index 51de26936b578..9d7585e944e77 100644 --- a/frontend/src/scenes/experiments/ExperimentPreview.tsx +++ b/frontend/src/scenes/experiments/ExperimentPreview.tsx @@ -1,8 +1,16 @@ +import { LemonButton, LemonDivider, LemonModal, Tooltip } from '@posthog/lemon-ui' import { InputNumber, Slider } from 'antd' -import { useValues, useActions } from 'kea' +import { useActions, useValues } from 'kea' +import { Field, Form } from 'kea-forms' import { InsightLabel } from 'lib/components/InsightLabel' import { PropertyFilterButton } from 'lib/components/PropertyFilters/components/PropertyFilterButton' +import { TZLabel } from 'lib/components/TZLabel' import { dayjs } from 'lib/dayjs' +import { IconInfo } from 'lib/lemon-ui/icons' +import { humanFriendlyNumber } from 'lib/utils' +import { groupFilters } from 'scenes/feature-flags/FeatureFlags' +import { urls } from 'scenes/urls' + import { ActionFilter as ActionFilterType, AnyPropertyFilter, @@ -10,17 +18,11 @@ import { InsightType, MultivariateFlagVariant, } from '~/types' + +import { EXPERIMENT_EXPOSURE_INSIGHT_ID, EXPERIMENT_INSIGHT_ID } from './constants' import { experimentLogic } from './experimentLogic' import { ExperimentWorkflow } from './ExperimentWorkflow' -import { humanFriendlyNumber } from 'lib/utils' -import { LemonButton, LemonDivider, LemonModal, Tooltip } from '@posthog/lemon-ui' -import { Field, Form } from 'kea-forms' import { MetricSelector } from './MetricSelector' -import { IconInfo } from 'lib/lemon-ui/icons' -import { TZLabel } from 'lib/components/TZLabel' -import { EXPERIMENT_EXPOSURE_INSIGHT_ID, EXPERIMENT_INSIGHT_ID } from './constants' -import { groupFilters } from 'scenes/feature-flags/FeatureFlags' -import { urls } from 'scenes/urls' interface ExperimentPreviewProps { experimentId: number | 'new' diff --git a/frontend/src/scenes/experiments/ExperimentResult.tsx b/frontend/src/scenes/experiments/ExperimentResult.tsx index bf275975b6f5e..1cec85e4b05b6 100644 --- a/frontend/src/scenes/experiments/ExperimentResult.tsx +++ b/frontend/src/scenes/experiments/ExperimentResult.tsx @@ -1,18 +1,21 @@ +import './Experiment.scss' + +import { IconInfo } from '@posthog/icons' +import { Tooltip } from '@posthog/lemon-ui' import { Col, Progress } from 'antd' import { useValues } from 'kea' -import { ChartDisplayType, FilterType, FunnelVizType, InsightShortId, InsightType } from '~/types' -import './Experiment.scss' -import { experimentLogic } from './experimentLogic' -import { FunnelLayout } from 'lib/constants' -import { capitalizeFirstLetter } from 'lib/utils' import { getSeriesColor } from 'lib/colors' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { NodeKind } from '~/queries/schema' +import { FunnelLayout } from 'lib/constants' +import { capitalizeFirstLetter } from 'lib/utils' + import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' -import { IconInfo } from '@posthog/icons' +import { NodeKind } from '~/queries/schema' +import { ChartDisplayType, FilterType, FunnelVizType, InsightShortId, InsightType } from '~/types' + import { LoadingState } from './Experiment' -import { Tooltip } from '@posthog/lemon-ui' +import { experimentLogic } from './experimentLogic' export function ExperimentResult(): JSX.Element { const { @@ -149,7 +152,7 @@ export function ExperimentResult(): JSX.Element { )} {experimentResults ? ( // :KLUDGE: using `insights-page` for proper styling, should rather adapt styles -
    +
    { return experiment.end_date @@ -144,7 +146,7 @@ export function Experiments(): JSX.Element { return (
    Experiments
    } + title={
    A/B testing
    } buttons={ hasAvailableFeature(AvailableFeature.EXPERIMENTATION) ? ( @@ -154,14 +156,13 @@ export function Experiments(): JSX.Element { } caption={ <> - Check out our {' '} - Experimentation user guide + Visit the guide {' '} to learn more. @@ -182,7 +183,7 @@ export function Experiments(): JSX.Element { {(shouldShowEmptyState || shouldShowProductIntroduction) && (tab === ExperimentsTabs.Archived ? ( ) : ( diff --git a/frontend/src/scenes/experiments/ExperimentsPayGate.tsx b/frontend/src/scenes/experiments/ExperimentsPayGate.tsx index 678ad322638ef..0a0ebe684e684 100644 --- a/frontend/src/scenes/experiments/ExperimentsPayGate.tsx +++ b/frontend/src/scenes/experiments/ExperimentsPayGate.tsx @@ -1,4 +1,5 @@ import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' + import { AvailableFeature } from '~/types' export function ExperimentsPayGate(): JSX.Element { diff --git a/frontend/src/scenes/experiments/MetricSelector.tsx b/frontend/src/scenes/experiments/MetricSelector.tsx index 7ac3b5e84d8d9..5b2a09f1e1eec 100644 --- a/frontend/src/scenes/experiments/MetricSelector.tsx +++ b/frontend/src/scenes/experiments/MetricSelector.tsx @@ -1,29 +1,29 @@ -import { BindLogic, useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import './Experiment.scss' +import { IconInfo } from '@posthog/icons' +import { LemonSelect } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { Attribution } from 'scenes/insights/EditorFilters/AttributionFilter' +import { SamplingFilter } from 'scenes/insights/EditorFilters/SamplingFilter' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { EditorFilterProps, FilterType, InsightLogicProps, InsightShortId, InsightType } from '~/types' import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { LemonSelect } from '@posthog/lemon-ui' -import { SamplingFilter } from 'scenes/insights/EditorFilters/SamplingFilter' -import { Query } from '~/queries/Query/Query' -import { FunnelsQuery, InsightQueryNode, TrendsQuery } from '~/queries/schema' import { AggregationSelect } from 'scenes/insights/filters/AggregationSelect' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { FunnelConversionWindowFilter } from 'scenes/insights/views/Funnels/FunnelConversionWindowFilter' -import './Experiment.scss' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Attribution } from 'scenes/insights/EditorFilters/AttributionFilter' +import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { TestAccountFilter } from '~/queries/nodes/InsightViz/filters/TestAccountFilter' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Query } from '~/queries/Query/Query' +import { FunnelsQuery, InsightQueryNode, TrendsQuery } from '~/queries/schema' +import { EditorFilterProps, FilterType, InsightLogicProps, InsightShortId, InsightType } from '~/types' + import { DEFAULT_DURATION } from './experimentLogic' -import { IconInfo } from '@posthog/icons' export interface MetricSelectorProps { dashboardItemId: InsightShortId diff --git a/frontend/src/scenes/experiments/SecondaryMetrics.tsx b/frontend/src/scenes/experiments/SecondaryMetrics.tsx index 75d22a10d19ec..b1c710dcb9da1 100644 --- a/frontend/src/scenes/experiments/SecondaryMetrics.tsx +++ b/frontend/src/scenes/experiments/SecondaryMetrics.tsx @@ -1,22 +1,24 @@ -import { Col, Row } from 'antd' -import { useActions, useValues } from 'kea' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { Form } from 'kea-forms' -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { InsightType } from '~/types' import './Experiment.scss' -import { secondaryMetricsLogic, SecondaryMetricsProps } from './secondaryMetricsLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { IconDelete, IconEdit } from 'lib/lemon-ui/icons' + import { LemonInput, LemonModal, LemonTable } from '@posthog/lemon-ui' -import { Field } from 'lib/forms/Field' -import { MetricSelector } from './MetricSelector' -import { experimentLogic, TabularSecondaryMetricResults } from './experimentLogic' +import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' import { getSeriesColor } from 'lib/colors' -import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { Field } from 'lib/forms/Field' +import { IconDelete, IconEdit } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' + +import { InsightType } from '~/types' + import { SECONDARY_METRIC_INSIGHT_ID } from './constants' +import { experimentLogic, TabularSecondaryMetricResults } from './experimentLogic' +import { MetricSelector } from './MetricSelector' +import { secondaryMetricsLogic, SecondaryMetricsProps } from './secondaryMetricsLogic' export function SecondaryMetrics({ onMetricsChange, @@ -163,11 +165,11 @@ export function SecondaryMetrics({ {experimentId == 'new' || editingExistingExperiment ? ( - -
    +
    +
    {metrics.map((metric, idx) => ( - - +
    +
    {metric.name}
    @@ -185,7 +187,7 @@ export function SecondaryMetrics({ onClick={() => deleteMetric(idx)} />
    - +
    {metric.filters.insight === InsightType.FUNNELS && ( )} -
    +
    ))} {metrics && !(metrics.length > 2) && ( -
    -
    - - Add metric - -
    - +
    + + Add metric + +
    )} - - + + ) : ( <>
    Secondary metrics
    diff --git a/frontend/src/scenes/experiments/experimentLogic.test.ts b/frontend/src/scenes/experiments/experimentLogic.test.ts index 0165dec8c4d9b..cf088b3103711 100644 --- a/frontend/src/scenes/experiments/experimentLogic.test.ts +++ b/frontend/src/scenes/experiments/experimentLogic.test.ts @@ -1,10 +1,12 @@ import { expectLogic } from 'kea-test-utils' +import { userLogic } from 'scenes/userLogic' + +import { useAvailableFeatures } from '~/mocks/features' +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' import { AvailableFeature } from '~/types' + import { experimentLogic } from './experimentLogic' -import { useMocks } from '~/mocks/jest' -import { useAvailableFeatures } from '~/mocks/features' -import { userLogic } from 'scenes/userLogic' const RUNNING_EXP_ID = 45 const RUNNING_FUNNEL_EXP_ID = 46 diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 6f28ea14006b9..af0f013600fbd 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -1,46 +1,49 @@ -import { ReactElement } from 'react' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import { router, urlToAction } from 'kea-router' import api from 'lib/api' +import { FunnelLayout } from 'lib/constants' import { dayjs } from 'lib/dayjs' +import { IconInfo } from 'lib/lemon-ui/icons' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { ReactElement } from 'react' +import { validateFeatureFlagKey } from 'scenes/feature-flags/featureFlagLogic' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { cleanFilters, getDefaultEvent } from 'scenes/insights/utils/cleanFilters' +import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' + +import { groupsModel } from '~/models/groupsModel' +import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { InsightVizNode } from '~/queries/schema' import { + ActionFilter as ActionFilterType, Breadcrumb, + CountPerActorMathType, Experiment, ExperimentResults, FilterType, + FunnelStep, FunnelVizType, InsightType, MultivariateFlagVariant, - TrendResult, - FunnelStep, + PropertyMathType, SecondaryExperimentMetric, SignificanceCode, - CountPerActorMathType, - ActionFilter as ActionFilterType, TrendExperimentVariant, - PropertyMathType, + TrendResult, } from '~/types' + +import { EXPERIMENT_EXPOSURE_INSIGHT_ID, EXPERIMENT_INSIGHT_ID } from './constants' import type { experimentLogicType } from './experimentLogicType' -import { router, urlToAction } from 'kea-router' import { experimentsLogic } from './experimentsLogic' -import { FunnelLayout } from 'lib/constants' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { toParams } from 'lib/utils' -import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { forms } from 'kea-forms' -import { loaders } from 'kea-loaders' -import { IconInfo } from 'lib/lemon-ui/icons' -import { validateFeatureFlagKey } from 'scenes/feature-flags/featureFlagLogic' -import { EXPERIMENT_EXPOSURE_INSIGHT_ID, EXPERIMENT_INSIGHT_ID } from './constants' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { InsightVizNode } from '~/queries/schema' -import { groupsModel } from '~/models/groupsModel' export const DEFAULT_DURATION = 14 // days @@ -634,10 +637,12 @@ export const experimentLogic = kea([ (s) => [s.experiment, s.experimentId], (experiment, experimentId): Breadcrumb[] => [ { + key: Scene.Experiments, name: 'Experiments', path: urls.experiments(), }, { + key: experimentId, name: experiment?.name || 'New', path: urls.experiment(experimentId || 'new'), }, diff --git a/frontend/src/scenes/experiments/experimentsLogic.ts b/frontend/src/scenes/experiments/experimentsLogic.ts index f76151f5e5b95..b290677205a01 100644 --- a/frontend/src/scenes/experiments/experimentsLogic.ts +++ b/frontend/src/scenes/experiments/experimentsLogic.ts @@ -1,16 +1,18 @@ +import { LemonTagType } from '@posthog/lemon-ui' +import Fuse from 'fuse.js' import { actions, connect, events, kea, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' -import type { experimentsLogicType } from './experimentsLogicType' -import { teamLogic } from 'scenes/teamLogic' -import { AvailableFeature, Experiment, ExperimentsTabs, ProductKey, ProgressStatus } from '~/types' +import { FEATURE_FLAGS } from 'lib/constants' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import Fuse from 'fuse.js' -import { userLogic } from 'scenes/userLogic' -import { subscriptions } from 'kea-subscriptions' -import { loaders } from 'kea-loaders' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { LemonTagType } from '@posthog/lemon-ui' +import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature, Experiment, ExperimentsTabs, ProductKey, ProgressStatus } from '~/types' + +import type { experimentsLogicType } from './experimentsLogicType' export function getExperimentStatus(experiment: Experiment): ProgressStatus { if (!experiment.start_date) { diff --git a/frontend/src/scenes/experiments/secondaryMetricsLogic.ts b/frontend/src/scenes/experiments/secondaryMetricsLogic.ts index 9687b9e3b7627..d3b04d4a29c38 100644 --- a/frontend/src/scenes/experiments/secondaryMetricsLogic.ts +++ b/frontend/src/scenes/experiments/secondaryMetricsLogic.ts @@ -1,20 +1,19 @@ -import { actions, connect, kea, listeners, path, props, key, reducers } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea' import { forms } from 'kea-forms' -import { dayjs } from 'lib/dayjs' - -import { Experiment, FilterType, FunnelVizType, InsightType, SecondaryExperimentMetric } from '~/types' -import { cleanFilters, getDefaultEvent } from 'scenes/insights/utils/cleanFilters' import { FunnelLayout } from 'lib/constants' -import { InsightVizNode } from '~/queries/schema' - -import { SECONDARY_METRIC_INSIGHT_ID } from './constants' -import { insightLogic } from 'scenes/insights/insightLogic' +import { dayjs } from 'lib/dayjs' import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { cleanFilters, getDefaultEvent } from 'scenes/insights/utils/cleanFilters' +import { teamLogic } from 'scenes/teamLogic' + import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { teamLogic } from 'scenes/teamLogic' +import { InsightVizNode } from '~/queries/schema' +import { Experiment, FilterType, FunnelVizType, InsightType, SecondaryExperimentMetric } from '~/types' +import { SECONDARY_METRIC_INSIGHT_ID } from './constants' import type { secondaryMetricsLogicType } from './secondaryMetricsLogicType' const DEFAULT_DURATION = 14 @@ -121,7 +120,7 @@ export const secondaryMetricsLogic = kea([ })), forms(({ props }) => ({ secondaryMetricModal: { - defaults: defaultFormValuesGenerator(props.defaultAggregationType) as SecondaryMetricForm, + defaults: defaultFormValuesGenerator(props.defaultAggregationType), errors: () => ({}), submit: async () => { // We don't use the form submit anymore diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.scss b/frontend/src/scenes/feature-flags/FeatureFlag.scss index a793ab541a7fe..319512c5f7670 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.scss +++ b/frontend/src/scenes/feature-flags/FeatureFlag.scss @@ -1,4 +1,4 @@ -.variant-form-list { +.VariantFormList { font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius); @@ -19,13 +19,16 @@ align-items: center; } } + + .VariantFormList__row { + grid-template-columns: repeat(24, minmax(0, 1fr)); + } } .feature-flag-property-display { display: flex; - flex-direction: row; + flex-flow: row wrap; align-items: center; - flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; @@ -61,7 +64,11 @@ } .FeatureConditionCard { + .posthog-3000 & { + background: var(--bg-light); + } + .FeatureConditionCard--border--highlight { - border-color: var(--primary); + border-color: var(--primary-3000); } } diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 1118c87a2b31d..8c2ef3d75606e 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -1,72 +1,75 @@ -import { useEffect, useState } from 'react' -import { Form, Group } from 'kea-forms' -import { Row, Col, Radio, Popconfirm, Skeleton, Card } from 'antd' +import './FeatureFlag.scss' + +import { Card, Popconfirm, Radio, Skeleton } from 'antd' import { useActions, useValues } from 'kea' -import { alphabet, capitalizeFirstLetter } from 'lib/utils' -import { featureFlagLogic } from './featureFlagLogic' -import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import { Form, Group } from 'kea-forms' +import { router } from 'kea-router' +import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { NotFound } from 'lib/components/NotFound' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' -import './FeatureFlag.scss' +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 { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' +import { Link } from 'lib/lemon-ui/Link' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { SceneExport } from 'scenes/sceneTypes' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import { alphabet, capitalizeFirstLetter } from 'lib/utils' +import { PostHogFeature } from 'posthog-js/react' +import { useEffect, useState } from 'react' +import { billingLogic } from 'scenes/billing/billingLogic' +import { Dashboard } from 'scenes/dashboard/Dashboard' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent' import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput' +import { concatWithPunctuation } from 'scenes/insights/utils' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { ResourcePermission } from 'scenes/ResourcePermissionModal' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + +import { tagsModel } from '~/models/tagsModel' +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' import { AnyPropertyFilter, AvailableFeature, DashboardPlacement, + FeatureFlagGroupType, + FeatureFlagType, + NotebookNodeType, PropertyFilterType, PropertyOperator, - Resource, - FeatureFlagType, ReplayTabs, - FeatureFlagGroupType, - NotebookNodeType, + Resource, } from '~/types' -import { Link } from 'lib/lemon-ui/Link' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Field } from 'lib/forms/Field' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { urls } from 'scenes/urls' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { router } from 'kea-router' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' -import { FEATURE_FLAGS } from 'lib/constants' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { FeatureFlagsTab, featureFlagsLogic } from './featureFlagsLogic' -import { RecentFeatureFlagInsights } from './RecentFeatureFlagInsightsCard' -import { NotFound } from 'lib/components/NotFound' + +import { AnalysisTab } from './FeatureFlagAnalysisTab' import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout' -import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' -import { ResourcePermission } from 'scenes/ResourcePermissionModal' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { tagsModel } from '~/models/tagsModel' -import { Dashboard } from 'scenes/dashboard/Dashboard' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent' import { FeatureFlagCodeExample } from './FeatureFlagCodeExample' -import { billingLogic } from 'scenes/billing/billingLogic' -import { organizationLogic } from '../organizationLogic' -import { AnalysisTab } from './FeatureFlagAnalysisTab' -import { NodeKind } from '~/queries/schema' -import { Query } from '~/queries/Query/Query' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' -import { PostHogFeature } from 'posthog-js/react' -import { concatWithPunctuation } from 'scenes/insights/utils' -import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { featureFlagLogic } from './featureFlagLogic' +import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' import FeatureFlagProjects from './FeatureFlagProjects' +import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions' +import { featureFlagsLogic, FeatureFlagsTab } from './featureFlagsLogic' +import { RecentFeatureFlagInsights } from './RecentFeatureFlagInsightsCard' export const scene: SceneExport = { component: FeatureFlag, @@ -78,16 +81,23 @@ export const scene: SceneExport = { function focusVariantKeyField(index: number): void { setTimeout( - () => document.querySelector(`.variant-form-list input[data-key-index="${index}"]`)?.focus(), + () => document.querySelector(`.VariantFormList input[data-key-index="${index}"]`)?.focus(), 50 ) } export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { - const { props, featureFlag, featureFlagLoading, featureFlagMissing, isEditingFlag, recordingFilterForFlag } = - useValues(featureFlagLogic) + const { + props, + featureFlag, + featureFlagLoading, + featureFlagMissing, + isEditingFlag, + recordingFilterForFlag, + newCohortLoading, + } = useValues(featureFlagLogic) const { featureFlags } = useValues(enabledFeaturesLogic) - const { deleteFeatureFlag, editFeatureFlag, loadFeatureFlag, triggerFeatureFlagUpdate } = + const { deleteFeatureFlag, editFeatureFlag, loadFeatureFlag, triggerFeatureFlagUpdate, createStaticCohort } = useActions(featureFlagLogic) const { addableRoles, unfilteredAddableRolesLoading, rolesToAdd, derivedRoles } = useValues( @@ -97,8 +107,6 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { featureFlagPermissionsLogic({ flagId: featureFlag.id }) ) - const { currentOrganization } = useValues(organizationLogic) - const { tags } = useValues(tagsModel) const { hasAvailableFeature } = useValues(userLogic) @@ -155,8 +163,7 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { }) } - const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 - if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS] && hasMultipleProjects) { + if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS]) { tabs.push({ label: 'Projects', key: FeatureFlagsTab.PROJECTS, @@ -332,7 +339,7 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { tagsAvailable={tags.filter( (tag) => !featureFlag.tags?.includes(tag) )} - className="insight-metadata-tags" + className="mt-2" /> ) }} @@ -510,14 +517,14 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { tagsAvailable={tags.filter( (tag) => !featureFlag.tags?.includes(tag) )} - className="insight-metadata-tags" + className="mt-2" /> ) : featureFlag.tags.length ? ( ) : null} @@ -527,6 +534,58 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { buttons={ <>
    + + + View Recordings + + {featureFlags[ + FEATURE_FLAGS.FEATURE_FLAG_COHORT_CREATION + ] && ( + { + createStaticCohort() + }} + fullWidth + > + Create Cohort + + )} + + { + deleteFeatureFlag(featureFlag) + }} + disabledReason={ + featureFlagLoading + ? 'Loading...' + : !featureFlag.can_edit + ? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator." + : (featureFlag.features?.length || 0) > 0 + ? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag' + : (featureFlag.experiment_set?.length || 0) > 0 + ? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag' + : null + } + > + Delete feature flag + + + } + /> + - - View Recordings - - - { - deleteFeatureFlag(featureFlag) - }} - disabledReason={ - featureFlagLoading - ? 'Loading...' - : !featureFlag.can_edit - ? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator." - : (featureFlag.features?.length || 0) > 0 - ? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag' - : (featureFlag.experiment_set?.length || 0) > 0 - ? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag' - : null - } - > - Delete feature flag -

    Variant keys

    - -
    Key - Description - Payload - Rollout - +
    +
    Key
    +
    Description
    +
    Payload
    +
    Rollout
    +
    {variants.map((variant, index) => (
    - -
    +
    +
    {variant.key} - -
    + +
    {variant.name || 'There is no description for this variant key'} - -
    + +
    {featureFlag.filters.payloads?.[index] ? ( )} - -
    {variant.rollout_percentage}% - + +
    {variant.rollout_percentage}%
    + {index !== variants.length - 1 && } ))} @@ -874,24 +905,22 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element { No payload associated with this flag ) ) : ( - - -
    - Specify a payload to be returned when the served value is{' '} - - true - -
    - - - - - - - +
    +
    + Specify a payload to be returned when the served value is{' '} + + true + +
    + + + + + +
    )} )} @@ -899,33 +928,33 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {

    Variant keys

    The rollout percentage of feature flag variants must add up to 100% -
    - -
    - Variant key - Description - -
    +
    +
    +
    +
    Variant key
    +
    Description
    +
    +
    Payload - + Specify return payload when the variant key matches
    - -
    - Rollout + +
    + Rollout (Redistribute) - - +
    + {variants.map((variant, index) => ( - - +
    +
    - -
    + +
    - -
    + +
    - -
    + +
    {({ value, onChange }) => { return ( @@ -966,8 +995,8 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element { ) }} - -
    + +
    {({ value, onChange }) => (
    @@ -1012,28 +1041,25 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
    )}
    - -
    - - {variants.length > 1 && ( - } - status="primary-alt" - data-attr={`delete-prop-filter-${index}`} - noPadding - onClick={() => removeVariant(index)} - disabledReason={ - featureFlag.experiment_set && - featureFlag.experiment_set?.length > 0 - ? 'Cannot delete variants from a feature flag that is part of an experiment' - : undefined - } - tooltipPlacement="topRight" - /> - )} - - - + +
    + {variants.length > 1 && ( + } + status="primary-alt" + data-attr={`delete-prop-filter-${index}`} + noPadding + onClick={() => removeVariant(index)} + disabledReason={ + featureFlag.experiment_set && featureFlag.experiment_set?.length > 0 + ? 'Cannot delete variants from a feature flag that is part of an experiment' + : undefined + } + tooltipPlacement="topRight" + /> + )} +
    + ))} {variants.length > 0 && !areVariantRolloutsValid && ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlagAnalysisTab.tsx b/frontend/src/scenes/feature-flags/FeatureFlagAnalysisTab.tsx index bbdc85d879a7f..01f30493967da 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagAnalysisTab.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagAnalysisTab.tsx @@ -1,12 +1,14 @@ +import { LemonButton } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' -import { DashboardTemplateChooser, NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' -import { DashboardsTable } from 'scenes/dashboard/dashboards/DashboardsTable' import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' +import { DashboardsTable } from 'scenes/dashboard/dashboards/DashboardsTable' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { DashboardTemplateChooser, NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' + import { dashboardsModel } from '~/models/dashboardsModel' import { FeatureFlagType } from '~/types' + import { featureFlagLogic } from './featureFlagLogic' -import { LemonButton } from '@posthog/lemon-ui' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' export function AnalysisTab({ featureFlag }: { id: string; featureFlag: FeatureFlagType }): JSX.Element { return ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx index 93d251e7e4aa4..c27158a9983df 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx @@ -1,17 +1,18 @@ import { LemonButton, LemonDivider, LemonInput, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Group } from 'kea-forms' -import { IconDelete } from 'lib/lemon-ui/icons' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { Field } from 'lib/forms/Field' +import { IconDelete } from 'lib/lemon-ui/icons' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { capitalizeFirstLetter, genericOperatorMap, humanFriendlyNumber } from 'lib/utils' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' - import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + import { RolloutConditionType } from '~/types' + import { featureFlagLogic } from './featureFlagLogic' interface FeatureFlagAutoRollbackProps { @@ -168,8 +169,8 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro
    {sentryErrorCount ? ( - {humanFriendlyNumber(sentryErrorCount as number)} sentry - errors in the past 24 hours.{' '} + {humanFriendlyNumber(sentryErrorCount)} Sentry errors in the + past 24 hours.{' '} ) : ( @@ -204,7 +205,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro {humanFriendlyNumber( Math.round( - (sentryErrorCount as number) * + sentryErrorCount * (1 + (featureFlag.rollback_conditions[index] .threshold || 0) / diff --git a/frontend/src/scenes/feature-flags/FeatureFlagCodeExample.tsx b/frontend/src/scenes/feature-flags/FeatureFlagCodeExample.tsx index e624613a86edf..7f1735c7df9f7 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagCodeExample.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagCodeExample.tsx @@ -1,4 +1,5 @@ import { FeatureFlagType } from '~/types' + import { FeatureFlagInstructions } from './FeatureFlagInstructions' export function FeatureFlagCodeExample({ featureFlag }: { featureFlag: FeatureFlagType }): JSX.Element { diff --git a/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx b/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx index 48605e78b7123..064b3b7e41dd7 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx @@ -1,12 +1,13 @@ import { Meta } from '@storybook/react' -import { CodeInstructions, CodeInstructionsProps } from './FeatureFlagInstructions' -import { OPTIONS } from './FeatureFlagCodeOptions' -import { FeatureFlagType, SDKKey } from '~/types' import { useStorybookMocks } from '~/mocks/browser' import { useAvailableFeatures } from '~/mocks/features' +import { FeatureFlagType, SDKKey } from '~/types' import { AvailableFeature } from '~/types' +import { OPTIONS } from './FeatureFlagCodeOptions' +import { CodeInstructions, CodeInstructionsProps } from './FeatureFlagInstructions' + const REGULAR_FEATURE_FLAG: FeatureFlagType = { id: 1, name: 'test', diff --git a/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx index 8615f736a925e..9ef72f4e779b5 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagCodeOptions.tsx @@ -1,19 +1,20 @@ import { SDKKey } from '~/types' + import { - UTM_TAGS, - FeatureFlagSnippet, - JSSnippet, AndroidSnippet, - iOSSnippet, - ReactNativeSnippet, - NodeJSSnippet, - PythonSnippet, - RubySnippet, APISnippet, - PHPSnippet, + FeatureFlagSnippet, GolangSnippet, + iOSSnippet, JSBootstrappingSnippet, + JSSnippet, + NodeJSSnippet, + PHPSnippet, + PythonSnippet, + ReactNativeSnippet, ReactSnippet, + RubySnippet, + UTM_TAGS, } from './FeatureFlagSnippets' const DOC_BASE_URL = 'https://posthog.com/docs/' diff --git a/frontend/src/scenes/feature-flags/FeatureFlagInstructions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagInstructions.tsx index 99b10ac9a558a..5f5b781ec48ee 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagInstructions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagInstructions.tsx @@ -1,24 +1,27 @@ -import { useEffect, useState } from 'react' -import { useActions, useValues } from 'kea' -import { IconInfo } from 'lib/lemon-ui/icons' import './FeatureFlagInstructions.scss' + import { LemonCheckbox, LemonSelect, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { INSTANTLY_AVAILABLE_PROPERTIES } from 'lib/constants' +import { IconInfo } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { useEffect, useState } from 'react' + +import { groupsModel } from '~/models/groupsModel' import { FeatureFlagType, GroupTypeIndex } from '~/types' + import { BOOTSTRAPPING_OPTIONS, FF_ANCHOR, InstructionOption, LibraryType, - LOCAL_EVALUATION_LIBRARIES, - PAYLOAD_LIBRARIES, LOCAL_EVAL_ANCHOR, + LOCAL_EVALUATION_LIBRARIES, OPTIONS, + PAYLOAD_LIBRARIES, PAYLOADS_ANCHOR, } from './FeatureFlagCodeOptions' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { groupsModel } from '~/models/groupsModel' -import { INSTANTLY_AVAILABLE_PROPERTIES } from 'lib/constants' function FeatureFlagInstructionsFooter({ documentationLink }: { documentationLink: string }): JSX.Element { return ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx index d22a2fb63ec09..8c610bbbcc151 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx @@ -1,16 +1,19 @@ -import { OrganizationFeatureFlag } from '~/types' -import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { LemonButton, LemonSelect, LemonTag, Link, LemonBanner } from '@posthog/lemon-ui' -import { IconArrowRight, IconSync } from 'lib/lemon-ui/icons' -import { groupFilters } from './FeatureFlags' +import { LemonBanner, LemonButton, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { featureFlagLogic } from './featureFlagLogic' -import { organizationLogic } from '../organizationLogic' +import { OrganizationMembershipLevel } from 'lib/constants' +import { IconArrowRight, IconSync } from 'lib/lemon-ui/icons' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { useEffect } from 'react' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' -import { useEffect } from 'react' + import { groupsModel } from '~/models/groupsModel' +import { OrganizationFeatureFlag } from '~/types' + +import { organizationLogic } from '../organizationLogic' +import { featureFlagLogic } from './featureFlagLogic' +import { groupFilters } from './FeatureFlags' const getColumns = (): LemonTableColumns => { const { currentTeamId } = useValues(teamLogic) @@ -77,71 +80,106 @@ const getColumns = (): LemonTableColumns => { ] } -export default function FeatureFlagProjects(): JSX.Element { +function InfoBanner(): JSX.Element { + const { currentOrganization } = useValues(organizationLogic) + const { featureFlag } = useValues(featureFlagLogic) + const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 + + const isMember = + !currentOrganization?.membership_level || + currentOrganization.membership_level < OrganizationMembershipLevel.Admin + + let text + + if (isMember && !hasMultipleProjects) { + text = `You currently have access to only one project. If your organization manages multiple projects and you wish to copy this feature flag across them, request project access from your administrator.` + } else if (!hasMultipleProjects) { + text = `This feature enables the copying of a feature flag across different projects. Once additional projects are added within your organization, you'll be able to replicate this flag to them.` + } else if (!featureFlag.can_edit) { + text = `You don't have the necessary permissions to copy this flag to another project. Contact your administrator to request editing rights.` + } else { + return <> + } + + return ( + + {text} + + ) +} + +function FeatureFlagCopySection(): JSX.Element { const { featureFlag, copyDestinationProject, projectsWithCurrentFlag, featureFlagCopyLoading } = useValues(featureFlagLogic) - const { setCopyDestinationProject, loadProjectsWithCurrentFlag, copyFlag } = useActions(featureFlagLogic) + const { setCopyDestinationProject, copyFlag } = useActions(featureFlagLogic) const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) + const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 + + return hasMultipleProjects && featureFlag.can_edit ? ( + <> +

    Feature flag copy

    +
    Copy your flag and its configuration to another project.
    +
    +
    +
    Key
    +
    + {featureFlag.key} +
    +
    +
    +
    + +
    +
    +
    Destination project
    + setCopyDestinationProject(id)} + options={ + currentOrganization?.teams + ?.map((team) => ({ value: team.id, label: team.name })) + .filter((option) => option.value !== currentTeam?.id) || [] + } + className="min-w-40" + /> +
    +
    +
    + } + onClick={() => copyFlag()} + className="w-28 max-w-28" + > + {projectsWithCurrentFlag.find((p) => Number(p.team_id) === copyDestinationProject) + ? 'Update' + : 'Copy'} + +
    +
    + + ) : ( + <> + ) +} + +export default function FeatureFlagProjects(): JSX.Element { + const { projectsWithCurrentFlag } = useValues(featureFlagLogic) + const { loadProjectsWithCurrentFlag } = useActions(featureFlagLogic) + useEffect(() => { loadProjectsWithCurrentFlag() }, []) return (
    - {featureFlag.can_edit ? ( - <> -

    Feature flag copy

    -
    Copy your flag and its configuration to another project.
    -
    -
    -
    Key
    -
    - {featureFlag.key} -
    -
    -
    -
    - -
    -
    -
    Destination project
    - setCopyDestinationProject(id)} - options={ - currentOrganization?.teams - ?.map((team) => ({ value: team.id, label: team.name })) - .filter((option) => option.value !== currentTeam?.id) || [] - } - className="min-w-40" - /> -
    -
    -
    - } - onClick={() => copyFlag()} - className="w-28 max-w-28" - > - {projectsWithCurrentFlag.find((p) => Number(p.team_id) === copyDestinationProject) - ? 'Update' - : 'Copy'} - -
    -
    - - ) : ( - - You currently cannot copy this flag to another project. Contact your administrator to request - editing rights. - - )} + + { return ( -
    +
    {index > 0 &&
    OR
    }
    @@ -244,7 +247,7 @@ export function FeatureFlagReleaseConditions({ { - updateConditionSet(index, value as number) + updateConditionSet(index, value) }} value={group.rollout_percentage != null ? group.rollout_percentage : 100} min={0} @@ -314,7 +317,7 @@ export function FeatureFlagReleaseConditions({ )}
    - +
    ) } @@ -327,7 +330,7 @@ export function FeatureFlagReleaseConditions({ const hasMatchingEarlyAccessFeature = featureFlag.features?.find((f: any) => f.flagKey === featureFlag.key) return ( -
    +
    {index > 0 &&
    OR
    }
    @@ -386,7 +389,7 @@ export function FeatureFlagReleaseConditions({
    - +
    ) } @@ -456,11 +459,11 @@ export function FeatureFlagReleaseConditions({ )} - +
    {filterGroups.map((group, index) => isSuper ? renderSuperReleaseConditionGroup(group, index) : renderReleaseConditionGroup(group, index) )} - +
    {!readOnly && ( }> Add condition set diff --git a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx index c4f3b42c935fe..8ed6088dea7ee 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx @@ -1,6 +1,7 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' + import { GroupType } from '~/types' export const UTM_TAGS = '?utm_medium=in-product&utm_campaign=feature-flag' diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx index eb74c9fe61248..532062f8b75da 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx @@ -1,13 +1,15 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' -import featureFlags from './__mocks__/feature_flags.json' import { useAvailableFeatures } from '~/mocks/features' import { AvailableFeature } from '~/types' +import featureFlags from './__mocks__/feature_flags.json' + const meta: Meta = { title: 'Scenes-App/Feature Flags', parameters: { diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index b021107a84379..5eff9ffc7b491 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -1,35 +1,38 @@ +import { LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { featureFlagsLogic, FeatureFlagsTab } from './featureFlagsLogic' -import { Link } from 'lib/lemon-ui/Link' -import { copyToClipboard, deleteWithUndo } from 'lib/utils' +import { router } from 'kea-router' +import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { FeatureFlagHog } from 'lib/components/hedgehogs' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' -import { AnyPropertyFilter, AvailableFeature, FeatureFlagFilters, FeatureFlagType, ProductKey } from '~/types' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import PropertyFiltersDisplay from 'lib/components/PropertyFilters/components/PropertyFiltersDisplay' import { normalizeColumnTitle } from 'lib/components/Table/utils' -import { urls } from 'scenes/urls' -import stringWithWBR from 'lib/utils/stringWithWBR' -import { teamLogic } from '../teamLogic' -import { SceneExport } from 'scenes/sceneTypes' +import { FEATURE_FLAGS } from 'lib/constants' +import { IconLock } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { More } from 'lib/lemon-ui/LemonButton/More' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import PropertyFiltersDisplay from 'lib/components/PropertyFilters/components/PropertyFiltersDisplay' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconLock } from 'lib/lemon-ui/icons' -import { router } from 'kea-router' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { userLogic } from 'scenes/userLogic' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { FeatureFlagHog } from 'lib/components/hedgehogs' -import { Noun, groupsModel } from '~/models/groupsModel' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import stringWithWBR from 'lib/utils/stringWithWBR' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { groupsModel, Noun } from '~/models/groupsModel' +import { AnyPropertyFilter, AvailableFeature, FeatureFlagFilters, FeatureFlagType, ProductKey } from '~/types' + +import { teamLogic } from '../teamLogic' +import { featureFlagsLogic, FeatureFlagsTab } from './featureFlagsLogic' export const scene: SceneExport = { component: FeatureFlags, @@ -158,8 +161,8 @@ export function OverViewTab({ <> { - await copyToClipboard(featureFlag.key, 'feature flag key') + onClick={() => { + void copyToClipboard(featureFlag.key, 'feature flag key') }} fullWidth > @@ -210,7 +213,7 @@ export function OverViewTab({ { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: loadFeatureFlags, @@ -257,6 +260,7 @@ export function OverViewTab({
    void diff --git a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx index 5fdf19a9a4cb6..225a69bdbf761 100644 --- a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx +++ b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx @@ -2,7 +2,9 @@ import { useValues } from 'kea' import { CompactList } from 'lib/components/CompactList/CompactList' import { InsightRow } from 'scenes/project-homepage/RecentInsights' import { urls } from 'scenes/urls' + import { InsightModel } from '~/types' + import { featureFlagLogic } from './featureFlagLogic' export function RecentFeatureFlagInsights(): JSX.Element { diff --git a/frontend/src/scenes/feature-flags/activityDescriptions.tsx b/frontend/src/scenes/feature-flags/activityDescriptions.tsx index e7f90ab9c628c..d2b3b754aaf50 100644 --- a/frontend/src/scenes/feature-flags/activityDescriptions.tsx +++ b/frontend/src/scenes/feature-flags/activityDescriptions.tsx @@ -6,13 +6,14 @@ import { detectBoolean, HumanizedChange, } from 'lib/components/ActivityLog/humanizeActivity' +import { SentenceList } from 'lib/components/ActivityLog/SentenceList' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { PropertyFilterButton } from 'lib/components/PropertyFilters/components/PropertyFilterButton' import { Link } from 'lib/lemon-ui/Link' +import { pluralize } from 'lib/utils' import { urls } from 'scenes/urls' + import { AnyPropertyFilter, FeatureFlagFilters, FeatureFlagGroupType, FeatureFlagType } from '~/types' -import { pluralize } from 'lib/utils' -import { SentenceList } from 'lib/components/ActivityLog/SentenceList' -import { PropertyFilterButton } from 'lib/components/PropertyFilters/components/PropertyFilterButton' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' const nameOrLinkToFlag = (id: string | undefined, name: string | null | undefined): string | JSX.Element => { // detail.name diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts index db6df537e3eac..e4d21cb4b7c64 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts @@ -1,8 +1,9 @@ -import { initKeaTests } from '~/test/init' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { expectLogic } from 'kea-test-utils' -import { useMocks } from '~/mocks/jest' import api from 'lib/api' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' import { FeatureFlagGroupType, FeatureFlagType, diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index aa331b35cbc12..742eff1b44cb8 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -1,9 +1,35 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import type { featureFlagLogicType } from './featureFlagLogicType' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import { router, urlToAction } from 'kea-router' +import api from 'lib/api' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { dayjs } from 'lib/dayjs' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { sum, toParams } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' +import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { NEW_EARLY_ACCESS_FEATURE } from 'scenes/early-access-features/earlyAccessFeatureLogic' +import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' +import { filterTrendsClientSideParams } from 'scenes/insights/sharedUtils' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { Scene } from 'scenes/sceneTypes' +import { NEW_SURVEY, NewSurvey } from 'scenes/surveys/constants' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + +import { groupsModel } from '~/models/groupsModel' import { AnyPropertyFilter, AvailableFeature, Breadcrumb, + CohortType, + DashboardBasicType, + EarlyAccessFeatureType, + FeatureFlagGroupType, FeatureFlagRollbackConditions, FeatureFlagType, FilterType, @@ -11,40 +37,20 @@ import { InsightType, MultivariateFlagOptions, MultivariateFlagVariant, + NewEarlyAccessFeatureType, + OrganizationFeatureFlag, PropertyFilterType, PropertyOperator, RolloutConditionType, - FeatureFlagGroupType, - UserBlastRadiusType, - DashboardBasicType, - NewEarlyAccessFeatureType, - EarlyAccessFeatureType, Survey, SurveyQuestionType, - OrganizationFeatureFlag, + UserBlastRadiusType, } from '~/types' -import api from 'lib/api' -import { router, urlToAction } from 'kea-router' -import { convertPropertyGroupToProperties, deleteWithUndo, sum, toParams } from 'lib/utils' -import { urls } from 'scenes/urls' + +import { organizationLogic } from '../organizationLogic' import { teamLogic } from '../teamLogic' -import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' -import { groupsModel } from '~/models/groupsModel' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { loaders } from 'kea-loaders' -import { forms } from 'kea-forms' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' -import { dayjs } from 'lib/dayjs' -import { filterTrendsClientSideParams } from 'scenes/insights/sharedUtils' +import type { featureFlagLogicType } from './featureFlagLogicType' import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' -import { userLogic } from 'scenes/userLogic' -import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' -import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' -import { organizationLogic } from '../organizationLogic' -import { NEW_EARLY_ACCESS_FEATURE } from 'scenes/early-access-features/earlyAccessFeatureLogic' -import { NEW_SURVEY, NewSurvey } from 'scenes/surveys/constants' const getDefaultRollbackCondition = (): FeatureFlagRollbackConditions => ({ operator: 'gt', @@ -577,6 +583,17 @@ export const featureFlagLogic = kea([ }, }, ], + newCohort: [ + null as CohortType | null, + { + createStaticCohort: async () => { + if (props.id && props.id !== 'new' && props.id !== 'link') { + return (await api.featureFlags.createStaticCohort(props.id)).cohort + } + return null + }, + }, + ], projectsWithCurrentFlag: { __default: [] as OrganizationFeatureFlag[], loadProjectsWithCurrentFlag: async () => { @@ -638,7 +655,7 @@ export const featureFlagLogic = kea([ actions.editFeatureFlag(false) }, deleteFeatureFlag: async ({ featureFlag }) => { - deleteWithUndo({ + await deleteWithUndo({ endpoint: `projects/${values.currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: () => { @@ -779,12 +796,24 @@ export const featureFlagLogic = kea([ : 'copied' lemonToast.success(`Feature flag ${operation} successfully!`) } else { - lemonToast.error(`Error while saving feature flag: ${featureFlagCopy?.failed || featureFlagCopy}`) + lemonToast.error( + `Error while saving feature flag: ${JSON.stringify(featureFlagCopy?.failed) || featureFlagCopy}` + ) } actions.loadProjectsWithCurrentFlag() actions.setCopyDestinationProject(null) }, + createStaticCohortSuccess: ({ newCohort }) => { + if (newCohort) { + lemonToast.success('Static cohort created successfully', { + button: { + label: 'View cohort', + action: () => router.actions.push(urls.cohort(newCohort.id)), + }, + }) + } + }, })), selectors({ sentryErrorCount: [(s) => [s.sentryStats], (stats) => stats.total_count], @@ -834,10 +863,11 @@ export const featureFlagLogic = kea([ (s) => [s.featureFlag], (featureFlag): Breadcrumb[] => [ { + key: Scene.FeatureFlags, name: 'Feature Flags', path: urls.featureFlags(), }, - ...(featureFlag ? [{ name: featureFlag.key || 'Unnamed' }] : []), + { key: featureFlag.id || 'unknown', name: featureFlag.key || 'Unnamed' }, ], ], propertySelectErrors: [ diff --git a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx index 43c0733e877e7..34f78193ae1f8 100644 --- a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx +++ b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx @@ -3,6 +3,7 @@ import { loaders } from 'kea-loaders' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { rolesLogic } from 'scenes/settings/organization/Permissions/Roles/rolesLogic' + import { AccessLevel, FeatureFlagAssociatedRoleType, Resource, RoleType } from '~/types' import type { featureFlagPermissionsLogicType } from './featureFlagPermissionsLogicType' diff --git a/frontend/src/scenes/feature-flags/featureFlagsLogic.test.ts b/frontend/src/scenes/feature-flags/featureFlagsLogic.test.ts index d1b64c65710d4..ce39c7481bc12 100644 --- a/frontend/src/scenes/feature-flags/featureFlagsLogic.test.ts +++ b/frontend/src/scenes/feature-flags/featureFlagsLogic.test.ts @@ -1,9 +1,10 @@ -import { initKeaTests } from '~/test/init' -import { featureFlagsLogic, FeatureFlagsTab } from 'scenes/feature-flags/featureFlagsLogic' -import { expectLogic } from 'kea-test-utils' import { router } from 'kea-router' +import { expectLogic } from 'kea-test-utils' +import { featureFlagsLogic, FeatureFlagsTab } from 'scenes/feature-flags/featureFlagsLogic' import { urls } from 'scenes/urls' +import { initKeaTests } from '~/test/init' + describe('the feature flags logic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/feature-flags/featureFlagsLogic.ts b/frontend/src/scenes/feature-flags/featureFlagsLogic.ts index e88e418635a2c..e196a65ac98b5 100644 --- a/frontend/src/scenes/feature-flags/featureFlagsLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagsLogic.ts @@ -1,13 +1,16 @@ +import Fuse from 'fuse.js' +import { actions, connect, events, kea, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, path, connect, actions, reducers, selectors, listeners, events } from 'kea' +import { actionToUrl, router, urlToAction } from 'kea-router' import api from 'lib/api' -import Fuse from 'fuse.js' -import type { featureFlagsLogicType } from './featureFlagsLogicType' +import { LemonSelectOption } from 'lib/lemon-ui/LemonSelect' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { Breadcrumb, FeatureFlagType } from '~/types' + import { teamLogic } from '../teamLogic' -import { urls } from 'scenes/urls' -import { router, actionToUrl, urlToAction } from 'kea-router' -import { LemonSelectOption } from 'lib/lemon-ui/LemonSelect' +import type { featureFlagsLogicType } from './featureFlagsLogicType' export enum FeatureFlagsTab { OVERVIEW = 'overview', @@ -154,7 +157,8 @@ export const featureFlagsLogic = kea([ () => [], (): Breadcrumb[] => [ { - name: 'Feature Flags', + key: Scene.FeatureFlags, + name: 'Feature flags', path: urls.featureFlags(), }, ], diff --git a/frontend/src/scenes/feedback/Feedback.scss b/frontend/src/scenes/feedback/Feedback.scss deleted file mode 100644 index b644140412e4a..0000000000000 --- a/frontend/src/scenes/feedback/Feedback.scss +++ /dev/null @@ -1,2 +0,0 @@ -.Feedback { -} diff --git a/frontend/src/scenes/feedback/Feedback.stories.tsx b/frontend/src/scenes/feedback/Feedback.stories.tsx index 1fb673c9d23e0..d767eb8ba26aa 100644 --- a/frontend/src/scenes/feedback/Feedback.stories.tsx +++ b/frontend/src/scenes/feedback/Feedback.stories.tsx @@ -3,18 +3,20 @@ import { router } from 'kea-router' import { useEffect } from 'react' import { App } from 'scenes/App' import { urls } from 'scenes/urls' + import { mswDecorator } from '~/mocks/browser' + import { feedbackLogic } from './feedbackLogic' import { inAppFeedbackLogic } from './inAppFeedbackLogic' import { userInterviewSchedulerLogic } from './userInterviewSchedulerLogic' const meta: Meta = { title: 'Scenes-App/Feedback', + tags: ['test-skip'], // FIXME: Use mockdate in this story parameters: { layout: 'fullscreen', testOptions: { excludeNavigationFromSnapshot: true, - skip: true, // FIXME: Use mockdate in this story }, viewMode: 'story', // Might need to add a mockdate here, however when I do it breaks the page diff --git a/frontend/src/scenes/feedback/Feedback.tsx b/frontend/src/scenes/feedback/Feedback.tsx index 8646f01e82186..38da1029a2457 100644 --- a/frontend/src/scenes/feedback/Feedback.tsx +++ b/frontend/src/scenes/feedback/Feedback.tsx @@ -1,14 +1,12 @@ import { LemonTag } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { SceneExport } from 'scenes/sceneTypes' import { feedbackLogic } from './feedbackLogic' - -import './Feedback.scss' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { InAppFeedback, InAppFeedbackHeaderButtons } from './InAppFeedback' import { UserInterviewScheduler, UserInterviewSchedulerHeaderButtons } from './UserInterviewScheduler' -import { useActions, useValues } from 'kea' export const Feedback = (): JSX.Element => { const { activeTab } = useValues(feedbackLogic) diff --git a/frontend/src/scenes/feedback/InAppFeedback.tsx b/frontend/src/scenes/feedback/InAppFeedback.tsx index 126ca7111f5f9..15d52352d6a9a 100644 --- a/frontend/src/scenes/feedback/InAppFeedback.tsx +++ b/frontend/src/scenes/feedback/InAppFeedback.tsx @@ -1,14 +1,13 @@ -import { LemonButton, LemonCollapse, LemonDivider, LemonModal, Link } from '@posthog/lemon-ui' - import { urls } from '@posthog/apps-common' +import { LemonButton, LemonCollapse, LemonDivider, LemonModal, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { inAppFeedbackLogic } from './inAppFeedbackLogic' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' - -import './Feedback.scss' import { IconHelpOutline } from 'lib/lemon-ui/icons' + import { Query } from '~/queries/Query/Query' +import { inAppFeedbackLogic } from './inAppFeedbackLogic' + const OPT_IN_SNIPPET = `posthog.init('YOUR_PROJECT_API_KEY', { api_host: 'YOUR API HOST', opt_in_site_apps: true // <--- Add this line diff --git a/frontend/src/scenes/feedback/UserInterviewScheduler.tsx b/frontend/src/scenes/feedback/UserInterviewScheduler.tsx index 6b9105f4cae09..a1539d13fa273 100644 --- a/frontend/src/scenes/feedback/UserInterviewScheduler.tsx +++ b/frontend/src/scenes/feedback/UserInterviewScheduler.tsx @@ -1,16 +1,15 @@ -import { LemonButton, LemonCollapse, LemonInput, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui' +import './UserInterviewScheduler.scss' import { urls } from '@posthog/apps-common' +import { LemonButton, LemonCollapse, LemonInput, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' - +import { Field } from 'lib/forms/Field' import { IconHelpOutline } from 'lib/lemon-ui/icons' -import { FLAG_PREFIX, userInterviewSchedulerLogic } from './userInterviewSchedulerLogic' import { OverViewTab } from 'scenes/feature-flags/FeatureFlags' -import { Form } from 'kea-forms' -import './UserInterviewScheduler.scss' -import { Field } from 'lib/forms/Field' +import { FLAG_PREFIX, userInterviewSchedulerLogic } from './userInterviewSchedulerLogic' const OPT_IN_SNIPPET = `posthog.init('YOUR_PROJECT_API_KEY', { api_host: 'YOUR API HOST', diff --git a/frontend/src/scenes/feedback/inAppFeedbackLogic.ts b/frontend/src/scenes/feedback/inAppFeedbackLogic.ts index 8af2d525e9433..646947084ab2b 100644 --- a/frontend/src/scenes/feedback/inAppFeedbackLogic.ts +++ b/frontend/src/scenes/feedback/inAppFeedbackLogic.ts @@ -1,11 +1,12 @@ -import { EventsQuery, InsightVizNode } from '../../queries/schema' import { actions, afterMount, kea, path, reducers } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + import { DataTableNode, Node, NodeKind, QuerySchema, TrendsQuery } from '~/queries/schema' +import { EventType } from '~/types' +import { EventsQuery, InsightVizNode } from '../../queries/schema' import type { inAppFeedbackLogicType } from './inAppFeedbackLogicType' -import api from 'lib/api' -import { loaders } from 'kea-loaders' -import { EventType } from '~/types' const EVENT_NAME = 'Feedback Sent' const FEEDBACK_PROPERTY = '$feedback' @@ -61,7 +62,7 @@ export const inAppFeedbackLogic = kea([ }, ], dataTableQuery: [ - DEFAULT_DATATABLE_QUERY as DataTableNode, + DEFAULT_DATATABLE_QUERY, { setDataTableQuery: (_, { query }) => { if (query.kind === NodeKind.DataTableNode) { @@ -74,7 +75,7 @@ export const inAppFeedbackLogic = kea([ }, ], trendQuery: [ - DEFAULT_TREND_INSIGHT_VIZ_NODE as InsightVizNode, + DEFAULT_TREND_INSIGHT_VIZ_NODE, { setDataTableQuery: (_, { query }) => { if (query.kind === NodeKind.DataTableNode) { @@ -114,7 +115,7 @@ export const inAppFeedbackLogic = kea([ event: eventName, orderBy: ['-timestamp'], }) - return response.results as EventType[] + return response.results }, }, ], diff --git a/frontend/src/scenes/feedback/userInterviewSchedulerLogic.ts b/frontend/src/scenes/feedback/userInterviewSchedulerLogic.ts index 8273634fc716c..a9413936c8828 100644 --- a/frontend/src/scenes/feedback/userInterviewSchedulerLogic.ts +++ b/frontend/src/scenes/feedback/userInterviewSchedulerLogic.ts @@ -1,8 +1,10 @@ -import { isURL } from 'lib/utils' import { actions, kea, path, reducers, selectors } from 'kea' import { forms } from 'kea-forms' +import { isURL } from 'lib/utils' import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' + import { FeatureFlagGroupType, FeatureFlagType, PropertyFilterType, PropertyOperator } from '~/types' + import type { userInterviewSchedulerLogicType } from './userInterviewSchedulerLogicType' export const FLAG_PREFIX = 'interview-' diff --git a/frontend/src/scenes/funnels/Funnel.tsx b/frontend/src/scenes/funnels/Funnel.tsx index 571dec68e84e1..167841f982f08 100644 --- a/frontend/src/scenes/funnels/Funnel.tsx +++ b/frontend/src/scenes/funnels/Funnel.tsx @@ -1,32 +1,40 @@ import './Funnel.scss' -import { useValues } from 'kea' +import { useValues } from 'kea' +import { FunnelLayout } from 'lib/constants' +import { FunnelLineGraph } from 'scenes/funnels/FunnelLineGraph' import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelDataLogic } from './funnelDataLogic' import { ChartParams, FunnelVizType } from '~/types' -import { FunnelLayout } from 'lib/constants' -import { FunnelHistogram } from './FunnelHistogram' -import { FunnelLineGraph } from 'scenes/funnels/FunnelLineGraph' + import { FunnelBarChart } from './FunnelBarChart/FunnelBarChart' import { FunnelBarGraph } from './FunnelBarGraph/FunnelBarGraph' +import { funnelDataLogic } from './funnelDataLogic' +import { FunnelHistogram } from './FunnelHistogram' export function Funnel(props: ChartParams): JSX.Element { const { insightProps } = useValues(insightLogic) const { funnelsFilter } = useValues(funnelDataLogic(insightProps)) const { funnel_viz_type, layout } = funnelsFilter || {} + let viz: JSX.Element | null = null if (funnel_viz_type == FunnelVizType.Trends) { - return - } - - if (funnel_viz_type == FunnelVizType.TimeToConvert) { - return - } - - if ((layout || FunnelLayout.vertical) === FunnelLayout.vertical) { - return + viz = + } else if (funnel_viz_type == FunnelVizType.TimeToConvert) { + viz = + } else if ((layout || FunnelLayout.vertical) === FunnelLayout.vertical) { + viz = + } else { + viz = } - return + return ( +
    + {viz} +
    + ) } diff --git a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss index faf5e1edb7938..b4a3090c12a25 100644 --- a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss +++ b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.scss @@ -3,28 +3,28 @@ .FunnelBarChart { position: relative; width: 100%; - height: 26rem; + height: 100%; overflow: hidden; - .InsightCard & { - height: 100%; - table { - margin: 0.5rem 0 0; - } - } + flex: 1; + table { --bar-width: 0.5rem; // This should be overriden from React --bar-row-height: 18rem; - margin: 0.5rem 1rem 0; + width: 100%; + height: 100%; + > tbody { > tr { &:first-child { border-bottom: 1px solid var(--border); + > td { padding: 1.5rem 0; padding-top: 1rem; } } + > td { // Sneaky hack to make height: 100% work in .StepLegend. The wonders of CSS - there's NO other way! height: 1px; @@ -49,6 +49,7 @@ .StepBarLabels__segment { flex-grow: 1; padding: 0 0.5rem; + &:first-child { flex-grow: 0; height: 0; @@ -69,6 +70,7 @@ border-bottom: 1px solid var(--border); height: calc(var(--bar-row-height) - 3rem); padding: 0 1rem; + &:not(.StepBars--first) { border-left: 1px dashed var(--border); } @@ -87,6 +89,7 @@ .StepBars__gridline { flex-grow: 1; + &.StepBars__gridline--horizontal { border-top: 1px dashed var(--border); } @@ -95,14 +98,17 @@ .StepBar { --series-color: #000; // This should be overriden from React --conversion-rate: 100%; // This should be overriden from React + position: relative; border-radius: var(--radius); width: calc(var(--bar-width) / 2); // We need to conserve space in narrow viewports flex-shrink: 0; height: 100%; + @include screen($lg) { width: var(--bar-width); } + .InsightCard & { width: calc(var(--bar-width) / 2) !important; // Also need to conserve space in cards } @@ -116,10 +122,12 @@ width: 100%; border-radius: var(--radius); cursor: pointer; + .InsightCard & { cursor: default; } } + .StepBar__unclickable { .StepBar__backdrop, .StepBar__fill { @@ -134,14 +142,16 @@ -22.5deg, transparent, transparent 0.5rem, - rgba(255, 255, 255, 0.5) 0.5rem, - rgba(255, 255, 255, 0.5) 1rem + rgb(255 255 255 / 50%) 0.5rem, + rgb(255 255 255 / 50%) 1rem ), var(--series-color); opacity: 0.125; + &:hover { opacity: 0.2; } + &:active { opacity: 0.25; } @@ -151,9 +161,11 @@ transition: filter 200ms ease; background: var(--series-color); height: var(--conversion-rate); + &:hover { filter: brightness(0.9); } + &:active { filter: brightness(0.85); } @@ -163,17 +175,20 @@ border-left: 1px solid var(--border); white-space: nowrap; height: 100%; + > .LemonRow { min-height: 1.5rem; padding: 0 0.5rem; font-weight: 500; margin-top: 0.25rem; + &:first-child { width: fit-content; font-weight: 600; margin-top: 0; } } + .funnel-inspect-button { line-height: 1.5rem; font-weight: inherit; @@ -182,23 +197,28 @@ .FunnelTooltip { width: 20rem; + table { width: 100%; border-collapse: collapse; border-spacing: 0; } + tr { height: 1.75rem; } + td:first-child { padding: 0 0.5rem; font-weight: 500; } + td:last-child { padding-right: 0.5rem; font-weight: 600; text-align: right; } + .table-subtext { padding-bottom: 0.25rem; } diff --git a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx index 8db62510709bb..ac2dbc2342804 100644 --- a/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx +++ b/frontend/src/scenes/funnels/FunnelBarChart/FunnelBarChart.tsx @@ -1,35 +1,35 @@ -import { useValues } from 'kea' -import { useMemo } from 'react' import './FunnelBarChart.scss' -import { ChartParams } from '~/types' + import clsx from 'clsx' -import { useScrollable } from 'lib/hooks/useScrollable' +import { useValues } from 'kea' import { useResizeObserver } from 'lib/hooks/useResizeObserver' -import { useFunnelTooltip } from '../useFunnelTooltip' -import { StepLegend } from './StepLegend' -import { StepBars } from './StepBars' -import { StepBarLabels } from './StepBarLabels' +import { useScrollable } from 'lib/hooks/useScrollable' +import { useMemo } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' + +import { ChartParams } from '~/types' + import { funnelDataLogic } from '../funnelDataLogic' import { funnelPersonsModalLogic } from '../funnelPersonsModalLogic' +import { useFunnelTooltip } from '../useFunnelTooltip' +import { StepBarLabels } from './StepBarLabels' +import { StepBars } from './StepBars' +import { StepLegend } from './StepLegend' interface FunnelBarChartCSSProperties extends React.CSSProperties { '--bar-width': string '--bar-row-height': string } -export function FunnelBarChart({ - inCardView, - showPersonsModal: showPersonsModalProp = true, -}: ChartParams): JSX.Element { +export function FunnelBarChart({ showPersonsModal: showPersonsModalProp = true }: ChartParams): JSX.Element { const { insightProps } = useValues(insightLogic) const { visibleStepsWithConversionMetrics } = useValues(funnelDataLogic(insightProps)) const { canOpenPersonModal } = useValues(funnelPersonsModalLogic(insightProps)) + const showPersonsModal = canOpenPersonModal && showPersonsModalProp + const vizRef = useFunnelTooltip(showPersonsModal) const [scrollRef, [isScrollableLeft, isScrollableRight]] = useScrollable() - const { height } = useResizeObserver({ ref: scrollRef }) - - const showPersonsModal = canOpenPersonModal && showPersonsModalProp + const { height } = useResizeObserver({ ref: vizRef }) const seriesCount = visibleStepsWithConversionMetrics[0]?.nested_breakdown?.length ?? 0 const barWidthPx = @@ -55,8 +55,6 @@ export function FunnelBarChart({ ? 96 : 192 - const vizRef = useFunnelTooltip(showPersonsModal) - const table = useMemo(() => { /** Average conversion time is only shown if it's known for at least one step. */ // != is intentional to catch undefined too @@ -109,16 +107,12 @@ export function FunnelBarChart({ ) }, [visibleStepsWithConversionMetrics, height]) - // negative margin-top so that the scrollable shadow is visible on the canvas label as well - const scrollableAdjustmentCanvasLabel = !inCardView && '-mt-12 pt-10' - return (
    ): JSX.Element { return ( diff --git a/frontend/src/scenes/funnels/FunnelBarChart/StepLegend.tsx b/frontend/src/scenes/funnels/FunnelBarChart/StepLegend.tsx index 08bed0d6fec05..820ec035a8d3b 100644 --- a/frontend/src/scenes/funnels/FunnelBarChart/StepLegend.tsx +++ b/frontend/src/scenes/funnels/FunnelBarChart/StepLegend.tsx @@ -1,18 +1,20 @@ import { useActions, useValues } from 'kea' -import { AvailableFeature, ChartParams, FunnelStepWithConversionMetrics } from '~/types' -import { LemonRow } from 'lib/lemon-ui/LemonRow' -import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' import { IconSchedule, IconTrendingFlat, IconTrendingFlatDown } from 'lib/lemon-ui/icons' +import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { capitalizeFirstLetter, humanFriendlyDuration, percentage, pluralize } from 'lib/utils' -import { ValueInspectorButton } from '../ValueInspectorButton' -import { FunnelStepMore } from '../FunnelStepMore' -import { userLogic } from 'scenes/userLogic' -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { insightLogic } from 'scenes/insights/insightLogic' +import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature, ChartParams, FunnelStepWithConversionMetrics } from '~/types' + import { funnelPersonsModalLogic } from '../funnelPersonsModalLogic' +import { FunnelStepMore } from '../FunnelStepMore' +import { ValueInspectorButton } from '../ValueInspectorButton' type StepLegendProps = { step: FunnelStepWithConversionMetrics diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx index 9fd86c95b9dd6..5b77c77556392 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/Bar.tsx @@ -1,11 +1,13 @@ -import { useEffect, useRef, useState } from 'react' +import { LemonDropdown } from '@posthog/lemon-ui' +import { getSeriesColor } from 'lib/colors' import { capitalizeFirstLetter, percentage } from 'lib/utils' +import { useEffect, useRef, useState } from 'react' import { LEGACY_InsightTooltip } from 'scenes/insights/InsightTooltip/LEGACY_InsightTooltip' -import { getSeriesPositionName } from '../funnelUtils' -import { getSeriesColor } from 'lib/colors' + import { Noun } from '~/models/groupsModel' + +import { getSeriesPositionName } from '../funnelUtils' import { MetricRow } from './MetricRow' -import { LemonDropdown } from '@posthog/lemon-ui' interface BarProps { percentage: number diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss index f1662dd783db1..ad4a376864a61 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss +++ b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.scss @@ -4,11 +4,9 @@ $label_position_offset: 8px; $series_container_width: 1.5rem; $glyph_height: 23px; // Based on .funnel-step-glyph -.funnel-bar-graph { +.FunnelBarGraph { min-height: 100%; - padding-left: 1.5rem; - padding-right: 1.5rem; - padding-bottom: 1rem; + padding: 1rem; .InsightCard & { padding-left: 1rem; @@ -18,6 +16,7 @@ $glyph_height: 23px; // Based on .funnel-step-glyph .funnel-step { position: relative; padding-left: $series_container_width + 0.5rem; + &:not(:first-child) { &, .funnel-series-container { @@ -36,6 +35,7 @@ $glyph_height: 23px; // Based on .funnel-step-glyph .funnel-inspect-button { line-height: 1.5rem; + .value-inspector-button-icon { font-size: 1.5rem; margin-right: 0.25rem; @@ -99,6 +99,7 @@ $glyph_height: 23px; // Based on .funnel-step-glyph .funnel-step-title { @extend %mixin-text-ellipsis; + font-weight: bold; } @@ -119,7 +120,7 @@ $glyph_height: 23px; // Based on .funnel-step-glyph .funnel-bar { position: relative; height: 100%; - background: var(--funnel-default); + background: var(--primary-3000); transition: width 0.2s ease, height 0.2s ease; &.first { @@ -147,7 +148,7 @@ $glyph_height: 23px; // Based on .funnel-step-glyph &.outside { left: calc(100% + #{$label_position_offset}); - color: var(--funnel-default); + color: var(--primary-3000); } } } diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx index 8a450fff32c27..10b44978f1cf8 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx @@ -1,22 +1,25 @@ -import clsx from 'clsx' -import { humanFriendlyDuration, percentage, pluralize } from 'lib/utils' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { SeriesGlyph } from 'lib/components/SeriesGlyph' -import { IconTrendingFlatDown, IconInfinity, IconTrendingFlat } from 'lib/lemon-ui/icons' import './FunnelBarGraph.scss' + +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { getBreakdownMaxIndex, getReferenceStep } from '../funnelUtils' -import { ChartParams, FunnelStepReference, StepOrderValue } from '~/types' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { SeriesGlyph } from 'lib/components/SeriesGlyph' import { useResizeObserver } from 'lib/hooks/useResizeObserver' -import { FunnelStepMore } from '../FunnelStepMore' -import { ValueInspectorButton } from '../ValueInspectorButton' -import { DuplicateStepIndicator } from './DuplicateStepIndicator' -import { Bar } from './Bar' +import { IconInfinity, IconTrendingFlat, IconTrendingFlatDown } from 'lib/lemon-ui/icons' +import { humanFriendlyDuration, percentage, pluralize } from 'lib/utils' import { insightLogic } from 'scenes/insights/insightLogic' +import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' + +import { ChartParams, FunnelStepReference, StepOrderValue } from '~/types' + import { funnelDataLogic } from '../funnelDataLogic' import { funnelPersonsModalLogic } from '../funnelPersonsModalLogic' +import { FunnelStepMore } from '../FunnelStepMore' +import { getBreakdownMaxIndex, getReferenceStep } from '../funnelUtils' +import { ValueInspectorButton } from '../ValueInspectorButton' +import { Bar } from './Bar' +import { DuplicateStepIndicator } from './DuplicateStepIndicator' export function FunnelBarGraph({ inCardView, @@ -39,11 +42,7 @@ export function FunnelBarGraph({ // Everything rendered after is a funnel in top-to-bottom mode. return ( -
    +
    {steps.map((step, stepIndex) => { const basisStep = getReferenceStep(steps, stepReference, stepIndex) const previousStep = getReferenceStep(steps, FunnelStepReference.previous, stepIndex) diff --git a/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx b/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx index 8b77e430650c9..ebc6b55ad2896 100644 --- a/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx +++ b/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx @@ -1,16 +1,16 @@ -import React from 'react' -import { useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelDataLogic } from './funnelDataLogic' - import { Link } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useActions, useValues } from 'kea' import { IconInfo } from 'lib/lemon-ui/icons' -import { FunnelVizType } from '~/types' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { humanFriendlyDuration, percentage } from 'lib/utils' +import React from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' import { FunnelStepsPicker } from 'scenes/insights/views/Funnels/FunnelStepsPicker' +import { FunnelVizType } from '~/types' + +import { funnelDataLogic } from './funnelDataLogic' + export function FunnelCanvasLabel(): JSX.Element | null { const { insightProps } = useValues(insightLogic) const { conversionMetrics, aggregationTargetLabel, funnelsFilter } = useValues(funnelDataLogic(insightProps)) diff --git a/frontend/src/scenes/funnels/FunnelHistogram.scss b/frontend/src/scenes/funnels/FunnelHistogram.scss index d07b541993718..e56336be344a7 100644 --- a/frontend/src/scenes/funnels/FunnelHistogram.scss +++ b/frontend/src/scenes/funnels/FunnelHistogram.scss @@ -1,4 +1,6 @@ -.funnel-histogram-outer-container { +.FunnelHistogram { + flex: 1; + &.scrollable { overflow-x: auto; } diff --git a/frontend/src/scenes/funnels/FunnelHistogram.tsx b/frontend/src/scenes/funnels/FunnelHistogram.tsx index 66d2cd1549d10..0c1c95ebbc80f 100644 --- a/frontend/src/scenes/funnels/FunnelHistogram.tsx +++ b/frontend/src/scenes/funnels/FunnelHistogram.tsx @@ -1,11 +1,13 @@ -import { useRef } from 'react' -import { useValues } from 'kea' -import clsx from 'clsx' +import './FunnelHistogram.scss' + import useSize from '@react-hook/size' +import clsx from 'clsx' +import { useValues } from 'kea' import { hashCodeForString, humanFriendlyDuration } from 'lib/utils' -import { Histogram } from 'scenes/insights/views/Histogram' +import { useRef } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' -import './FunnelHistogram.scss' +import { Histogram } from 'scenes/insights/views/Histogram' + import { funnelDataLogic } from './funnelDataLogic' export function FunnelHistogram(): JSX.Element | null { @@ -25,7 +27,7 @@ export function FunnelHistogram(): JSX.Element | null { return (
    humanFriendlyDuration(v, 2)} />
    diff --git a/frontend/src/scenes/funnels/FunnelLineGraph.tsx b/frontend/src/scenes/funnels/FunnelLineGraph.tsx index 9c38acdc17a7b..d5b7bf0dea8f8 100644 --- a/frontend/src/scenes/funnels/FunnelLineGraph.tsx +++ b/frontend/src/scenes/funnels/FunnelLineGraph.tsx @@ -1,23 +1,25 @@ -import { LineGraph } from 'scenes/insights/views/LineGraph/LineGraph' -import { ChartParams, GraphType, GraphDataset } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { capitalizeFirstLetter, shortTimeZone } from 'lib/utils' +import { useValues } from 'kea' import { dayjs } from 'lib/dayjs' +import { capitalizeFirstLetter, shortTimeZone } from 'lib/utils' +import { insightLogic } from 'scenes/insights/insightLogic' import { getFormattedDate } from 'scenes/insights/InsightTooltip/insightTooltipUtils' -import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' +import { LineGraph } from 'scenes/insights/views/LineGraph/LineGraph' import { buildPeopleUrl } from 'scenes/trends/persons-modal/persons-modal-utils' -import { useValues } from 'kea' -import { funnelDataLogic } from './funnelDataLogic' +import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' + import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { isInsightQueryNode } from '~/queries/utils' import { TrendsFilter } from '~/queries/schema' +import { isInsightQueryNode } from '~/queries/utils' +import { ChartParams, GraphDataset, GraphType } from '~/types' + +import { funnelDataLogic } from './funnelDataLogic' const LineGraphWrapper = ({ inCardView, children }: { inCardView?: boolean; children: JSX.Element }): JSX.Element => { if (inCardView) { return <>{children} } - return
    {children}
    + return
    {children}
    } export function FunnelLineGraph({ diff --git a/frontend/src/scenes/funnels/FunnelStepMore.tsx b/frontend/src/scenes/funnels/FunnelStepMore.tsx index 722f1d54aeb14..8f62437b3a619 100644 --- a/frontend/src/scenes/funnels/FunnelStepMore.tsx +++ b/frontend/src/scenes/funnels/FunnelStepMore.tsx @@ -1,12 +1,14 @@ -import { FunnelPathType, PathType, InsightType } from '~/types' import { useValues } from 'kea' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' import { insightLogic } from 'scenes/insights/insightLogic' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { urls } from 'scenes/urls' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { funnelDataLogic } from './funnelDataLogic' + import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { FunnelPathType, InsightType, PathType } from '~/types' + +import { funnelDataLogic } from './funnelDataLogic' type FunnelStepMoreProps = { stepIndex: number @@ -15,7 +17,7 @@ type FunnelStepMoreProps = { export function FunnelStepMore({ stepIndex }: FunnelStepMoreProps): JSX.Element | null { const { insightProps } = useValues(insightLogic) const { querySource } = useValues(funnelDataLogic(insightProps)) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const filterProps = cleanFilters(queryNodeToFilter(querySource!)) const aggregationGroupTypeIndex = querySource?.aggregation_group_type_index diff --git a/frontend/src/scenes/funnels/ValueInspectorButton.tsx b/frontend/src/scenes/funnels/ValueInspectorButton.tsx index ab11458287034..2aa8f29aaef22 100644 --- a/frontend/src/scenes/funnels/ValueInspectorButton.tsx +++ b/frontend/src/scenes/funnels/ValueInspectorButton.tsx @@ -1,6 +1,5 @@ -import { forwardRef } from 'react' - import { Link } from '@posthog/lemon-ui' +import { forwardRef } from 'react' interface ValueInspectorButtonProps { onClick?: (e?: React.MouseEvent) => void diff --git a/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.test.ts b/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.test.ts index 304dbc0b2e2cc..9d807251d6761 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.test.ts @@ -1,11 +1,11 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { DataNode } from '~/queries/schema' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' +import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' +import { DataNode } from '~/queries/schema' +import { initKeaTests } from '~/test/init' import { FunnelCorrelationResultsType, FunnelCorrelationType, InsightLogicProps, InsightType } from '~/types' -import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { funnelCorrelationDetailsLogic } from './funnelCorrelationDetailsLogic' const funnelResults = [ diff --git a/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.ts b/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.ts index 1fd130da21e74..ba55207cb746f 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.ts @@ -1,10 +1,10 @@ -import { kea, props, key, path, connect, selectors, reducers, actions } from 'kea' +import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { FunnelCorrelation, InsightLogicProps } from '~/types' -import { funnelDataLogic } from './funnelDataLogic' +import { FunnelCorrelation, InsightLogicProps } from '~/types' import type { funnelCorrelationDetailsLogicType } from './funnelCorrelationDetailsLogicType' +import { funnelDataLogic } from './funnelDataLogic' export const funnelCorrelationDetailsLogic = kea([ props({} as InsightLogicProps), diff --git a/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.test.ts b/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.test.ts index 58880d9167c04..b88946646be5c 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.test.ts @@ -1,9 +1,11 @@ -import posthog from 'posthog-js' import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { AvailableFeature, InsightLogicProps, InsightType } from '~/types' +import posthog from 'posthog-js' + import { useAvailableFeatures } from '~/mocks/features' +import { initKeaTests } from '~/test/init' +import { AvailableFeature, InsightLogicProps, InsightType } from '~/types' + import { funnelCorrelationFeedbackLogic } from './funnelCorrelationFeedbackLogic' describe('funnelCorrelationFeedbackLogic', () => { diff --git a/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.ts b/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.ts index 14d89ea0b8f42..3277c70423541 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.ts @@ -1,10 +1,11 @@ -import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea' import { lemonToast } from '@posthog/lemon-ui' +import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import type { funnelCorrelationFeedbackLogicType } from './funnelCorrelationFeedbackLogicType' import { InsightLogicProps } from '~/types' + +import type { funnelCorrelationFeedbackLogicType } from './funnelCorrelationFeedbackLogicType' import { funnelCorrelationLogic } from './funnelCorrelationLogic' import { funnelPropertyCorrelationLogic } from './funnelPropertyCorrelationLogic' diff --git a/frontend/src/scenes/funnels/funnelCorrelationLogic.ts b/frontend/src/scenes/funnels/funnelCorrelationLogic.ts index dad42e10f64b6..565be21a9d5e3 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationLogic.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationLogic.ts @@ -1,4 +1,12 @@ -import { kea, props, key, path, selectors, listeners, connect, reducers, actions, defaults } from 'kea' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, connect, defaults, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { teamLogic } from 'scenes/teamLogic' + +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { FunnelCorrelation, FunnelCorrelationResultsType, @@ -6,16 +14,9 @@ import { FunnelsFilterType, InsightLogicProps, } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import api from 'lib/api' import type { funnelCorrelationLogicType } from './funnelCorrelationLogicType' -import { loaders } from 'kea-loaders' -import { lemonToast } from '@posthog/lemon-ui' -import { teamLogic } from 'scenes/teamLogic' import { funnelDataLogic } from './funnelDataLogic' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { appendToCorrelationConfig } from './funnelUtils' export const funnelCorrelationLogic = kea([ diff --git a/frontend/src/scenes/funnels/funnelCorrelationUsageLogic.ts b/frontend/src/scenes/funnels/funnelCorrelationUsageLogic.ts index baf28ae1fc156..d2bfb449a52b2 100644 --- a/frontend/src/scenes/funnels/funnelCorrelationUsageLogic.ts +++ b/frontend/src/scenes/funnels/funnelCorrelationUsageLogic.ts @@ -1,18 +1,18 @@ import { BreakPointFunction, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { visibilitySensorLogic } from 'lib/components/VisibilitySensor/visibilitySensorLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { EntityTypes, FunnelCorrelationResultsType, FunnelsFilterType, InsightLogicProps } from '~/types' -import { visibilitySensorLogic } from 'lib/components/VisibilitySensor/visibilitySensorLogic' +import { funnelCorrelationLogic } from './funnelCorrelationLogic' import type { funnelCorrelationUsageLogicType } from './funnelCorrelationUsageLogicType' -import { insightLogic } from 'scenes/insights/insightLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { parseEventAndProperty } from './funnelUtils' -import { funnelPersonsModalLogic } from './funnelPersonsModalLogic' import { funnelDataLogic } from './funnelDataLogic' -import { funnelCorrelationLogic } from './funnelCorrelationLogic' +import { funnelPersonsModalLogic } from './funnelPersonsModalLogic' import { funnelPropertyCorrelationLogic } from './funnelPropertyCorrelationLogic' +import { parseEventAndProperty } from './funnelUtils' export const funnelCorrelationUsageLogic = kea([ props({} as InsightLogicProps), diff --git a/frontend/src/scenes/funnels/funnelDataLogic.test.ts b/frontend/src/scenes/funnels/funnelDataLogic.test.ts index 1b48816206ee3..a2d90bfc81ee2 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.test.ts @@ -1,13 +1,12 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { teamLogic } from 'scenes/teamLogic' import timekeeper from 'timekeeper' -import { teamLogic } from 'scenes/teamLogic' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' -import { funnelDataLogic } from './funnelDataLogic' - -import { FunnelConversionWindowTimeUnit, FunnelVizType, InsightLogicProps, InsightModel, InsightType } from '~/types' import { DataNode, FunnelsQuery, NodeKind } from '~/queries/schema' +import { initKeaTests } from '~/test/init' +import { FunnelConversionWindowTimeUnit, FunnelVizType, InsightLogicProps, InsightModel, InsightType } from '~/types' + import { funnelResult, funnelResultTimeToConvert, @@ -16,6 +15,7 @@ import { funnelResultWithBreakdown, funnelResultWithMultiBreakdown, } from './__mocks__/funnelDataLogicMocks' +import { funnelDataLogic } from './funnelDataLogic' let logic: ReturnType let builtDataNodeLogic: ReturnType diff --git a/frontend/src/scenes/funnels/funnelDataLogic.ts b/frontend/src/scenes/funnels/funnelDataLogic.ts index 024c41ac1e0d1..944ed7501434f 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.ts @@ -1,30 +1,32 @@ -import { kea, path, props, key, connect, selectors, actions, reducers } from 'kea' +import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea' +import { BIN_COUNT_AUTO } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { average, percentage, sum } from 'lib/utils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { groupsModel, Noun } from '~/models/groupsModel' +import { NodeKind } from '~/queries/schema' +import { isFunnelsQuery } from '~/queries/utils' import { + FlattenedFunnelStepByBreakdown, + FunnelAPIResponse, + FunnelConversionWindow, + FunnelConversionWindowTimeUnit, FunnelResultType, - FunnelVizType, - FunnelStep, FunnelStepReference, - FunnelStepWithNestedBreakdown, - InsightLogicProps, - StepOrderValue, FunnelStepWithConversionMetrics, - FlattenedFunnelStepByBreakdown, + FunnelStepWithNestedBreakdown, FunnelsTimeConversionBins, - HistogramGraphDatum, - FunnelAPIResponse, FunnelTimeConversionMetrics, + FunnelVizType, + HistogramGraphDatum, + InsightLogicProps, + StepOrderValue, TrendResult, - FunnelConversionWindowTimeUnit, - FunnelConversionWindow, } from '~/types' -import { NodeKind } from '~/queries/schema' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { groupsModel, Noun } from '~/models/groupsModel' import type { funnelDataLogicType } from './funnelDataLogicType' -import { isFunnelsQuery } from '~/queries/utils' -import { percentage, sum, average } from 'lib/utils' -import { dayjs } from 'lib/dayjs' import { aggregateBreakdownResult, aggregationLabelForHogQL, @@ -36,8 +38,6 @@ import { isBreakdownFunnelResults, stepsWithConversionMetrics, } from './funnelUtils' -import { BIN_COUNT_AUTO } from 'lib/constants' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' const DEFAULT_FUNNEL_LOGIC_KEY = 'default_funnel_key' @@ -164,7 +164,7 @@ export const funnelDataLogic = kea([ : breakdown?.breakdown ?? undefined return aggregateBreakdownResult(results, breakdownProperty).sort((a, b) => a.order - b.order) } - return (results as FunnelStep[]).sort((a, b) => a.order - b.order) + return results.sort((a, b) => a.order - b.order) } else { return [] } diff --git a/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts b/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts index f99fca6e37db1..a15b9f5466010 100644 --- a/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelPersonsModalLogic.test.ts @@ -1,15 +1,15 @@ -import { expectLogic } from 'kea-test-utils' import { router } from 'kea-router' -import { initKeaTests } from '~/test/init' - -import { funnelPersonsModalLogic } from './funnelPersonsModalLogic' -import { teamLogic } from 'scenes/teamLogic' +import { expectLogic } from 'kea-test-utils' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' - -import { InsightLogicProps, InsightShortId, InsightType } from '~/types' +import { teamLogic } from 'scenes/teamLogic' +import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' import { urls } from 'scenes/urls' + import { useMocks } from '~/mocks/jest' -import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' +import { initKeaTests } from '~/test/init' +import { InsightLogicProps, InsightShortId, InsightType } from '~/types' + +import { funnelPersonsModalLogic } from './funnelPersonsModalLogic' jest.mock('scenes/trends/persons-modal/PersonsModal') diff --git a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts index 242ebe4a6d343..d9f6c286d8043 100644 --- a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts +++ b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts @@ -1,5 +1,9 @@ import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea' +import { insightLogic } from 'scenes/insights/insightLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { funnelTitle } from 'scenes/trends/persons-modal/persons-modal-utils' +import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' + import { FunnelCorrelation, FunnelCorrelationResultsType, @@ -8,19 +12,14 @@ import { InsightLogicProps, } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelDataLogic } from './funnelDataLogic' - +import type { funnelPersonsModalLogicType } from './funnelPersonsModalLogicType' import { - getBreakdownStepValues, generateBaselineConversionUrl, + getBreakdownStepValues, parseBreakdownValue, parseEventAndProperty, } from './funnelUtils' -import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' -import { funnelTitle } from 'scenes/trends/persons-modal/persons-modal-utils' - -import type { funnelPersonsModalLogicType } from './funnelPersonsModalLogicType' const DEFAULT_FUNNEL_LOGIC_KEY = 'default_funnel_key' diff --git a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts index c9e3b8ae77be1..731ef82dba5f3 100644 --- a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.test.ts @@ -2,10 +2,12 @@ import { expectLogic, partial } from 'kea-test-utils' import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' + import { useAvailableFeatures } from '~/mocks/features' import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' import { AvailableFeature, CorrelationConfigType, InsightLogicProps, InsightShortId, InsightType } from '~/types' + import { DEFAULT_EXCLUDED_PERSON_PROPERTIES, funnelPropertyCorrelationLogic } from './funnelPropertyCorrelationLogic' const Insight123 = '123' as InsightShortId diff --git a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.ts b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.ts index 46a207dbbd290..f611b88510fc7 100644 --- a/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.ts +++ b/frontend/src/scenes/funnels/funnelPropertyCorrelationLogic.ts @@ -1,17 +1,16 @@ -import { kea, props, key, path, selectors, listeners, connect, reducers, actions, defaults } from 'kea' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, connect, defaults, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { teamLogic } from '../teamLogic' import { groupPropertiesModel } from '~/models/groupPropertiesModel' - import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType, InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { appendToCorrelationConfig } from './funnelUtils' -import api from 'lib/api' -import { lemonToast } from '@posthog/lemon-ui' -import type { funnelPropertyCorrelationLogicType } from './funnelPropertyCorrelationLogicType' +import { teamLogic } from '../teamLogic' import { funnelCorrelationLogic } from './funnelCorrelationLogic' +import type { funnelPropertyCorrelationLogicType } from './funnelPropertyCorrelationLogicType' +import { appendToCorrelationConfig } from './funnelUtils' // List of events that should be excluded, if we don't have an explicit list of // excluded properties. Copied from diff --git a/frontend/src/scenes/funnels/funnelTooltipLogic.ts b/frontend/src/scenes/funnels/funnelTooltipLogic.ts index e2c09db2705bd..3c29a96f581cf 100644 --- a/frontend/src/scenes/funnels/funnelTooltipLogic.ts +++ b/frontend/src/scenes/funnels/funnelTooltipLogic.ts @@ -1,4 +1,4 @@ -import { actions, kea, reducers, path, props, key } from 'kea' +import { actions, kea, key, path, props, reducers } from 'kea' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { FunnelStepWithConversionMetrics, InsightLogicProps } from '~/types' diff --git a/frontend/src/scenes/funnels/funnelUtils.test.ts b/frontend/src/scenes/funnels/funnelUtils.test.ts index fac6a2b82f0cd..d47b7e171a39d 100644 --- a/frontend/src/scenes/funnels/funnelUtils.test.ts +++ b/frontend/src/scenes/funnels/funnelUtils.test.ts @@ -1,12 +1,5 @@ -import { - EMPTY_BREAKDOWN_VALUES, - getBreakdownStepValues, - getIncompleteConversionWindowStartDate, - getMeanAndStandardDeviation, - getClampedStepRangeFilter, - getVisibilityKey, - parseDisplayNameForCorrelation, -} from './funnelUtils' +import { dayjs } from 'lib/dayjs' + import { FilterType, FunnelConversionWindowTimeUnit, @@ -15,7 +8,16 @@ import { FunnelCorrelationType, FunnelExclusion, } from '~/types' -import { dayjs } from 'lib/dayjs' + +import { + EMPTY_BREAKDOWN_VALUES, + getBreakdownStepValues, + getClampedStepRangeFilter, + getIncompleteConversionWindowStartDate, + getMeanAndStandardDeviation, + getVisibilityKey, + parseDisplayNameForCorrelation, +} from './funnelUtils' describe('getMeanAndStandardDeviation', () => { const arrayToExpectedValues: [number[], number[]][] = [ diff --git a/frontend/src/scenes/funnels/funnelUtils.ts b/frontend/src/scenes/funnels/funnelUtils.ts index 8dfc6a0539e73..b19dcd865fed2 100644 --- a/frontend/src/scenes/funnels/funnelUtils.ts +++ b/frontend/src/scenes/funnels/funnelUtils.ts @@ -1,31 +1,32 @@ +import { combineUrl } from 'kea-router' +import { FunnelLayout } from 'lib/constants' +import { dayjs } from 'lib/dayjs' import { autoCaptureEventToDescription, clamp } from 'lib/utils' +import { elementsToAction } from 'scenes/events/createActionFromEvent' +import { teamLogic } from 'scenes/teamLogic' + +import { Noun } from '~/models/groupsModel' +import { FunnelsQuery } from '~/queries/schema' import { - FunnelExclusion, - FunnelStep, - FunnelStepWithNestedBreakdown, + AnyPropertyFilter, + Breakdown, BreakdownKeyType, - FunnelResultType, - FunnelStepReference, + CorrelationConfigType, + ElementPropertyFilter, + FlattenedFunnelStepByBreakdown, FunnelConversionWindow, + FunnelCorrelation, + FunnelCorrelationResultsType, + FunnelExclusion, + FunnelResultType, FunnelsFilterType, - Breakdown, + FunnelStep, + FunnelStepReference, FunnelStepWithConversionMetrics, - FlattenedFunnelStepByBreakdown, - FunnelCorrelation, - AnyPropertyFilter, - PropertyOperator, - ElementPropertyFilter, + FunnelStepWithNestedBreakdown, PropertyFilterType, - FunnelCorrelationResultsType, - CorrelationConfigType, + PropertyOperator, } from '~/types' -import { dayjs } from 'lib/dayjs' -import { combineUrl } from 'kea-router' -import { FunnelsQuery } from '~/queries/schema' -import { FunnelLayout } from 'lib/constants' -import { elementsToAction } from 'scenes/events/createActionFromEvent' -import { teamLogic } from 'scenes/teamLogic' -import { Noun } from '~/models/groupsModel' /** Chosen via heuristics by eyeballing some values * Assuming a normal distribution, then 90% of values are within 1.5 standard deviations of the mean @@ -600,7 +601,7 @@ export const parseDisplayNameForCorrelation = ( first_value = autoCaptureEventToDescription({ ...record.event, event: '$autocapture', - }) as string + }) return { first_value, second_value } } else { // FunnelCorrelationResultsType.EventWithProperties diff --git a/frontend/src/scenes/funnels/useFunnelTooltip.tsx b/frontend/src/scenes/funnels/useFunnelTooltip.tsx index c0d311e6b7361..d6faa42f4e8ac 100644 --- a/frontend/src/scenes/funnels/useFunnelTooltip.tsx +++ b/frontend/src/scenes/funnels/useFunnelTooltip.tsx @@ -1,21 +1,23 @@ import { useValues } from 'kea' -import { useEffect, useRef } from 'react' -import { FunnelStepWithConversionMetrics } from '~/types' +import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonRow } from 'lib/lemon-ui/LemonRow' import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' -import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' import { humanFriendlyDuration, humanFriendlyNumber, percentage } from 'lib/utils' +import { useEffect, useRef } from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' +import { ClickToInspectActors } from 'scenes/insights/InsightTooltip/InsightTooltip' +import { formatBreakdownLabel } from 'scenes/insights/utils' +import { getActionFilterFromFunnelStep } from 'scenes/insights/views/Funnels/funnelStepTableUtils' import { ensureTooltip } from 'scenes/insights/views/LineGraph/LineGraph' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + import { cohortsModel } from '~/models/cohortsModel' -import { ClickToInspectActors } from 'scenes/insights/InsightTooltip/InsightTooltip' import { groupsModel } from '~/models/groupsModel' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatBreakdownLabel } from 'scenes/insights/utils' import { BreakdownFilter } from '~/queries/schema' +import { FunnelStepWithConversionMetrics } from '~/types' + import { funnelDataLogic } from './funnelDataLogic' -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelTooltipLogic } from './funnelTooltipLogic' /** The tooltip is offset horizontally by a few pixels from the bar to give it some breathing room. */ diff --git a/frontend/src/scenes/groups/Group.tsx b/frontend/src/scenes/groups/Group.tsx index 21c53e74133f4..41fc57fcb6b03 100644 --- a/frontend/src/scenes/groups/Group.tsx +++ b/frontend/src/scenes/groups/Group.tsx @@ -1,22 +1,23 @@ import { useActions, useValues } from 'kea' +import { router } from 'kea-router' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' import { PropertiesTable } from 'lib/components/PropertiesTable' import { TZLabel } from 'lib/components/TZLabel' -import { GroupLogicProps, groupLogic } from 'scenes/groups/groupLogic' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Spinner, SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { GroupDashboard } from 'scenes/groups/GroupDashboard' +import { groupLogic, GroupLogicProps } from 'scenes/groups/groupLogic' import { RelatedGroups } from 'scenes/groups/RelatedGroups' -import { SceneExport } from 'scenes/sceneTypes' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { Group as IGroup, NotebookNodeType, PersonsTabType, PropertyDefinitionType } from '~/types' -import { PageHeader } from 'lib/components/PageHeader' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { Spinner, SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { NotFound } from 'lib/components/NotFound' import { RelatedFeatureFlags } from 'scenes/persons/RelatedFeatureFlags' -import { Query } from '~/queries/Query/Query' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { GroupDashboard } from 'scenes/groups/GroupDashboard' -import { router } from 'kea-router' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' + +import { Query } from '~/queries/Query/Query' +import { Group as IGroup, NotebookNodeType, PersonsTabType, PropertyDefinitionType } from '~/types' interface GroupSceneProps { groupTypeIndex?: string diff --git a/frontend/src/scenes/groups/GroupDashboard.tsx b/frontend/src/scenes/groups/GroupDashboard.tsx index aaf4bd166d7c9..4f38d22b367cf 100644 --- a/frontend/src/scenes/groups/GroupDashboard.tsx +++ b/frontend/src/scenes/groups/GroupDashboard.tsx @@ -1,14 +1,15 @@ -import { useActions, useValues } from 'kea' -import { Dashboard } from 'scenes/dashboard/Dashboard' -import { Scene } from 'scenes/sceneTypes' -import { Group, GroupPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types' -import { groupDashboardLogic } from 'scenes/groups/groupDashboardLogic' import { LemonButton } from '@posthog/lemon-ui' -import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' +import { useActions, useValues } from 'kea' import { SceneDashboardChoiceModal } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceModal' import { sceneDashboardChoiceModalLogic } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' import { useEffect } from 'react' +import { Dashboard } from 'scenes/dashboard/Dashboard' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { groupDashboardLogic } from 'scenes/groups/groupDashboardLogic' +import { Scene } from 'scenes/sceneTypes' + +import { Group, GroupPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types' export function GroupDashboard({ groupData }: { groupData: Group }): JSX.Element { const { showSceneDashboardChoiceModal } = useActions(sceneDashboardChoiceModalLogic({ scene: Scene.Group })) diff --git a/frontend/src/scenes/groups/Groups.tsx b/frontend/src/scenes/groups/Groups.tsx index ec50e9950543c..c84835d24ac7a 100644 --- a/frontend/src/scenes/groups/Groups.tsx +++ b/frontend/src/scenes/groups/Groups.tsx @@ -1,20 +1,22 @@ import { useActions, useValues } from 'kea' -import { Group, PropertyDefinitionType } from '~/types' -import { groupsListLogic } from './groupsListLogic' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { PropertiesTable } from 'lib/components/PropertiesTable' -import { LemonTableColumns } from 'lib/lemon-ui/LemonTable/types' import { TZLabel } from 'lib/components/TZLabel' -import { LemonTable } from 'lib/lemon-ui/LemonTable' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' -import { GroupsIntroduction } from 'scenes/groups/GroupsIntroduction' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' -import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { LemonTableColumns } from 'lib/lemon-ui/LemonTable/types' +import { Link } from 'lib/lemon-ui/Link' import { capitalizeFirstLetter } from 'lib/utils' +import { GroupsIntroduction } from 'scenes/groups/GroupsIntroduction' +import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { urls } from 'scenes/urls' + +import { Group, PropertyDefinitionType } from '~/types' + +import { groupsListLogic } from './groupsListLogic' export function Groups({ groupTypeIndex }: { groupTypeIndex: number }): JSX.Element { const { diff --git a/frontend/src/scenes/groups/GroupsIntroduction.tsx b/frontend/src/scenes/groups/GroupsIntroduction.tsx index dde05b7e91c94..eb2bda5801ff9 100644 --- a/frontend/src/scenes/groups/GroupsIntroduction.tsx +++ b/frontend/src/scenes/groups/GroupsIntroduction.tsx @@ -1,7 +1,8 @@ -import { GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' +import { Link } from '@posthog/lemon-ui' import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' +import { GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' + import { AvailableFeature } from '~/types' -import { Link } from '@posthog/lemon-ui' interface Props { access: GroupsAccessStatus.NoAccess | GroupsAccessStatus.HasAccess | GroupsAccessStatus.HasGroupTypes diff --git a/frontend/src/scenes/groups/RelatedGroups.tsx b/frontend/src/scenes/groups/RelatedGroups.tsx index 396ff4150be9f..c82ee3488aa33 100644 --- a/frontend/src/scenes/groups/RelatedGroups.tsx +++ b/frontend/src/scenes/groups/RelatedGroups.tsx @@ -1,12 +1,13 @@ +import { IconPerson } from '@posthog/icons' import { useValues } from 'kea' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { ActorType } from '~/types' -import { groupsModel } from '~/models/groupsModel' import { capitalizeFirstLetter } from 'lib/utils' -import { PersonDisplay } from 'scenes/persons/PersonDisplay' import { relatedGroupsLogic } from 'scenes/groups/relatedGroupsLogic' import { GroupActorDisplay } from 'scenes/persons/GroupActorDisplay' -import { IconPerson } from '@posthog/icons' +import { PersonDisplay } from 'scenes/persons/PersonDisplay' + +import { groupsModel } from '~/models/groupsModel' +import { ActorType } from '~/types' interface Props { groupTypeIndex: number | null diff --git a/frontend/src/scenes/groups/groupDashboardLogic.ts b/frontend/src/scenes/groups/groupDashboardLogic.ts index 01c46d05946ae..1714c529365ab 100644 --- a/frontend/src/scenes/groups/groupDashboardLogic.ts +++ b/frontend/src/scenes/groups/groupDashboardLogic.ts @@ -1,11 +1,11 @@ -import { connect, kea, selectors, path } from 'kea' +import { connect, kea, path, selectors } from 'kea' +import { DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' +import { Scene } from 'scenes/sceneTypes' +import { userLogic } from 'scenes/userLogic' import { DashboardPlacement } from '~/types' -import { Scene } from 'scenes/sceneTypes' import type { groupDashboardLogicType } from './groupDashboardLogicType' -import { DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' -import { userLogic } from 'scenes/userLogic' export const groupDashboardLogic = kea([ path(['scenes', 'groups', 'groupDashboardLogic']), diff --git a/frontend/src/scenes/groups/groupLogic.ts b/frontend/src/scenes/groups/groupLogic.ts index dce2cfdc6e04b..5fa0ab912d69f 100644 --- a/frontend/src/scenes/groups/groupLogic.ts +++ b/frontend/src/scenes/groups/groupLogic.ts @@ -1,20 +1,23 @@ import { actions, afterMount, connect, kea, key, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { urlToAction } from 'kea-router' import api from 'lib/api' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { toParams } from 'lib/utils' -import { teamLogic } from 'scenes/teamLogic' -import { groupsModel } from '~/models/groupsModel' -import { Breadcrumb, Group, GroupTypeIndex, PropertyFilterType, PropertyOperator } from '~/types' -import type { groupLogicType } from './groupLogicType' -import { urls } from 'scenes/urls' import { capitalizeFirstLetter } from 'lib/utils' import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { DataTableNode, Node, NodeKind } from '~/queries/schema' +import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { groupsModel } from '~/models/groupsModel' import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' +import { DataTableNode, Node, NodeKind } from '~/queries/schema' import { isDataTableNode } from '~/queries/utils' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { loaders } from 'kea-loaders' -import { urlToAction } from 'kea-router' +import { Breadcrumb, Group, GroupTypeIndex, PropertyFilterType, PropertyOperator } from '~/types' + +import type { groupLogicType } from './groupLogicType' function getGroupEventsQuery(groupTypeIndex: number, groupKey: string): DataTableNode { return { @@ -104,10 +107,17 @@ export const groupLogic = kea([ (s, p) => [s.groupTypeName, p.groupTypeIndex, p.groupKey, s.groupData], (groupTypeName, groupTypeIndex, groupKey, groupData): Breadcrumb[] => [ { + key: Scene.DataManagement, + name: 'People', + path: urls.persons(), + }, + { + key: groupTypeIndex, name: capitalizeFirstLetter(groupTypeName), path: urls.groups(String(groupTypeIndex)), }, { + key: `${groupTypeIndex}-${groupKey}`, name: groupDisplayId(groupKey, groupData?.group_properties || {}), path: urls.group(String(groupTypeIndex), groupKey), }, diff --git a/frontend/src/scenes/groups/groupsListLogic.ts b/frontend/src/scenes/groups/groupsListLogic.ts index 639aecd2ee007..212545077ae10 100644 --- a/frontend/src/scenes/groups/groupsListLogic.ts +++ b/frontend/src/scenes/groups/groupsListLogic.ts @@ -1,10 +1,12 @@ +import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, afterMount } from 'kea' import api from 'lib/api' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' import { teamLogic } from 'scenes/teamLogic' -import { Noun, groupsModel } from '~/models/groupsModel' + +import { groupsModel, Noun } from '~/models/groupsModel' import { Group } from '~/types' + import type { groupsListLogicType } from './groupsListLogicType' export interface GroupsPaginatedResponse { diff --git a/frontend/src/scenes/groups/relatedGroupsLogic.ts b/frontend/src/scenes/groups/relatedGroupsLogic.ts index 60c9d224d80be..a5088d95ddd3e 100644 --- a/frontend/src/scenes/groups/relatedGroupsLogic.ts +++ b/frontend/src/scenes/groups/relatedGroupsLogic.ts @@ -1,9 +1,11 @@ +import { actions, connect, events, kea, key, path, props } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, events } from 'kea' import api from 'lib/api' import { toParams } from 'lib/utils' import { teamLogic } from 'scenes/teamLogic' + import { ActorType } from '~/types' + import type { relatedGroupsLogicType } from './relatedGroupsLogicType' export const relatedGroupsLogic = kea([ diff --git a/frontend/src/scenes/ingestion/CardContainer.tsx b/frontend/src/scenes/ingestion/CardContainer.tsx deleted file mode 100644 index ed26c428d8101..0000000000000 --- a/frontend/src/scenes/ingestion/CardContainer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { PanelFooter } from './panels/PanelComponents' -import './panels/Panels.scss' -import { IngestionState } from 'scenes/ingestion/ingestionLogic' - -export function CardContainer({ - children, - nextProps, - onContinue, - finalStep = false, - showInviteTeamMembers = true, -}: { - children: React.ReactNode - nextProps?: Partial - onContinue?: () => void - finalStep?: boolean - showInviteTeamMembers?: boolean -}): JSX.Element { - return ( - // We want a forced width for this view only - // eslint-disable-next-line react/forbid-dom-props -
    - {children} -
    - {nextProps && ( - - )} -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx deleted file mode 100644 index 439390bd83164..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { LemonButton } from '@posthog/lemon-ui' -import { useActions } from 'kea' -import { IconArrowRight } from 'lib/lemon-ui/icons' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' - -export function IngestionInviteMembersButton(): JSX.Element { - const { showInviteModal } = useActions(inviteLogic) - const { reportInviteMembersButtonClicked } = useActions(eventUsageLogic) - - return ( - } - className="mt-6" - onClick={() => { - showInviteModal() - reportInviteMembersButtonClicked() - }} - > - Invite a team member to help with this step - - ) -} diff --git a/frontend/src/scenes/ingestion/IngestionWizard.scss b/frontend/src/scenes/ingestion/IngestionWizard.scss deleted file mode 100644 index e1d2b422bdf7c..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionWizard.scss +++ /dev/null @@ -1,77 +0,0 @@ -.IngestionContainer { - display: flex; - height: 100%; - align-items: center; - justify-content: center; - padding: 2rem; - flex-direction: column; - width: 100%; -} - -.IngestionContent { - .BridgePage__content { - max-width: 700px; - } -} - -.IngestionTopbar { - border-bottom: 1px solid var(--border); - padding: 0.25rem 1rem; - display: flex; - justify-content: space-between; - position: sticky; - top: 0; - background-color: white; - width: 100%; - z-index: 10; - - .help-button { - margin-right: 1rem; - } -} - -.platform-item { - margin-right: 10px; - padding: 10px; - padding-left: 20px; - padding-right: 20px; - border: 1px solid gray; - border-radius: 2px; - cursor: pointer; -} - -.platform-item:hover { - background-color: gainsboro; -} - -.selectable-item:hover { - background-color: gainsboro; - cursor: pointer; -} - -.IngestionSidebar__bottom { - margin-top: auto; - - .popover { - padding-left: 0.5rem; - padding-right: 0.5rem; - } -} - -.IngestionSidebar__help { - display: flex; - flex-direction: column; - font-weight: 500; - color: var(--primary); - margin-top: 1rem; -} - -.IngestionSidebar__steps { - color: var(--muted-alt); - font-size: 14px; - - .LemonButton { - font-weight: 600; - margin-bottom: 0.5rem; - } -} diff --git a/frontend/src/scenes/ingestion/IngestionWizard.tsx b/frontend/src/scenes/ingestion/IngestionWizard.tsx deleted file mode 100644 index 80c8d8c8690a2..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionWizard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from 'react' -import './IngestionWizard.scss' - -import { VerificationPanel } from 'scenes/ingestion/panels/VerificationPanel' -import { InstructionsPanel } from 'scenes/ingestion/panels/InstructionsPanel' -import { useValues, useActions } from 'kea' -import { ingestionLogic, INGESTION_VIEWS } from 'scenes/ingestion/ingestionLogic' -import { FrameworkPanel } from 'scenes/ingestion/panels/FrameworkPanel' -import { PlatformPanel } from 'scenes/ingestion/panels/PlatformPanel' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { GeneratingDemoDataPanel } from './panels/GeneratingDemoDataPanel' -import { ThirdPartyPanel } from './panels/ThirdPartyPanel' -import { BillingPanel } from './panels/BillingPanel' -import { Sidebar } from './Sidebar' -import { InviteModal } from 'scenes/settings/organization/InviteModal' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import { Logo } from '~/toolbar/assets/Logo' -import { SitePopover } from '~/layout/navigation/TopBar/SitePopover' -import { HelpButton } from 'lib/components/HelpButton/HelpButton' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { PanelHeader } from './panels/PanelComponents' -import { InviteTeamPanel } from './panels/InviteTeamPanel' -import { TeamInvitedPanel } from './panels/TeamInvitedPanel' -import { NoDemoIngestionPanel } from './panels/NoDemoIngestionPanel' -import { SuperpowersPanel } from 'scenes/ingestion/panels/SuperpowersPanel' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' - -export function IngestionWizard(): JSX.Element { - const { currentView, platform } = useValues(ingestionLogic) - const { reportIngestionLandingSeen } = useActions(eventUsageLogic) - const { featureFlags } = useValues(featureFlagLogic) - - useEffect(() => { - if (!platform) { - reportIngestionLandingSeen() - } - }, [platform]) - - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] === 'test') { - router.actions.replace(urls.products()) - } - - return ( - - {currentView === INGESTION_VIEWS.BILLING && } - {currentView === INGESTION_VIEWS.SUPERPOWERS && } - {currentView === INGESTION_VIEWS.INVITE_TEAM && } - {currentView === INGESTION_VIEWS.TEAM_INVITED && } - {currentView === INGESTION_VIEWS.CHOOSE_PLATFORM && } - {currentView === INGESTION_VIEWS.CHOOSE_FRAMEWORK && } - {currentView === INGESTION_VIEWS.WEB_INSTRUCTIONS && } - {currentView === INGESTION_VIEWS.VERIFICATION && } - {currentView === INGESTION_VIEWS.GENERATING_DEMO_DATA && } - {currentView === INGESTION_VIEWS.CHOOSE_THIRD_PARTY && } - {currentView === INGESTION_VIEWS.NO_DEMO_INGESTION && } - - ) -} - -function IngestionContainer({ children }: { children: React.ReactNode }): JSX.Element { - const { isInviteModalShown } = useValues(inviteLogic) - const { hideInviteModal } = useActions(inviteLogic) - const { isSmallScreen } = useValues(ingestionLogic) - - return ( -
    -
    - -
    - - -
    -
    -
    - {!isSmallScreen && } - {/*
    } - className="IngestionContent h-full" - fullScreen={false} - > - {children} - -
    - -
    - ) -} diff --git a/frontend/src/scenes/ingestion/Sidebar.tsx b/frontend/src/scenes/ingestion/Sidebar.tsx deleted file mode 100644 index b3f656cee12e7..0000000000000 --- a/frontend/src/scenes/ingestion/Sidebar.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { ingestionLogic } from './ingestionLogic' -import { useActions, useValues } from 'kea' -import './IngestionWizard.scss' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { IconArticle, IconQuestionAnswer } from 'lib/lemon-ui/icons' -import { HelpType } from '~/types' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' -import { Lettermark } from 'lib/lemon-ui/Lettermark' -import { organizationLogic } from 'scenes/organizationLogic' -import { Link } from '@posthog/lemon-ui' - -const HELP_UTM_TAGS = '?utm_medium=in-product-onboarding&utm_campaign=help-button-sidebar' - -export function Sidebar(): JSX.Element { - const { currentStep, sidebarSteps, isProjectSwitcherShown } = useValues(ingestionLogic) - const { sidebarStepClick, toggleProjectSwitcher, hideProjectSwitcher } = useActions(ingestionLogic) - const { reportIngestionHelpClicked, reportIngestionSidebarButtonClicked } = useActions(eventUsageLogic) - const { currentOrganization } = useValues(organizationLogic) - - const currentIndex = sidebarSteps.findIndex((x) => x === currentStep) - - return ( -
    -
    -
    - {sidebarSteps.map((step: string, index: number) => ( - currentIndex} - onClick={() => { - sidebarStepClick(step) - reportIngestionSidebarButtonClicked(step) - }} - > - {step} - - ))} -
    -
    - {currentOrganization?.teams && currentOrganization.teams.length > 1 && ( - <> - } - onClick={() => toggleProjectSwitcher()} - dropdown={{ - visible: isProjectSwitcherShown, - onClickOutside: hideProjectSwitcher, - overlay: , - actionable: true, - placement: 'top-end', - }} - type="secondary" - fullWidth - > - Switch project - - - - )} -
    - - } - fullWidth - onClick={() => { - reportIngestionHelpClicked(HelpType.Slack) - }} - > - Get support on Slack - - - - } - fullWidth - onClick={() => { - reportIngestionHelpClicked(HelpType.Docs) - }} - > - Read our documentation - - -
    -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/constants.tsx b/frontend/src/scenes/ingestion/constants.tsx deleted file mode 100644 index 50fefffadbf4a..0000000000000 --- a/frontend/src/scenes/ingestion/constants.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { PlatformType } from 'scenes/ingestion/types' -import { Segment, RSS } from './panels/ThirdPartyIcons' - -export const TECHNICAL = 'TECHNICAL' -export const PLATFORM_TYPE = 'PLATFORM_TYPE' -export const FRAMEWORK = 'FRAMEWORK' -export const INSTRUCTIONS = 'INSTRUCTIONS' -export const VERIFICATION = 'VERIFICATION' - -export const WEB = 'web' -export const MOBILE = 'mobile' -export const BACKEND = 'backend' -export const THIRD_PARTY = 'third-party' -export const platforms: PlatformType[] = [WEB, MOBILE, BACKEND] - -export const NODEJS = 'NODEJS' -export const GO = 'GO' -export const RUBY = 'RUBY' -export const PYTHON = 'PYTHON' -export const PHP = 'PHP' -export const ELIXIR = 'ELIXIR' -export const API = 'API' - -export const ANDROID = 'ANDROID' -export const IOS = 'IOS' -export const REACT_NATIVE = 'REACT_NATIVE' -export const FLUTTER = 'FLUTTER' - -export const httpFrameworks = { - [API]: 'HTTP API', -} -export const webFrameworks = { - [NODEJS]: 'Node.js', - [GO]: 'Go', - [RUBY]: 'Ruby', - [PYTHON]: 'Python', - [PHP]: 'PHP', - [ELIXIR]: 'Elixir', -} - -export const mobileFrameworks = { - [ANDROID]: 'Android', - [IOS]: 'iOS', - [REACT_NATIVE]: 'React Native', - [FLUTTER]: 'Flutter', -} - -export const allFrameworks = { - ...webFrameworks, - ...mobileFrameworks, - ...httpFrameworks, -} -export interface ThirdPartySource { - name: string - icon: JSX.Element - docsLink: string - aboutLink: string - labels?: string[] - description?: string -} - -export const thirdPartySources: ThirdPartySource[] = [ - { - name: 'Segment', - icon: , - docsLink: 'https://posthog.com/docs/integrate/third-party/segment', - aboutLink: 'https://segment.com', - }, - { - name: 'Rudderstack', - icon: ( - - ), - docsLink: 'https://posthog.com/docs/integrate/third-party/rudderstack', - aboutLink: 'https://rudderstack.com', - }, - { - name: 'RSS items', - description: 'Send events from releases, blog posts, status pages, or any other RSS feed into PostHog', - icon: , - docsLink: 'https://posthog.com/tutorials/rss-item-capture', - aboutLink: 'https://en.wikipedia.org/wiki/RSS', - }, -] diff --git a/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx deleted file mode 100644 index 3ac66f7281952..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function APISnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'POST ' + - url + - '/capture/\nContent-Type: application/json\n\n{\n\t"api_key": "' + - currentTeam?.api_token + - '",\n\t"event": "[event name]",\n\t"properties": {\n\t\t"distinct_id": "[your users\' distinct id]",\n\t\t"key1": "value1",\n\t\t"key2": "value2"\n\t},\n\t"timestamp": "[optional timestamp in ISO 8601 format]"\n}'} - - ) -} - -export function APIInstructions(): JSX.Element { - return ( - <> -

    Usage

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx deleted file mode 100644 index 1e7d262f36640..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function AndroidInstallSnippet(): JSX.Element { - return ( - - {`dependencies { - implementation 'com.posthog.android:posthog:1.+' -}`} - - ) -} - -function AndroidSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`public class SampleApp extends Application { - private static final String POSTHOG_API_KEY = "${currentTeam?.api_token}"; - private static final String POSTHOG_HOST = "${window.location.origin}"; - - @Override - public void onCreate() { - // Create a PostHog client with the given context, API key and host - PostHog posthog = new PostHog.Builder(this, POSTHOG_API_KEY, POSTHOG_HOST) - .captureApplicationLifecycleEvents() // Record certain application events automatically! - .recordScreenViews() // Record screen views automatically! - .build(); - - // Set the initialized instance as a globally accessible instance - PostHog.setSingletonInstance(posthog); - - // Now any time you call PostHog.with, the custom instance will be returned - PostHog posthog = PostHog.with(this); - }`} - - ) -} - -function AndroidCaptureSnippet(): JSX.Element { - return PostHog.with(this).capture("test-event"); -} - -export function AndroidInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx deleted file mode 100644 index 7d476a6630db0..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function ElixirInstallSnippet(): JSX.Element { - return ( - - {'def deps do\n [\n {:posthog, "~> 0.1"}\n ]\nend'} - - ) -} - -function ElixirSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'config :posthog,\n api_url: "' + url + '",\n api_key: "' + currentTeam?.api_token + '"'} - - ) -} - -export function ElixirInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx deleted file mode 100644 index 46d496917a9b9..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function FlutterInstallSnippet(): JSX.Element { - return {'posthog_flutter: # insert version number'} -} - -function FlutterCaptureSnippet(): JSX.Element { - return ( - - { - "import 'package:posthog_flutter/posthog_flutter.dart';\n\nPosthog().screen(\n\tscreenName: 'Example Screen',\n);" - } - - ) -} - -function FlutterAndroidSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'\n\t\n\t\t[...]\n\t\n\t\n\t\n\t\n\t\n'} - - ) -} - -function FlutterIOSSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'\n\t[...]\n\tcom.posthog.posthog.API_KEY\n\t' + - currentTeam?.api_token + - '\n\tcom.posthog.posthog.POSTHOG_HOST\n\t' + - url + - '\n\tcom.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS\n\t\n\t[...]\n'} - - ) -} - -export function FlutterInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Android Setup

    -

    {'Add these values in AndroidManifest.xml'}

    - -

    iOS Setup

    -

    {'Add these values in Info.plist'}

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx deleted file mode 100644 index 5dc33e57499c4..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function GoInstallSnippet(): JSX.Element { - return {'go get "github.com/posthog/posthog-go"'} -} - -function GoSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`package main -import ( - "github.com/posthog/posthog-go" -) -func main() { - client, _ := posthog.NewWithConfig("${currentTeam?.api_token}", posthog.Config{Endpoint: "${window.location.origin}"}) - defer client.Close() -}`} - - ) -} - -function GoCaptureSnippet(): JSX.Element { - return ( - - {'client.Enqueue(posthog.Capture{\n DistinctId: "test-user",\n Event: "test-snippet",\n})'} - - ) -} - -export function GoInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx deleted file mode 100644 index 46551b2627d32..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function NodeInstallSnippet(): JSX.Element { - return ( - - {`npm install posthog-node -# OR -yarn add posthog-node -# OR -pnpm add posthog-node`} - - ) -} - -function NodeSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`import { PostHog } from 'posthog-node' - -const client = new PostHog( - '${currentTeam?.api_token}', - { host: '${window.location.origin}' } -)`} - - ) -} - -function NodeCaptureSnippet(): JSX.Element { - return ( - - {`client.capture({ - distinctId: 'test-id', - event: 'test-event' -}) - -// Send queued events immediately. Use for example in a serverless environment -// where the program may terminate before everything is sent -client.flush()`} - - ) -} - -export function NodeInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx deleted file mode 100644 index c9dc1665ab977..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function PHPConfigSnippet(): JSX.Element { - return ( - - {`{ - "require": { - "posthog/posthog-php": "1.0.*" - } -}`} - - ) -} - -function PHPInstallSnippet(): JSX.Element { - return {'php composer.phar install'} -} - -function PHPSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`PostHog::init('${currentTeam?.api_token}', - array('host' => '${window.location.origin}') -);`} - - ) -} - -function PHPCaptureSnippet(): JSX.Element { - return ( - - {"PostHog::capture(array(\n 'distinctId' => 'test-user',\n 'event' => 'test-event'\n));"} - - ) -} - -export function PHPInstructions(): JSX.Element { - return ( - <> -

    Dependency Setup

    - -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx deleted file mode 100644 index 0d596f57e50e1..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function PythonInstallSnippet(): JSX.Element { - return {'pip install posthog'} -} - -function PythonSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`from posthog import Posthog - -posthog = Posthog(project_api_key='${currentTeam?.api_token}', host='${window.location.origin}') - - `} - - ) -} - -function PythonCaptureSnippet(): JSX.Element { - return {"posthog.capture('test-id', 'test-event')"} -} - -export function PythonInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx deleted file mode 100644 index 76298cc842821..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { Link } from '@posthog/lemon-ui' - -export function RNInstructions(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - <> -

    Install

    - - {`# Expo apps -expo install posthog-react-native expo-file-system expo-application expo-device expo-localization - -# Standard React Native apps -yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info -# or -npm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info - -# for iOS -cd ios -pod install`} - -

    Configure

    -

    - PostHog is most easily used via the PostHogProvider component but if you need to - instantiate it directly,{' '} - - check out the docs - {' '} - which explain how to do this correctly. -

    - - {`// App.(js|ts) -import { PostHogProvider } from 'posthog-react-native' -... - -export function MyApp() { - return ( - - - - ) -}`} - -

    Send an Event

    - {`// With hooks -import { usePostHog } from 'posthog-react-native' - -const MyComponent = () => { - const posthog = usePostHog() - - useEffect(() => { - posthog.capture("MyComponent loaded", { foo: "bar" }) - }, []) -} - `} - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx deleted file mode 100644 index b3a944a8ff11f..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function RubyInstallSnippet(): JSX.Element { - return {'gem "posthog-ruby"'} -} - -function RubySetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`posthog = PostHog::Client.new({ - api_key: "${currentTeam?.api_token}", - host: "${window.location.origin}", - on_error: Proc.new { |status, msg| print msg } -})`} - - ) -} - -function RubyCaptureSnippet(): JSX.Element { - return ( - - {"posthog.capture({\n distinct_id: 'test-id',\n event: 'test-event'})"} - - ) -} - -export function RubyInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure

    - -

    Send an Event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx deleted file mode 100644 index 4decaf1051d98..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Link } from 'lib/lemon-ui/Link' -import { JSSnippet } from 'lib/components/JSSnippet' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function JSInstallSnippet(): JSX.Element { - return ( - - {['npm install posthog-js', '# OR', 'yarn add posthog-js', '# OR', 'pnpm add posthog-js'].join('\n')} - - ) -} - -function JSSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {[ - "import posthog from 'posthog-js'", - '', - `posthog.init('${currentTeam?.api_token}', { api_host: '${window.location.origin}' })`, - ].join('\n')} - - ) -} - -function JSEventSnippet(): JSX.Element { - return ( - {`posthog.capture('my event', { property: 'value' })`} - ) -} - -export function WebInstructions(): JSX.Element { - return ( - <> -

    Connect your web app or product

    -
    -

    Option 1. Code snippet

    -
    - Recommended -
    -
    -

    - Just add this snippet to your website and we'll automatically capture page views, sessions and all - relevant interactions within your website.{' '} - - Learn more - - . -

    -

    Install the snippet

    -

    - Insert this snippet in your website within the <head> tag. -

    -

    Send events

    -

    Visit your site and click around to generate some initial events.

    - -
    -

    Option 2. Javascript Library

    -
    -

    - Use this option if you want more granular control of how PostHog runs in your website and the events you - capture. Recommended for teams with more stable products and more defined analytics requirements.{' '} - - Learn more - - . -

    -

    Install the package

    - -

    - Configure & initialize (see more{' '} - - configuration options - - ) -

    - -

    Send your first event

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx deleted file mode 100644 index 7ccc1ae487fc3..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' - -function IOSInstallSnippet(): JSX.Element { - return ( - - {'pod "PostHog", "~> 1.0" # Cocoapods \n# OR \ngithub "posthog/posthog-ios" # Carthage'} - - ) -} - -function IOS_OBJ_C_SetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`#import \n#import \n\nPHGPostHogConfiguration *configuration = [PHGPostHogConfiguration configurationWithApiKey:@"${currentTeam?.api_token}" host:@"${window.location.origin}"];\n\nconfiguration.captureApplicationLifecycleEvents = YES; // Record certain application events automatically!\nconfiguration.recordScreenViews = YES; // Record screen views automatically!\n\n[PHGPostHog setupWithConfiguration:configuration];`} - - ) -} - -function IOS_SWIFT_SetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`import PostHog\n\nlet configuration = PHGPostHogConfiguration(apiKey: "${currentTeam?.api_token}", host: "${window.location.origin}")\n\nconfiguration.captureApplicationLifecycleEvents = true; // Record certain application events automatically!\nconfiguration.recordScreenViews = true; // Record screen views automatically!\n\nPHGPostHog.setup(with: configuration)\nlet posthog = PHGPostHog.shared()`} - - ) -} - -function IOS_OBJ_C_CaptureSnippet(): JSX.Element { - return ( - - {'[[PHGPostHog sharedPostHog] capture:@"Test Event"];'} - - ) -} - -function IOS_SWIFT_CaptureSnippet(): JSX.Element { - return {'posthog.capture("Test Event")'} -} - -export function IOSInstructions(): JSX.Element { - return ( - <> -

    Install

    - -

    Configure Swift

    - -

    Or configure Objective-C

    - -

    Send an event with swift

    - -

    Send an event with Objective-C

    - - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/index.tsx b/frontend/src/scenes/ingestion/frameworks/index.tsx deleted file mode 100644 index 71597b3648c6b..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export * from './AndroidInstructions' -export * from './GoInstructions' -export * from './NodeInstructions' -export * from './iOSInstructions' -export * from './PHPInstructions' -export * from './PythonInstructions' -export * from './ReactNativeInstructions' -export * from './RubyInstructions' -export * from './APIInstructions' -export * from './ElixirInstructions' -export * from './FlutterInstructions' diff --git a/frontend/src/scenes/ingestion/ingestionLogic.ts b/frontend/src/scenes/ingestion/ingestionLogic.ts deleted file mode 100644 index cf64522b8e27e..0000000000000 --- a/frontend/src/scenes/ingestion/ingestionLogic.ts +++ /dev/null @@ -1,717 +0,0 @@ -import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' -import { Framework, PlatformType } from 'scenes/ingestion/types' -import { API, MOBILE, BACKEND, WEB, thirdPartySources, THIRD_PARTY, ThirdPartySource } from './constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { teamLogic } from 'scenes/teamLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { urls } from 'scenes/urls' -import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' -import { getBreakpoint } from 'lib/utils/responsiveUtils' -import { windowValues } from 'kea-window-values' -import { subscriptions } from 'kea-subscriptions' -import { TeamType } from '~/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import api from 'lib/api' -import { loaders } from 'kea-loaders' -import type { ingestionLogicType } from './ingestionLogicType' - -export enum INGESTION_STEPS { - START = 'Get started', - PLATFORM = 'Select your platform', - CONNECT_PRODUCT = 'Connect your product', - VERIFY = 'Listen for events', - SUPERPOWERS = 'Enable superpowers', - BILLING = 'Add payment method', - DONE = 'Done!', -} - -export enum INGESTION_STEPS_WITHOUT_BILLING { - START = 'Get started', - PLATFORM = 'Select your platform', - CONNECT_PRODUCT = 'Connect your product', - VERIFY = 'Listen for events', - SUPERPOWERS = 'Enable superpowers', - DONE = 'Done!', -} - -export enum INGESTION_VIEWS { - BILLING = 'billing', - SUPERPOWERS = 'superpowers', - INVITE_TEAM = 'invite-team', - TEAM_INVITED = 'post-invite-team', - CHOOSE_PLATFORM = 'choose-platform', - VERIFICATION = 'verification', - WEB_INSTRUCTIONS = 'web-instructions', - CHOOSE_FRAMEWORK = 'choose-framework', - GENERATING_DEMO_DATA = 'generating-demo-data', - CHOOSE_THIRD_PARTY = 'choose-third-party', - NO_DEMO_INGESTION = 'no-demo-ingestion', -} - -export const INGESTION_VIEW_TO_STEP = { - [INGESTION_VIEWS.BILLING]: INGESTION_STEPS.BILLING, - [INGESTION_VIEWS.SUPERPOWERS]: INGESTION_STEPS.SUPERPOWERS, - [INGESTION_VIEWS.INVITE_TEAM]: INGESTION_STEPS.START, - [INGESTION_VIEWS.TEAM_INVITED]: INGESTION_STEPS.START, - [INGESTION_VIEWS.NO_DEMO_INGESTION]: INGESTION_STEPS.START, - [INGESTION_VIEWS.CHOOSE_PLATFORM]: INGESTION_STEPS.PLATFORM, - [INGESTION_VIEWS.VERIFICATION]: INGESTION_STEPS.VERIFY, - [INGESTION_VIEWS.WEB_INSTRUCTIONS]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.CHOOSE_FRAMEWORK]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.GENERATING_DEMO_DATA]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.CHOOSE_THIRD_PARTY]: INGESTION_STEPS.CONNECT_PRODUCT, -} - -export type IngestionState = { - platform: PlatformType - framework: Framework - readyToVerify: boolean - showSuperpowers: boolean - showBilling: boolean - hasInvitedMembers: boolean | null - isTechnicalUser: boolean | null - isDemoProject: boolean | null - generatingDemoData: boolean | null -} - -const viewToState = (view: string, props: IngestionState): IngestionState => { - switch (view) { - case INGESTION_VIEWS.INVITE_TEAM: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.TEAM_INVITED: - return { - isTechnicalUser: false, - hasInvitedMembers: true, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.BILLING: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: false, - showSuperpowers: false, - showBilling: true, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.VERIFICATION: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: true, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.SUPERPOWERS: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: false, - showSuperpowers: true, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.CHOOSE_PLATFORM: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - - case INGESTION_VIEWS.CHOOSE_FRAMEWORK: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: props.platform, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - } - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } -} - -export const ingestionLogic = kea([ - path(['scenes', 'ingestion', 'ingestionLogic']), - connect({ - values: [ - featureFlagLogic, - ['featureFlags'], - teamLogic, - ['currentTeam'], - preflightLogic, - ['preflight'], - inviteLogic, - ['isInviteModalShown'], - ], - actions: [ - teamLogic, - ['updateCurrentTeamSuccess', 'createTeamSuccess'], - inviteLogic, - ['inviteTeamMembersSuccess'], - ], - }), - actions({ - setState: ({ - isTechnicalUser, - hasInvitedMembers, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isDemoProject, - generatingDemoData, - }: IngestionState) => ({ - isTechnicalUser, - hasInvitedMembers, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isDemoProject, - generatingDemoData, - }), - setInstructionsModal: (isOpen: boolean) => ({ isOpen }), - setThirdPartySource: (sourceIndex: number) => ({ sourceIndex }), - completeOnboarding: true, - setCurrentStep: (currentStep: string) => ({ currentStep }), - sidebarStepClick: (step: string) => ({ step }), - next: (props: Partial) => props, - onBack: true, - goToView: (view: INGESTION_VIEWS) => ({ view }), - setSidebarSteps: (steps: string[]) => ({ steps }), - setPollTimeout: (pollTimeout: number) => ({ pollTimeout }), - toggleProjectSwitcher: true, - hideProjectSwitcher: true, - }), - windowValues({ - isSmallScreen: (window: Window) => window.innerWidth < getBreakpoint('md'), - }), - reducers({ - isTechnicalUser: [ - null as null | boolean, - { - setState: (_, { isTechnicalUser }) => isTechnicalUser, - }, - ], - hasInvitedMembers: [ - null as null | boolean, - { - setState: (_, { hasInvitedMembers }) => hasInvitedMembers, - }, - ], - platform: [ - null as null | PlatformType, - { - setState: (_, { platform }) => platform, - }, - ], - framework: [ - null as null | Framework, - { - setState: (_, { framework }) => (framework ? (framework.toUpperCase() as Framework) : null), - }, - ], - readyToVerify: [ - false, - { - setState: (_, { readyToVerify }) => readyToVerify, - }, - ], - showSuperpowers: [ - false, - { - setState: (_, { showSuperpowers }) => showSuperpowers, - }, - ], - showBilling: [ - false, - { - setState: (_, { showBilling }) => showBilling, - }, - ], - instructionsModalOpen: [ - false as boolean, - { - setInstructionsModal: (_, { isOpen }) => isOpen, - }, - ], - thirdPartyIntegrationSource: [ - null as ThirdPartySource | null, - { - setThirdPartySource: (_, { sourceIndex }) => thirdPartySources[sourceIndex], - }, - ], - sidebarSteps: [ - Object.values(INGESTION_STEPS_WITHOUT_BILLING) as string[], - { - setSidebarSteps: (_, { steps }) => steps, - }, - ], - isDemoProject: [ - teamLogic.values.currentTeam?.is_demo as null | boolean, - { - setState: (_, { isDemoProject }) => isDemoProject, - }, - ], - generatingDemoData: [ - false as boolean | null, - { - setState: (_, { generatingDemoData }) => generatingDemoData, - }, - ], - pollTimeout: [ - 0, - { - setPollTimeout: (_, payload) => payload.pollTimeout, - }, - ], - isProjectSwitcherShown: [ - false, - { - toggleProjectSwitcher: (state) => !state, - hideProjectSwitcher: () => false, - }, - ], - }), - loaders(({ actions, values }) => ({ - isDemoDataReady: [ - false as boolean, - { - checkIfDemoDataIsReady: async (_, breakpoint) => { - await breakpoint(1) - - clearTimeout(values.pollTimeout) - - try { - const res = await api.get('api/projects/@current/is_generating_demo_data') - if (!res.is_generating_demo_data) { - return true - } - const pollTimeoutMilliseconds = 1000 - const timeout = window.setTimeout(actions.checkIfDemoDataIsReady, pollTimeoutMilliseconds) - actions.setPollTimeout(timeout) - return false - } catch (e) { - return false - } - }, - }, - ], - })), - selectors(() => ({ - currentState: [ - (s) => [ - s.platform, - s.framework, - s.readyToVerify, - s.showSuperpowers, - s.showBilling, - s.isTechnicalUser, - s.hasInvitedMembers, - s.isDemoProject, - s.generatingDemoData, - ], - ( - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isTechnicalUser, - hasInvitedMembers, - isDemoProject, - generatingDemoData - ) => ({ - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isTechnicalUser, - hasInvitedMembers, - isDemoProject, - generatingDemoData, - }), - ], - currentView: [ - (s) => [s.currentState], - ({ - isTechnicalUser, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - hasInvitedMembers, - isDemoProject, - generatingDemoData, - }) => { - if (isDemoProject) { - return INGESTION_VIEWS.NO_DEMO_INGESTION - } - if (showBilling) { - return INGESTION_VIEWS.BILLING - } - if (showSuperpowers) { - return INGESTION_VIEWS.SUPERPOWERS - } - if (readyToVerify) { - return INGESTION_VIEWS.VERIFICATION - } - if (isTechnicalUser) { - if (!platform) { - return INGESTION_VIEWS.CHOOSE_PLATFORM - } - if (framework || platform === WEB) { - return INGESTION_VIEWS.WEB_INSTRUCTIONS - } - if (platform === MOBILE || platform === BACKEND) { - return INGESTION_VIEWS.CHOOSE_FRAMEWORK - } - if (platform === THIRD_PARTY) { - return INGESTION_VIEWS.CHOOSE_THIRD_PARTY - } - // could be null, so we check that it's set to false - } else if (isTechnicalUser === false) { - if (generatingDemoData) { - return INGESTION_VIEWS.GENERATING_DEMO_DATA - } - if (hasInvitedMembers) { - return INGESTION_VIEWS.TEAM_INVITED - } - if (!platform && !readyToVerify) { - return INGESTION_VIEWS.INVITE_TEAM - } - } - return INGESTION_VIEWS.INVITE_TEAM - }, - ], - currentStep: [ - (s) => [s.currentView], - (currentView) => { - return INGESTION_VIEW_TO_STEP[currentView] - }, - ], - previousStep: [ - (s) => [s.currentStep], - (currentStep) => { - const currentStepIndex = Object.values(INGESTION_STEPS).indexOf(currentStep) - return Object.values(INGESTION_STEPS)[currentStepIndex - 1] - }, - ], - frameworkString: [ - (s) => [s.framework], - (framework): string => { - if (framework) { - const frameworkStrings = { - NODEJS: 'Node.js', - GO: 'Go', - RUBY: 'Ruby', - PYTHON: 'Python', - PHP: 'PHP', - ELIXIR: 'Elixir', - ANDROID: 'Android', - IOS: 'iOS', - REACT_NATIVE: 'React Native', - FLUTTER: 'Flutter', - API: 'HTTP API', - } - return frameworkStrings[framework] || framework - } - return '' - }, - ], - showBillingStep: [ - (s) => [s.preflight], - (preflight): boolean => { - return !!preflight?.cloud && !preflight?.demo - }, - ], - })), - - actionToUrl(({ values }) => ({ - setState: () => getUrl(values), - updateCurrentTeamSuccess: (val) => { - if ( - (router.values.location.pathname.includes( - values.showBillingStep ? '/ingestion/billing' : '/ingestion/superpowers' - ) || - router.values.location.pathname.includes('/ingestion/invites-sent')) && - val.payload?.completed_snippet_onboarding - ) { - return combineUrl(urls.events(), { onboarding_completed: true }).url - } - }, - })), - - urlToAction(({ actions, values }) => ({ - '/ingestion': () => actions.goToView(INGESTION_VIEWS.INVITE_TEAM), - '/ingestion/invites-sent': () => actions.goToView(INGESTION_VIEWS.TEAM_INVITED), - '/ingestion/superpowers': () => actions.goToView(INGESTION_VIEWS.SUPERPOWERS), - '/ingestion/billing': () => actions.goToView(INGESTION_VIEWS.BILLING), - '/ingestion/verify': () => actions.goToView(INGESTION_VIEWS.VERIFICATION), - '/ingestion/platform': () => actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK), - '/ingestion(/:platform)(/:framework)': (pathParams, searchParams) => { - const platform = pathParams.platform || searchParams.platform || null - const framework = pathParams.framework || searchParams.framework || null - actions.setState({ - isTechnicalUser: true, - hasInvitedMembers: null, - platform: platform, - framework: framework, - readyToVerify: false, - showBilling: false, - showSuperpowers: false, - isDemoProject: values.isDemoProject, - generatingDemoData: false, - }) - }, - })), - listeners(({ actions, values }) => ({ - next: (props) => { - actions.setState({ ...values.currentState, ...props } as IngestionState) - }, - goToView: ({ view }) => { - actions.setState(viewToState(view, values.currentState as IngestionState)) - }, - completeOnboarding: () => { - teamLogic.actions.updateCurrentTeam({ - completed_snippet_onboarding: true, - }) - if ( - !values.currentTeam?.session_recording_opt_in || - !values.currentTeam?.capture_console_log_opt_in || - !values.currentTeam?.capture_performance_opt_in - ) { - eventUsageLogic.actions.reportIngestionRecordingsTurnedOff( - !!values.currentTeam?.session_recording_opt_in, - !!values.currentTeam?.capture_console_log_opt_in, - !!values.currentTeam?.capture_performance_opt_in - ) - } - if (values.currentTeam?.autocapture_opt_out) { - eventUsageLogic.actions.reportIngestionAutocaptureToggled(!!values.currentTeam?.autocapture_opt_out) - } - }, - setPlatform: ({ platform }) => { - eventUsageLogic.actions.reportIngestionSelectPlatformType(platform) - }, - setFramework: ({ framework }) => { - eventUsageLogic.actions.reportIngestionSelectFrameworkType(framework) - }, - sidebarStepClick: ({ step }) => { - switch (step) { - case INGESTION_STEPS.START: - actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - return - case INGESTION_STEPS.PLATFORM: - actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - return - case INGESTION_STEPS.CONNECT_PRODUCT: - actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK) - return - case INGESTION_STEPS.VERIFY: - actions.goToView(INGESTION_VIEWS.VERIFICATION) - return - case INGESTION_STEPS.BILLING: - actions.goToView(INGESTION_VIEWS.BILLING) - return - case INGESTION_STEPS.SUPERPOWERS: - actions.goToView(INGESTION_VIEWS.SUPERPOWERS) - return - default: - return - } - }, - onBack: () => { - switch (values.currentView) { - case INGESTION_VIEWS.BILLING: - return actions.goToView(INGESTION_VIEWS.VERIFICATION) - case INGESTION_VIEWS.SUPERPOWERS: - return actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK) - case INGESTION_VIEWS.TEAM_INVITED: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.CHOOSE_PLATFORM: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.VERIFICATION: - return actions.goToView(INGESTION_VIEWS.SUPERPOWERS) - case INGESTION_VIEWS.WEB_INSTRUCTIONS: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - case INGESTION_VIEWS.CHOOSE_FRAMEWORK: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - // If they're on the InviteTeam step, but on the Team Invited panel, - // we still want them to be able to go back to the previous step. - // So this resets the state for that panel so they can go back. - case INGESTION_VIEWS.INVITE_TEAM: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.CHOOSE_THIRD_PARTY: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - default: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - } - }, - inviteTeamMembersSuccess: () => { - if (router.values.location.pathname.includes(urls.ingestion())) { - actions.setState(viewToState(INGESTION_VIEWS.TEAM_INVITED, values.currentState as IngestionState)) - } - }, - createTeamSuccess: ({ currentTeam }) => { - if (window.location.href.includes(urls.ingestion()) && currentTeam.is_demo) { - actions.checkIfDemoDataIsReady(null) - } else { - window.location.href = urls.ingestion() - } - }, - checkIfDemoDataIsReadySuccess: ({ isDemoDataReady }) => { - if (isDemoDataReady) { - window.location.href = urls.default() - } - }, - })), - subscriptions(({ actions, values }) => ({ - showBillingStep: (value) => { - const steps = value ? INGESTION_STEPS : INGESTION_STEPS_WITHOUT_BILLING - actions.setSidebarSteps(Object.values(steps)) - }, - currentTeam: (currentTeam: TeamType) => { - if (currentTeam?.ingested_event && values.readyToVerify && !values.showBillingStep) { - actions.setCurrentStep(INGESTION_STEPS.DONE) - } - }, - })), -]) - -function getUrl(values: ingestionLogicType['values']): string | [string, Record] { - const { - isTechnicalUser, - platform, - framework, - readyToVerify, - showBilling, - showSuperpowers, - hasInvitedMembers, - generatingDemoData, - } = values - - let url = '/ingestion' - - if (showBilling) { - return url + '/billing' - } - - if (showSuperpowers) { - url += '/superpowers' - return [ - url, - { - platform: platform || undefined, - framework: framework?.toLowerCase() || undefined, - }, - ] - } - - if (readyToVerify) { - url += '/verify' - return [ - url, - { - platform: platform || undefined, - framework: framework?.toLowerCase() || undefined, - }, - ] - } - - if (isTechnicalUser) { - if (framework === API) { - url += '/api' - return [ - url, - { - platform: platform || undefined, - }, - ] - } - - if (platform === MOBILE) { - url += '/mobile' - } - - if (platform === WEB) { - url += '/web' - } - - if (platform === BACKEND) { - url += '/backend' - } - - if (generatingDemoData) { - url += '/just-exploring' - } - - if (platform === THIRD_PARTY) { - url += '/third-party' - } - - if (!platform) { - url += '/platform' - } - - if (framework) { - url += `/${framework.toLowerCase()}` - } - } else { - if (!platform && hasInvitedMembers) { - url += '/invites-sent' - } - } - - return url -} diff --git a/frontend/src/scenes/ingestion/panels/BillingPanel.tsx b/frontend/src/scenes/ingestion/panels/BillingPanel.tsx deleted file mode 100644 index f52269890ece0..0000000000000 --- a/frontend/src/scenes/ingestion/panels/BillingPanel.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useActions, useValues } from 'kea' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { LemonDivider } from '@posthog/lemon-ui' -import { billingLogic } from 'scenes/billing/billingLogic' -import { Billing } from 'scenes/billing/Billing' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' - -export function BillingPanel(): JSX.Element { - const { completeOnboarding } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutBilling } = useActions(eventUsageLogic) - const { billing } = useValues(billingLogic) - - if (!billing) { - return ( - -
    - - - -
    -
    - - -
    - - ) - } - - const hasSubscribedToAllProducts = billing.products - .filter((product) => !product.contact_support) - .every((product) => product.subscribed) - const hasSubscribedToAnyProduct = billing.products.some((product) => product.subscribed) - - return ( - - {hasSubscribedToAllProducts ? ( -
    -

    You're good to go!

    - -

    - Your organisation is setup for billing with premium features and the increased free tiers - enabled. -

    - { - completeOnboarding() - }} - > - Complete - -
    - ) : ( -
    -

    Subscribe for access to all features

    - - - - - { - completeOnboarding() - !hasSubscribedToAnyProduct && reportIngestionContinueWithoutBilling() - }} - > - {hasSubscribedToAnyProduct ? 'Continue' : 'Skip for now'} - -
    - )} -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx b/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx deleted file mode 100644 index 598012915577d..0000000000000 --- a/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useActions, useValues } from 'kea' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { ingestionLogic } from '../ingestionLogic' -import { API, mobileFrameworks, BACKEND, webFrameworks } from 'scenes/ingestion/constants' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' - -export function FrameworkPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - const { platform } = useValues(ingestionLogic) - const frameworks = platform === BACKEND ? webFrameworks : mobileFrameworks - - return ( - -
    -

    - {platform === BACKEND ? 'Choose the framework your app is built in' : 'Pick a mobile platform'} -

    -

    - We'll provide you with snippets that you can easily add to your codebase to get started! -

    -
    - {(Object.keys(frameworks) as (keyof typeof frameworks)[]).map((item) => ( - next({ framework: item })} - > - {frameworks[item]} - - ))} - next({ framework: API })} - > - Other - - -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx b/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx deleted file mode 100644 index a568dd9a82340..0000000000000 --- a/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useValues } from 'kea' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { organizationLogic } from 'scenes/organizationLogic' -import { CardContainer } from '../CardContainer' -import './Panels.scss' - -export function GeneratingDemoDataPanel(): JSX.Element { - const { currentOrganization } = useValues(organizationLogic) - return ( - -
    -
    -
    - -
    -

    Generating demo data...

    -

    - Your demo data is on the way! This can take up to one minute - we'll redirect you when your demo - data is ready. -

    - - We're using a demo project. Your other {currentOrganization?.name} projects won't be - affected. - -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss b/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss deleted file mode 100644 index 005ff0d4af187..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss +++ /dev/null @@ -1,26 +0,0 @@ -.InstructionsPanel { - max-width: 50rem; - h1 { - font-size: 28px; - font-weight: 800; - line-height: 40px; - letter-spacing: -0.02em; - } - h2 { - font-size: 20px; - font-weight: 800; - line-height: 24px; - letter-spacing: -0.02em; - margin-top: 0.5rem; - } - h3 { - font-size: 16px; - font-weight: 700; - line-height: 24px; - letter-spacing: 0em; - margin-top: 0.5rem; - } - ol { - padding-left: 1rem; - } -} diff --git a/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx b/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx deleted file mode 100644 index 390620eaf2493..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import './InstructionsPanel.scss' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { - AndroidInstructions, - APIInstructions, - ElixirInstructions, - FlutterInstructions, - GoInstructions, - IOSInstructions, - NodeInstructions, - PHPInstructions, - PythonInstructions, - RNInstructions, - RubyInstructions, -} from 'scenes/ingestion/frameworks' -import { API, MOBILE, BACKEND, WEB } from '../constants' -import { useValues } from 'kea' -import { ingestionLogic } from '../ingestionLogic' -import { WebInstructions } from '../frameworks/WebInstructions' -import { Link } from '@posthog/lemon-ui' - -const frameworksSnippet: Record = { - NODEJS: NodeInstructions, - GO: GoInstructions, - RUBY: RubyInstructions, - PYTHON: PythonInstructions, - PHP: PHPInstructions, - ELIXIR: ElixirInstructions, - ANDROID: AndroidInstructions, - IOS: IOSInstructions, - REACT_NATIVE: RNInstructions, - FLUTTER: FlutterInstructions, - API: APIInstructions, -} - -export function InstructionsPanel(): JSX.Element { - const { platform, framework, frameworkString } = useValues(ingestionLogic) - - if (platform !== WEB && !framework) { - return <> - } - - const FrameworkSnippet: React.ComponentType = frameworksSnippet[framework as string] as React.ComponentType - - return ( -
    - {platform === WEB ? ( - - - - ) : framework === API ? ( - -

    {frameworkString}

    -

    - Need a different framework? Our HTTP API is a flexible way to use PostHog anywhere. Try the - endpoint below to send your first event, and view our API docs{' '} - here. -

    - -
    - ) : ( - -

    {`Setup ${frameworkString}`}

    - - {platform === BACKEND ? ( - <> -

    - Follow the instructions below to send custom events from your {frameworkString} backend. -

    - - - ) : null} - {platform === MOBILE ? : null} -
    - )} -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx deleted file mode 100644 index c6a30c5cf484f..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useActions } from 'kea' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { IconChevronRight } from 'lib/lemon-ui/icons' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { DemoProjectButton } from './PanelComponents' - -export function InviteTeamPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - const { showInviteModal } = useActions(inviteLogic) - const { reportInviteMembersButtonClicked } = useActions(eventUsageLogic) - - return ( -
    -

    Welcome to PostHog

    -

    - PostHog enables you to understand your customers, answer product questions, and test new features{' '} - - all in our comprehensive product suite. To get started, we'll need to add a code snippet to your - product. -

    - -
    - next({ isTechnicalUser: true })} - fullWidth - size="large" - className="mb-4" - type="primary" - sideIcon={} - > -
    -

    I can add a code snippet to my product.

    -

    - Available for JavaScript, Android, iOS, React Native, Node.js, Ruby, Go, and more. -

    -
    -
    - { - showInviteModal() - reportInviteMembersButtonClicked() - }} - fullWidth - size="large" - className="mb-4" - type="secondary" - sideIcon={} - > -
    -

    I'll need a team member to add the code snippet to our product.

    -

    - We'll send an invite and instructions for getting the code snippet added. -

    -
    -
    - -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx b/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx deleted file mode 100644 index 722ba20e9e603..0000000000000 --- a/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { LemonButton } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { IconArrowRight } from 'lib/lemon-ui/icons' -import { organizationLogic } from 'scenes/organizationLogic' -import { userLogic } from 'scenes/userLogic' -import { CardContainer } from '../CardContainer' -import './Panels.scss' - -export function NoDemoIngestionPanel(): JSX.Element { - const { currentOrganization } = useValues(organizationLogic) - const { updateCurrentTeam } = useActions(userLogic) - - return ( - -
    -

    Whoops!

    -

    - New events can't be ingested into a demo project. But, you can switch to another project if you'd - like: -

    -
    - {currentOrganization?.teams - ?.filter((team) => !team.is_demo) - .map((team) => ( -

    - } - fullWidth - onClick={() => updateCurrentTeam(team.id)} - > - {team.name} - -

    - ))} -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/PanelComponents.tsx b/frontend/src/scenes/ingestion/panels/PanelComponents.tsx deleted file mode 100644 index 4d817149a280b..0000000000000 --- a/frontend/src/scenes/ingestion/panels/PanelComponents.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { ingestionLogic, INGESTION_STEPS, IngestionState } from '../ingestionLogic' -import './Panels.scss' -import { IconArrowLeft, IconChevronRight } from 'lib/lemon-ui/icons' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' -import { teamLogic } from 'scenes/teamLogic' -import { organizationLogic } from 'scenes/organizationLogic' -import { userLogic } from 'scenes/userLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' - -const DEMO_TEAM_NAME: string = 'Hedgebox' - -export function PanelFooter({ - nextProps, - onContinue, - finalStep = false, - showInviteTeamMembers = true, -}: { - nextProps: Partial - onContinue?: () => void - finalStep?: boolean - showInviteTeamMembers?: boolean -}): JSX.Element { - const { next } = useActions(ingestionLogic) - - return ( -
    - -
    - { - onContinue && onContinue() - next(nextProps) - }} - > - {finalStep ? 'Complete' : 'Continue'} - - {showInviteTeamMembers && } -
    -
    - ) -} - -export function PanelHeader(): JSX.Element | null { - const { isSmallScreen, previousStep, currentStep, hasInvitedMembers } = useValues(ingestionLogic) - const { onBack } = useActions(ingestionLogic) - - // no back buttons on the Getting Started step - // but only if it's not the MembersInvited panel - // (since they'd want to be able to go back from there) - if (currentStep === INGESTION_STEPS.START && !hasInvitedMembers) { - return null - } - - return ( -
    - } size="small"> - {isSmallScreen - ? '' - : // If we're on the MembersInvited panel, they "go back" to - // the Get Started step, even though it's technically the same step - currentStep === INGESTION_STEPS.START && hasInvitedMembers - ? currentStep - : previousStep} - -
    - ) -} - -export function DemoProjectButton({ text, subtext }: { text: string; subtext?: string }): JSX.Element { - const { next } = useActions(ingestionLogic) - const { createTeam } = useActions(teamLogic) - const { currentOrganization } = useValues(organizationLogic) - const { updateCurrentTeam } = useActions(userLogic) - const { reportIngestionTryWithDemoDataClicked, reportProjectCreationSubmitted } = useActions(eventUsageLogic) - const { featureFlags } = useValues(featureFlagLogic) - - if (featureFlags[FEATURE_FLAGS.ONBOARDING_V2_DEMO] !== 'test') { - return <> - } - return ( - { - // If the current org has a demo team, just navigate there - if (currentOrganization?.teams && currentOrganization.teams.filter((team) => team.is_demo).length > 0) { - updateCurrentTeam(currentOrganization.teams.filter((team) => team.is_demo)[0].id) - } else { - // Create a new demo team - createTeam({ name: DEMO_TEAM_NAME, is_demo: true }) - next({ isTechnicalUser: false, generatingDemoData: true }) - reportProjectCreationSubmitted( - currentOrganization?.teams ? currentOrganization.teams.length : 0, - DEMO_TEAM_NAME.length - ) - } - reportIngestionTryWithDemoDataClicked() - }} - fullWidth - size="large" - className="ingestion-view-demo-data mb-4" - type="secondary" - sideIcon={} - > -
    -

    - {currentOrganization?.teams && currentOrganization.teams.filter((team) => team.is_demo).length > 0 - ? 'Explore the demo project' - : text} -

    - {subtext ?

    {subtext}

    : null} -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/Panels.scss b/frontend/src/scenes/ingestion/panels/Panels.scss deleted file mode 100644 index ca98aa806c405..0000000000000 --- a/frontend/src/scenes/ingestion/panels/Panels.scss +++ /dev/null @@ -1,37 +0,0 @@ -.FrameworkPanel { - max-width: 400px; -} - -.panel-footer { - background-color: white; - margin-bottom: 1rem; - bottom: 0; -} - -.ingestion-title { - font-size: 28px; - font-weight: 700; - line-height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0; -} - -.IngestionSubtitle { - font-size: 20px; - font-weight: 800; - margin: 1rem 0; -} - -.prompt-text { - margin-top: 1rem; -} - -.ingestion-listening-for-events { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - margin-bottom: 1rem; -} diff --git a/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx b/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx deleted file mode 100644 index 5ee33d73597c5..0000000000000 --- a/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useActions } from 'kea' -import { ingestionLogic } from '../ingestionLogic' -import { THIRD_PARTY, platforms } from '../constants' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' - -export function PlatformPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - - return ( -
    -

    Where do you want to send events from?

    -

    - With PostHog, you can collect events from nearly anywhere. Select one to start, and you can always add - more sources later. -

    - -
    - {platforms.map((platform) => ( - next({ platform })} - > - {platform} - - ))} - next({ platform: THIRD_PARTY })} - fullWidth - center - size="large" - className="mb-2" - type="primary" - > - Import events from a third party - - -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx b/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx deleted file mode 100644 index 5ec5cad19e87e..0000000000000 --- a/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { LemonSwitch, Link } from '@posthog/lemon-ui' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { useActions, useValues } from 'kea' -import { SupportHeroHog } from 'lib/components/hedgehogs' -import { useState } from 'react' -import { teamLogic } from 'scenes/teamLogic' -import { ingestionLogic } from '../ingestionLogic' - -export function SuperpowersPanel(): JSX.Element { - const { updateCurrentTeam } = useActions(teamLogic) - const { showBillingStep } = useValues(ingestionLogic) - const { completeOnboarding } = useActions(ingestionLogic) - const [sessionRecordingsChecked, setSessionRecordingsChecked] = useState(true) - const [autocaptureChecked, setAutocaptureChecked] = useState(true) - - return ( - { - updateCurrentTeam({ - session_recording_opt_in: sessionRecordingsChecked, - capture_console_log_opt_in: sessionRecordingsChecked, - capture_performance_opt_in: sessionRecordingsChecked, - autocapture_opt_out: !autocaptureChecked, - }) - if (!showBillingStep) { - completeOnboarding() - } - }} - finalStep={!showBillingStep} - > -
    -
    -

    Enable your product superpowers

    -

    - Collecting events from your app is just the first step toward building great products. PostHog - gives you other superpowers, too, like recording user sessions and automagically capturing - frontend interactions. -

    -
    -
    - -
    -
    -
    - { - setSessionRecordingsChecked(checked) - }} - label="Record user sessions" - fullWidth={true} - labelClassName={'text-base font-semibold'} - checked={sessionRecordingsChecked} - /> -

    - See recordings of how your users are really using your product with powerful features like error - tracking, filtering, and analytics.{' '} - - Learn more - {' '} - about Session recordings. -

    -
    -
    - { - setAutocaptureChecked(checked) - }} - label="Autocapture frontend interactions" - fullWidth={true} - labelClassName={'text-base font-semibold'} - checked={autocaptureChecked} - /> -

    - If you use our JavaScript or React Native libraries, we'll automagically capture frontend - interactions like pageviews, clicks, and more.{' '} - - Fine-tune what you capture - {' '} - directly in your code snippet. -

    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx b/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx deleted file mode 100644 index 5d6365d22c335..0000000000000 --- a/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useActions } from 'kea' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { IconChevronRight } from 'lib/lemon-ui/icons' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { DemoProjectButton } from './PanelComponents' - -export function TeamInvitedPanel(): JSX.Element { - const { completeOnboarding } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) - - return ( -
    -

    Help is on the way!

    -

    You can still explore PostHog while you wait for your team members to join.

    - -
    - - { - completeOnboarding() - reportIngestionContinueWithoutVerifying() - }} - fullWidth - size="large" - className="mb-4" - type="secondary" - sideIcon={} - > -
    -

    Continue without any events.

    -

    - It might look a little empty in there, but we'll do our best. -

    -
    -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx b/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx deleted file mode 100644 index 1ebb0e6545346..0000000000000 --- a/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx +++ /dev/null @@ -1,58 +0,0 @@ -export const Segment = (props: React.SVGProps): JSX.Element => { - return ( - - - - - - - - - - - - - ) -} - -export const RSS = (props: React.SVGProps): JSX.Element => { - return ( - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx b/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx deleted file mode 100644 index 8fd51654aafb9..0000000000000 --- a/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useValues, useActions } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { CardContainer } from '../CardContainer' -import { ingestionLogic } from '../ingestionLogic' -import './Panels.scss' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { thirdPartySources } from '../constants' -import { IconOpenInNew } from 'lib/lemon-ui/icons' -import { CodeSnippet } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { Link } from '@posthog/lemon-ui' - -export function ThirdPartyPanel(): JSX.Element { - const { setInstructionsModal, setThirdPartySource } = useActions(ingestionLogic) - const { reportIngestionThirdPartyAboutClicked, reportIngestionThirdPartyConfigureClicked } = - useActions(eventUsageLogic) - - return ( - -
    -

    Set up third-party integrations

    - {thirdPartySources.map((source, idx) => { - return ( -
    -
    -
    -
    {source.icon}
    -
    -

    - {source.name} Import - {source.labels?.map((label, labelIdx) => ( - - {label} - - ))} -

    -

    - {source.description - ? source.description - : `Send events from ${source.name} into PostHog`} -

    -
    -
    -
    - { - reportIngestionThirdPartyAboutClicked(source.name) - }} - > - About - - { - setThirdPartySource(idx) - setInstructionsModal(true) - reportIngestionThirdPartyConfigureClicked(source.name) - }} - > - Configure - -
    -
    -
    - ) - })} -
    - -
    - ) -} - -export function IntegrationInstructionsModal(): JSX.Element { - const { instructionsModalOpen, thirdPartyIntegrationSource } = useValues(ingestionLogic) - const { setInstructionsModal } = useActions(ingestionLogic) - const { currentTeam } = useValues(teamLogic) - - return ( - <> - {thirdPartyIntegrationSource?.name && ( - setInstructionsModal(false)} - title="Configure integration" - footer={ - setInstructionsModal(false)}> - Done - - } - > -
    -

    - {thirdPartyIntegrationSource.icon} - Integrate with {thirdPartyIntegrationSource.name} -

    -
    -
    -

    - The{' '} - - {thirdPartyIntegrationSource.name} docs page for the PostHog integration - {' '} - provides a detailed overview of how to set up this integration. -

    - PostHog Project API Key - {currentTeam?.api_token || ''} -
    -
    - window.open(thirdPartyIntegrationSource.aboutLink)} - sideIcon={} - > - Take me to the {thirdPartyIntegrationSource.name} docs - -
    -

    Steps:

    -
      -
    1. Complete the steps for the {thirdPartyIntegrationSource.name} integration.
    2. -
    3. - Close this step and click continue to begin listening for events. -
    4. -
    -
    -
    -
    - )} - - ) -} diff --git a/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx b/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx deleted file mode 100644 index a6138f22fa438..0000000000000 --- a/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useActions, useValues } from 'kea' -import { useInterval } from 'lib/hooks/useInterval' -import { CardContainer } from '../CardContainer' -import { ingestionLogic } from '../ingestionLogic' -import { teamLogic } from 'scenes/teamLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './Panels.scss' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' - -export function VerificationPanel(): JSX.Element { - const { loadCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - const { next } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) - - useInterval(() => { - if (!currentTeam?.ingested_event) { - loadCurrentTeam() - } - }, 2000) - - return !currentTeam?.ingested_event ? ( - -
    -
    - -

    Listening for events...

    -

    - Once you have integrated the snippet and sent an event, we will verify it was properly received - and continue. -

    - - { - next({ showSuperpowers: true }) - reportIngestionContinueWithoutVerifying() - }} - > - or continue without verifying - -
    -
    -
    - ) : ( - -
    -
    -

    Successfully sent events!

    -

    - You will now be able to explore PostHog and take advantage of all its features to understand - your users. -

    -
    -
    -
    - ) -} diff --git a/frontend/src/scenes/ingestion/types.ts b/frontend/src/scenes/ingestion/types.ts deleted file mode 100644 index 7d64652d8cdb5..0000000000000 --- a/frontend/src/scenes/ingestion/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { API, MOBILE, mobileFrameworks, BACKEND, WEB, webFrameworks, THIRD_PARTY } from 'scenes/ingestion/constants' - -export type Framework = keyof typeof webFrameworks | keyof typeof mobileFrameworks | typeof API | null - -export type PlatformType = typeof WEB | typeof MOBILE | typeof BACKEND | typeof THIRD_PARTY | null diff --git a/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx b/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx index 3aff4375e8c36..013ca006ca8d4 100644 --- a/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx @@ -1,8 +1,10 @@ -import { useActions, useValues } from 'kea' -import { BreakdownAttributionType, EditorFilterProps, StepOrderValue } from '~/types' import { LemonSelect } from '@posthog/lemon-ui' -import { FunnelsFilter } from '~/queries/schema' +import { useActions, useValues } from 'kea' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' + +import { FunnelsFilter } from '~/queries/schema' +import { BreakdownAttributionType, EditorFilterProps, StepOrderValue } from '~/types' + import { FUNNEL_STEP_COUNT_LIMIT } from './FunnelsQuerySteps' export function Attribution({ insightProps }: EditorFilterProps): JSX.Element { diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx index 8443b7e432f9d..b7ad15cc28546 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx @@ -1,12 +1,14 @@ -import { useValues, useActions } from 'kea' +import { useActions, useValues } from 'kea' +import { PureField } from 'lib/forms/Field' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' + +import { Noun } from '~/models/groupsModel' import { EditorFilterProps } from '~/types' -import { FunnelStepOrderPicker } from '../views/Funnels/FunnelStepOrderPicker' + import { FunnelExclusionsFilter } from '../filters/FunnelExclusionsFilter/FunnelExclusionsFilter' import { FunnelStepReferencePicker } from '../filters/FunnelStepReferencePicker' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { PureField } from 'lib/forms/Field' -import { Noun } from '~/models/groupsModel' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { FunnelStepOrderPicker } from '../views/Funnels/FunnelStepOrderPicker' export function FunnelsAdvanced({ insightProps }: EditorFilterProps): JSX.Element { const { querySource, aggregationTargetLabel, advancedOptionsUsedCount } = useValues(funnelDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx index 8b0cf093c12c3..265dbfa4267c7 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx @@ -1,20 +1,21 @@ -import { useValues, useActions } from 'kea' -import { groupsModel } from '~/models/groupsModel' - -import { FilterType, EditorFilterProps } from '~/types' -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { useActions, useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { FunnelVizType } from '../views/Funnels/FunnelVizType' -import { ActionFilter } from '../filters/ActionFilter/ActionFilter' -import { AggregationSelect } from '../filters/AggregationSelect' -import { FunnelConversionWindowFilter } from '../views/Funnels/FunnelConversionWindowFilter' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { groupsModel } from '~/models/groupsModel' import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { FunnelsQuery } from '~/queries/schema' import { isInsightQueryNode } from '~/queries/utils' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { EditorFilterProps, FilterType } from '~/types' + +import { ActionFilter } from '../filters/ActionFilter/ActionFilter' +import { AggregationSelect } from '../filters/AggregationSelect' +import { FunnelConversionWindowFilter } from '../views/Funnels/FunnelConversionWindowFilter' +import { FunnelVizType } from '../views/Funnels/FunnelVizType' export const FUNNEL_STEP_COUNT_LIMIT = 20 diff --git a/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx b/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx index 150222f6a0013..624045ea8c957 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx @@ -1,18 +1,17 @@ -import { useState } from 'react' -import { useActions, useValues } from 'kea' -import { InputNumber } from 'antd' - -import { AvailableFeature, PathEdgeParameters, EditorFilterProps } from '~/types' import { LemonDivider } from '@posthog/lemon-ui' +import { InputNumber } from 'antd' +import { useActions, useValues } from 'kea' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { IconSettings } from 'lib/lemon-ui/icons' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { Link } from 'lib/lemon-ui/Link' +import { useState } from 'react' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' +import { urls } from 'scenes/urls' -import { Link } from 'lib/lemon-ui/Link' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { IconSettings } from 'lib/lemon-ui/icons' +import { AvailableFeature, EditorFilterProps, PathEdgeParameters } from '~/types' import { PathCleaningFilter } from '../filters/PathCleaningFilter' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { urls } from 'scenes/urls' export function PathsAdvanced({ insightProps, ...rest }: EditorFilterProps): JSX.Element { const { pathsFilter } = useValues(pathsDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/PathsEventTypes.tsx b/frontend/src/scenes/insights/EditorFilters/PathsEventTypes.tsx index abd05ea75767d..62a3e57f61732 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsEventTypes.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsEventTypes.tsx @@ -1,11 +1,13 @@ -import { useValues, useActions } from 'kea' -import { PathType, EditorFilterProps, PathsFilterType } from '~/types' -import { LemonButtonWithDropdown, LemonButton } from 'lib/lemon-ui/LemonButton' -import { humanizePathsEventTypes } from '../utils' +import { useActions, useValues } from 'kea' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { capitalizeFirstLetter } from 'lib/utils' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' +import { EditorFilterProps, PathsFilterType, PathType } from '~/types' + +import { humanizePathsEventTypes } from '../utils' + export function PathsEventsTypes({ insightProps }: EditorFilterProps): JSX.Element { const { pathsFilter } = useValues(pathsDataLogic(insightProps)) const { updateInsightFilter } = useActions(pathsDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/PathsExclusions.tsx b/frontend/src/scenes/insights/EditorFilters/PathsExclusions.tsx index 7a38d4d004990..9d5cf33daff62 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsExclusions.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsExclusions.tsx @@ -1,9 +1,9 @@ import { useActions, useValues } from 'kea' - +import { PathItemFilters } from 'lib/components/PropertyFilters/PathItemFilters' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' -import { EventPropertyFilter, PropertyFilterType, PropertyOperator, EditorFilterProps } from '~/types' -import { PathItemFilters } from 'lib/components/PropertyFilters/PathItemFilters' +import { EditorFilterProps, EventPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types' export function PathsExclusions({ insightProps }: EditorFilterProps): JSX.Element { const { pathsFilter, taxonomicGroupTypes } = useValues(pathsDataLogic(insightProps)) @@ -13,7 +13,7 @@ export function PathsExclusions({ insightProps }: EditorFilterProps): JSX.Elemen return ( diff --git a/frontend/src/scenes/insights/EditorFilters/PathsWildcardGroups.tsx b/frontend/src/scenes/insights/EditorFilters/PathsWildcardGroups.tsx index 9682682262011..125abec183a42 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsWildcardGroups.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsWildcardGroups.tsx @@ -1,9 +1,8 @@ -import { useValues, useActions } from 'kea' - +import { useActions, useValues } from 'kea' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' import { EditorFilterProps } from '~/types' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' export function PathsWildcardGroups({ insightProps }: EditorFilterProps): JSX.Element { const { pathsFilter } = useValues(pathsDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/PercentStackViewFilter.tsx b/frontend/src/scenes/insights/EditorFilters/PercentStackViewFilter.tsx index ac8a5a6c841da..9e31687ef2971 100644 --- a/frontend/src/scenes/insights/EditorFilters/PercentStackViewFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PercentStackViewFilter.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { insightLogic } from '../insightLogic' import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' +import { insightLogic } from '../insightLogic' + export function PercentStackViewFilter(): JSX.Element { const { insightProps } = useValues(insightLogic) const { showPercentStackView } = useValues(trendsDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx b/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx index e1debffe25d8e..8859b225ef419 100644 --- a/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx +++ b/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx @@ -1,21 +1,23 @@ +import { IconInfo } from '@posthog/icons' +import { LemonInput, LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { AggregationSelect } from 'scenes/insights/filters/AggregationSelect' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { dateOptionPlurals, dateOptions, retentionOptionDescriptions, retentionOptions, } from 'scenes/retention/constants' -import { FilterType, EditorFilterProps, RetentionType } from '~/types' -import { ActionFilter } from '../filters/ActionFilter/ActionFilter' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { AggregationSelect } from 'scenes/insights/filters/AggregationSelect' + import { groupsModel } from '~/models/groupsModel' +import { EditorFilterProps, FilterType, RetentionType } from '~/types' + +import { ActionFilter } from '../filters/ActionFilter/ActionFilter' import { MathAvailability } from '../filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { Link } from 'lib/lemon-ui/Link' -import { LemonInput, LemonSelect } from '@posthog/lemon-ui' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { IconInfo } from '@posthog/icons' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Element { const { showGroupsOptions } = useValues(groupsModel) @@ -53,7 +55,7 @@ export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Eleme updateInsightFilter({ target_entity: undefined }) } }} - typeKey={`${keyForInsightLogicProps('new')(insightProps)}-RetentionSummary`} + typeKey={`${keyForInsightLogicProps('new')(insightProps)}-target_entity`} /> on any of the next {dateOptionPlurals[period ?? 'Day']}. diff --git a/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx b/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx index 45bdd4b08491c..9a93aad12783a 100644 --- a/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx @@ -1,9 +1,11 @@ -import { InsightLogicProps } from '~/types' import { LemonButton, LemonLabel, LemonSwitch, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { AVAILABLE_SAMPLING_PERCENTAGES, samplingFilterLogic } from './samplingFilterLogic' import posthog from 'posthog-js' +import { InsightLogicProps } from '~/types' + +import { AVAILABLE_SAMPLING_PERCENTAGES, samplingFilterLogic } from './samplingFilterLogic' + const DEFAULT_SAMPLING_INFO_TOOLTIP_CONTENT = 'Sampling computes the result on only a subset of the data, making insights load significantly faster.' diff --git a/frontend/src/scenes/insights/EditorFilters/ShowLegendFilter.tsx b/frontend/src/scenes/insights/EditorFilters/ShowLegendFilter.tsx index 0dc31ee402fd0..b6ae3a2a978fe 100644 --- a/frontend/src/scenes/insights/EditorFilters/ShowLegendFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/ShowLegendFilter.tsx @@ -1,9 +1,8 @@ -import { useValues, useActions } from 'kea' - +import { LemonCheckbox } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' -import { insightVizDataLogic } from '../insightVizDataLogic' -import { LemonCheckbox } from '@posthog/lemon-ui' +import { insightVizDataLogic } from '../insightVizDataLogic' export function ShowLegendFilter(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx b/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx index 37200a5dc3baf..142a2f1d89b4b 100644 --- a/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { insightLogic } from '../insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { insightLogic } from '../insightLogic' + export function ValueOnSeriesFilter(): JSX.Element { const { insightProps } = useValues(insightLogic) const { valueOnSeries } = useValues(insightVizDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/EditorFilters/samplingFilterLogic.ts b/frontend/src/scenes/insights/EditorFilters/samplingFilterLogic.ts index 2ffa3e6be974e..7c9ba8fbc3125 100644 --- a/frontend/src/scenes/insights/EditorFilters/samplingFilterLogic.ts +++ b/frontend/src/scenes/insights/EditorFilters/samplingFilterLogic.ts @@ -1,12 +1,11 @@ -import { kea, path, connect, actions, reducers, props, selectors, listeners, key } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' - -import { insightVizDataLogic } from '../insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { InsightLogicProps } from '~/types' +import { insightVizDataLogic } from '../insightVizDataLogic' import type { samplingFilterLogicType } from './samplingFilterLogicType' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' export const AVAILABLE_SAMPLING_PERCENTAGES = [0.1, 1, 10, 25] diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss b/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss index a7ec2f0af4c9d..abb96d022d645 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss @@ -3,8 +3,6 @@ flex-direction: column; justify-content: center; align-items: center; - padding-top: 2rem; - padding-bottom: 2rem; color: var(--muted); padding: 1rem; font-size: 1.1em; @@ -58,8 +56,10 @@ .ant-empty { height: 6rem; margin: 0; + .ant-empty-image { height: 100%; + svg { width: 4rem; } diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx index ab3e7206c558f..d8eceb1eb429a 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx @@ -1,21 +1,23 @@ -import { useEffect } from 'react' import { Meta, StoryObj } from '@storybook/react' -import funnelOneStep from './funnelOneStep.json' -import { useStorybookMocks } from '~/mocks/browser' import { router } from 'kea-router' -import insight from '../../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json' -import { InsightShortId } from '~/types' -import { createInsightStory } from 'scenes/insights/__mocks__/createInsightScene' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { createInsightStory } from 'scenes/insights/__mocks__/createInsightScene' + +import { useStorybookMocks } from '~/mocks/browser' +import { InsightShortId } from '~/types' + +import insight from '../../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json' import { insightVizDataLogic } from '../insightVizDataLogic' +import funnelOneStep from './funnelOneStep.json' type Story = StoryObj const meta: Meta = { title: 'Scenes-App/Insights/Error states', + tags: ['test-skip'], parameters: { layout: 'fullscreen', viewMode: 'story', - testOptions: { skip: true }, // FIXME }, } export default meta diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx index 747a093413c06..e99592c5176ac 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx @@ -1,28 +1,31 @@ -import { useActions, useValues } from 'kea' +import './EmptyStates.scss' + // eslint-disable-next-line no-restricted-imports import { PlusCircleOutlined, ThunderboltFilled } from '@ant-design/icons' +import { IconWarning } from '@posthog/icons' +import { LemonButton } from '@posthog/lemon-ui' +import { Empty } from 'antd' +import { useActions, useValues } from 'kea' +import { BuilderHog3 } from 'lib/components/hedgehogs' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { SupportModal } from 'lib/components/Support/SupportModal' import { IconErrorOutline, IconInfo, IconOpenInNew, IconPlus } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { posthog } from 'posthog-js' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' -import { Button, Empty } from 'antd' -import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { FilterType, InsightLogicProps, SavedInsightsTabs } from '~/types' import { insightLogic } from 'scenes/insights/insightLogic' -import './EmptyStates.scss' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' import { urls } from 'scenes/urls' -import { Link } from 'lib/lemon-ui/Link' -import { LemonButton } from '@posthog/lemon-ui' -import { samplingFilterLogic } from '../EditorFilters/samplingFilterLogic' -import { posthog } from 'posthog-js' -import { seriesToActionsAndEvents } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' + import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { seriesToActionsAndEvents } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { FunnelsQuery } from '~/queries/schema' -import { supportLogic } from 'lib/components/Support/supportLogic' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { BuilderHog3 } from 'lib/components/hedgehogs' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { SupportModal } from 'lib/components/Support/SupportModal' -import { IconWarning } from '@posthog/icons' +import { FilterType, InsightLogicProps, SavedInsightsTabs } from '~/types' + +import { samplingFilterLogic } from '../EditorFilters/samplingFilterLogic' export function InsightEmptyState({ heading = 'There are no matching events for this query', @@ -312,17 +315,19 @@ export function SavedInsightsEmptyState(): JSX.Element {

    {description}

    )} {tab !== SavedInsightsTabs.Favorites && ( - - - +
    + + } + className="add-insight-button" + > + New Insight + + +
    )}
    diff --git a/frontend/src/scenes/insights/Insight.scss b/frontend/src/scenes/insights/Insight.scss index fafdd2c177116..e0c6b1dd0f13b 100644 --- a/frontend/src/scenes/insights/Insight.scss +++ b/frontend/src/scenes/insights/Insight.scss @@ -1,74 +1,6 @@ @import '../../styles/mixins'; -.insights-page { - .insight-wrapper { - &.insight-wrapper--singlecolumn { - position: relative; - @include screen($xl) { - display: flex; - overflow: hidden; - - .insights-container { - flex: 1; - overflow-x: auto; - } - } - } - } - - .insights-graph-container { - margin-bottom: 1rem; - - .ant-card-head { - border-bottom: 1px solid var(--border); - min-height: unset; - background-color: var(--bg-light); - padding-left: 1rem; - padding-right: 1rem; - - .ant-card-head-title { - padding: 0; - } - } - - .ant-card-body { - padding: 0; - } - - .insights-graph-container-row { - .insights-graph-container-row-left { - width: 100%; - } - - .insights-graph-container-row-right { - height: min(calc(90vh - 16rem), 36rem); // same as .trends-insights-container - max-width: 45%; - min-width: 300px; - width: fit-content; - padding: 0 1rem 1rem 0; - display: flex; - align-items: center; - } - } - - .LineGraph { - // hacky because container not respecting position: relative; - width: calc(100% - 3rem); - height: calc(100% - 3rem); - } - } - - .insight-title-container { - display: flex; - align-items: center; - - .insight-title-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - +.Insight { .retention-date-picker { background-color: transparent; border: 0; @@ -77,20 +9,4 @@ color: var(--default); } } - - .insights-graph-header { - margin-top: 0 !important; - margin-bottom: 0 !important; - padding-left: 1rem; - padding-right: 1rem; - min-height: 48px; - } -} - -.insight-metadata-tags { - margin-top: 0.5rem; - - .ant-tag { - margin-top: 0; - } } diff --git a/frontend/src/scenes/insights/Insight.tsx b/frontend/src/scenes/insights/Insight.tsx index 49d71470b3027..bb3bbc8280f21 100644 --- a/frontend/src/scenes/insights/Insight.tsx +++ b/frontend/src/scenes/insights/Insight.tsx @@ -1,17 +1,20 @@ import './Insight.scss' -import { useEffect } from 'react' + import { BindLogic, useActions, useMountedLogic, useValues } from 'kea' +import { useEffect } from 'react' +import { InsightPageHeader } from 'scenes/insights/InsightPageHeader' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { insightLogic } from './insightLogic' -import { insightCommandLogic } from './insightCommandLogic' -import { insightDataLogic } from './insightDataLogic' -import { InsightShortId, ItemMode } from '~/types' -import { InsightsNav } from './InsightNav/InsightsNav' import { InsightSkeleton } from 'scenes/insights/InsightSkeleton' + import { Query } from '~/queries/Query/Query' -import { InsightPageHeader } from 'scenes/insights/InsightPageHeader' -import { containsHogQLQuery, isInsightVizNode } from '~/queries/utils' import { Node } from '~/queries/schema' +import { containsHogQLQuery, isInsightVizNode } from '~/queries/utils' +import { InsightShortId, ItemMode } from '~/types' + +import { insightCommandLogic } from './insightCommandLogic' +import { insightDataLogic } from './insightDataLogic' +import { insightLogic } from './insightLogic' +import { InsightsNav } from './InsightNav/InsightsNav' export interface InsightSceneProps { insightId: InsightShortId | 'new' } @@ -57,7 +60,7 @@ export function Insight({ insightId }: InsightSceneProps): JSX.Element { return ( -
    +
    {insightMode === ItemMode.Edit && } diff --git a/frontend/src/scenes/insights/InsightNav/InsightsNav.tsx b/frontend/src/scenes/insights/InsightNav/InsightsNav.tsx index ee6541901a058..2f21d90494199 100644 --- a/frontend/src/scenes/insights/InsightNav/InsightsNav.tsx +++ b/frontend/src/scenes/insights/InsightNav/InsightsNav.tsx @@ -1,12 +1,13 @@ import { useActions, useValues } from 'kea' -import { insightLogic } from '../insightLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { FunnelsCue } from '../views/Trends/FunnelsCue' -import { INSIGHT_TYPES_METADATA } from 'scenes/saved-insights/SavedInsights' -import { Link } from 'lib/lemon-ui/Link' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Link } from 'lib/lemon-ui/Link' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { insightNavLogic } from 'scenes/insights/InsightNav/insightNavLogic' import { insightTypeURL } from 'scenes/insights/utils' +import { INSIGHT_TYPES_METADATA } from 'scenes/saved-insights/SavedInsights' + +import { insightLogic } from '../insightLogic' +import { FunnelsCue } from '../views/Trends/FunnelsCue' export function InsightsNav(): JSX.Element { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts b/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts index ca4602559150a..ba1bd58000029 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts @@ -1,13 +1,15 @@ -import { insightLogic } from 'scenes/insights/insightLogic' -import { FunnelVizType, InsightLogicProps, InsightShortId, InsightType, StepOrderValue } from '~/types' -import { insightNavLogic } from 'scenes/insights/InsightNav/insightNavLogic' import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightNavLogic } from 'scenes/insights/InsightNav/insightNavLogic' + import { useMocks } from '~/mocks/jest' +import { nodeKindToDefaultQuery } from '~/queries/nodes/InsightQuery/defaults' import { InsightVizNode, Node, NodeKind } from '~/queries/schema' +import { initKeaTests } from '~/test/init' +import { FunnelVizType, InsightLogicProps, InsightShortId, InsightType, StepOrderValue } from '~/types' + import { insightDataLogic } from '../insightDataLogic' -import { nodeKindToDefaultQuery } from '~/queries/nodes/InsightQuery/defaults' describe('insightNavLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index 500e492003132..3d9936869b22e 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -1,44 +1,45 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' - -import { InsightLogicProps, InsightType, ActionFilter } from '~/types' -import type { insightNavLogicType } from './insightNavLogicType' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { insightDataLogic, queryFromKind } from 'scenes/insights/insightDataLogic' import { insightLogic } from 'scenes/insights/insightLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' + +import { examples, TotalEventsTable } from '~/queries/examples' +import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { insightMap } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { getDisplay, getShowPercentStackView, getShowValueOnSeries } from '~/queries/nodes/InsightViz/utils' import { - InsightVizNode, + ActionsNode, + EventsNode, + FunnelsFilter, + FunnelsQuery, InsightQueryNode, + InsightVizNode, + LifecycleFilter, + LifecycleQuery, NodeKind, - TrendsQuery, - FunnelsQuery, - RetentionQuery, + PathsFilter, PathsQuery, - StickinessQuery, - LifecycleQuery, - TrendsFilter, - FunnelsFilter, RetentionFilter, - PathsFilter, + RetentionQuery, StickinessFilter, - LifecycleFilter, - EventsNode, - ActionsNode, + StickinessQuery, + TrendsFilter, + TrendsQuery, } from '~/queries/schema' -import { insightDataLogic, queryFromKind } from 'scenes/insights/insightDataLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { insightMap } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { - isInsightVizNode, - isRetentionQuery, + containsHogQLQuery, + filterKeyForQuery, isInsightQueryWithBreakdown, isInsightQueryWithSeries, - filterKeyForQuery, - containsHogQLQuery, + isInsightVizNode, + isRetentionQuery, } from '~/queries/utils' -import { examples, TotalEventsTable } from '~/queries/examples' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { getDisplay, getShowPercentStackView, getShowValueOnSeries } from '~/queries/nodes/InsightViz/utils' -import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' +import { ActionFilter, InsightLogicProps, InsightType } from '~/types' + +import type { insightNavLogicType } from './insightNavLogicType' export interface Tab { label: string | JSX.Element @@ -95,9 +96,12 @@ export const insightNavLogic = kea([ }), }, ], - userSelectedView: { - setActiveView: (_, { view }) => view, - }, + userSelectedView: [ + null as InsightType | null, + { + setActiveView: (_, { view }) => view, + }, + ], }), selectors({ activeView: [ diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx index e6ba4ea5b9f2f..c100746612e11 100644 --- a/frontend/src/scenes/insights/InsightPageHeader.tsx +++ b/frontend/src/scenes/insights/InsightPageHeader.tsx @@ -1,48 +1,48 @@ +import { useActions, useMountedLogic, useValues } from 'kea' +import { router } from 'kea-router' +import { AddToDashboard } from 'lib/components/AddToDashboard/AddToDashboard' +import { AddToDashboardModal } from 'lib/components/AddToDashboard/AddToDashboardModal' import { EditableField } from 'lib/components/EditableField/EditableField' - -import { - AvailableFeature, - ExporterFormat, - InsightLogicProps, - InsightModel, - InsightShortId, - ItemMode, - NotebookNodeType, -} from '~/types' +import { ExportButton } from 'lib/components/ExportButton/ExportButton' +import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { PageHeader } from 'lib/components/PageHeader' +import { SharingModal } from 'lib/components/Sharing/SharingModal' +import { SubscribeButton, SubscriptionsModal } from 'lib/components/Subscriptions/SubscriptionsModal' +import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' import { IconLock } from 'lib/lemon-ui/icons' -import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { urls } from 'scenes/urls' -import { SubscribeButton, SubscriptionsModal } from 'lib/components/Subscriptions/SubscriptionsModal' -import { ExportButton } from 'lib/components/ExportButton/ExportButton' -import { deleteWithUndo } from 'lib/utils' -import { AddToDashboard } from 'lib/components/AddToDashboard/AddToDashboard' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { useState } from 'react' +import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' +import { insightCommandLogic } from 'scenes/insights/insightCommandLogic' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' import { InsightSaveButton } from 'scenes/insights/InsightSaveButton' -import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' -import { PageHeader } from 'lib/components/PageHeader' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { insightLogic } from 'scenes/insights/insightLogic' +import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { insightCommandLogic } from 'scenes/insights/insightCommandLogic' +import { teamLogic } from 'scenes/teamLogic' +import { mathsLogic } from 'scenes/trends/mathsLogic' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { groupsModel } from '~/models/groupsModel' + import { cohortsModel } from '~/models/cohortsModel' -import { mathsLogic } from 'scenes/trends/mathsLogic' +import { groupsModel } from '~/models/groupsModel' import { tagsModel } from '~/models/tagsModel' -import { teamLogic } from 'scenes/teamLogic' -import { useActions, useMountedLogic, useValues } from 'kea' -import { router } from 'kea-router' -import { SharingModal } from 'lib/components/Sharing/SharingModal' -import { isInsightVizNode } from '~/queries/utils' -import { summarizeInsight } from 'scenes/insights/summarizeInsight' -import { AddToDashboardModal } from 'lib/components/AddToDashboard/AddToDashboardModal' -import { useState } from 'react' -import { NewDashboardModal } from 'scenes/dashboard/NewDashboardModal' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { DataTableNode, NodeKind } from '~/queries/schema' +import { isInsightVizNode } from '~/queries/utils' +import { + AvailableFeature, + ExporterFormat, + InsightLogicProps, + InsightModel, + InsightShortId, + ItemMode, + NotebookNodeType, +} from '~/types' export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: InsightLogicProps }): JSX.Element { // insightSceneLogic @@ -256,7 +256,7 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: () => { @@ -345,14 +345,14 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In saving={insightSaving} onChange={(_, tags) => setInsightMetadata({ tags: tags ?? [] })} tagsAvailable={tags} - className="insight-metadata-tags" + className="mt-2" data-attr="insight-tags" /> ) : insight.tags?.length ? ( diff --git a/frontend/src/scenes/insights/InsightScene.tsx b/frontend/src/scenes/insights/InsightScene.tsx index 5a8dda166f6ca..99e1e3a5ae23a 100644 --- a/frontend/src/scenes/insights/InsightScene.tsx +++ b/frontend/src/scenes/insights/InsightScene.tsx @@ -1,9 +1,9 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { useValues } from 'kea' +import { NotFound } from 'lib/components/NotFound' import { Insight } from 'scenes/insights/Insight' +import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { InsightSkeleton } from 'scenes/insights/InsightSkeleton' -import { NotFound } from 'lib/components/NotFound' +import { SceneExport } from 'scenes/sceneTypes' export function InsightScene(): JSX.Element { const { insightId, insight, insightLogicRef } = useValues(insightSceneLogic) diff --git a/frontend/src/scenes/insights/InsightSkeleton.tsx b/frontend/src/scenes/insights/InsightSkeleton.tsx index c376a20158586..1ee2f8c53ff97 100644 --- a/frontend/src/scenes/insights/InsightSkeleton.tsx +++ b/frontend/src/scenes/insights/InsightSkeleton.tsx @@ -4,8 +4,8 @@ export function InsightSkeleton(): JSX.Element { return ( <>
    - - + +
    diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss index 2dba11b4efb7e..df45e9cdac85b 100644 --- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss +++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss @@ -24,6 +24,7 @@ border: none; border-bottom-left-radius: 0; border-bottom-right-radius: 0; + &:not(:last-child) { border-bottom: 1px solid var(--border); } @@ -41,23 +42,28 @@ width: 100%; overflow: hidden; } + .LemonTable__content > table > thead { letter-spacing: 0; + .datum-column { .LemonTable__header-content { white-space: nowrap; } } } + .LemonTable__content > table { .datum-label-column { font-weight: 600; display: flex; align-items: center; } + .series-data-cell { font-weight: 600; } + .tag-pill { background-color: var(--border-3000); margin-right: 0; diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx index 0ed7370ac4ec5..af69bc1bf967e 100644 --- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx +++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx @@ -1,10 +1,12 @@ -import { InsightTooltip } from './InsightTooltip' -import { cohortsModel } from '~/models/cohortsModel' -import { useMountedLogic } from 'kea' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { InsightTooltipProps } from './insightTooltipUtils' -import { humanFriendlyNumber } from 'lib/utils' +import { useMountedLogic } from 'kea' import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { humanFriendlyNumber } from 'lib/utils' + +import { cohortsModel } from '~/models/cohortsModel' + +import { InsightTooltip } from './InsightTooltip' +import { InsightTooltipProps } from './insightTooltipUtils' const data = { date: '2022-08-31', @@ -130,9 +132,7 @@ const meta: Meta = { renderSeries: (value) => value, groupTypeLabel: 'people', }, - parameters: { - testOptions: { skip: true }, // FIXME: The InWrapper story fails at locator.screenshot() for some reason - }, + tags: ['test-skip'], // FIXME: The InWrapper story fails at locator.screenshot() for some reason } export default meta diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx index 93d7fa2653e4d..9d4ad4b8b7664 100644 --- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx +++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.tsx @@ -1,22 +1,25 @@ import './InsightTooltip.scss' -import { ReactNode } from 'react' + +import { useValues } from 'kea' +import { InsightLabel } from 'lib/components/InsightLabel' +import { IconHandClick } from 'lib/lemon-ui/icons' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { shortTimeZone } from 'lib/utils' +import { ReactNode } from 'react' +import { formatAggregationValue } from 'scenes/insights/utils' + +import { FormatPropertyValueForDisplayFunction, propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' + import { COL_CUTOFF, - ROW_CUTOFF, + getFormattedDate, getTooltipTitle, InsightTooltipProps, invertDataSource, InvertedSeriesDatum, + ROW_CUTOFF, SeriesDatum, - getFormattedDate, } from './insightTooltipUtils' -import { InsightLabel } from 'lib/components/InsightLabel' -import { IconHandClick } from 'lib/lemon-ui/icons' -import { shortTimeZone } from 'lib/utils' -import { useValues } from 'kea' -import { FormatPropertyValueForDisplayFunction, propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatAggregationValue } from 'scenes/insights/utils' export function ClickToInspectActors({ isTruncated, @@ -54,14 +57,12 @@ function renderDatumToTableCell( // Value can be undefined if the datum's series doesn't have ANY value for the breakdown value being rendered return (
    - { - color && ( - // eslint-disable-next-line react/forbid-dom-props - - ● - - ) /* eslint-disable-line react/forbid-dom-props */ - } + {color && ( + // eslint-disable-next-line react/forbid-dom-props + + ● + + )} {datumValue !== undefined ? formatAggregationValue(datumMathProperty, datumValue, renderCount, formatPropertyValueForDisplay) : '–'} @@ -156,7 +157,7 @@ export function InsightTooltip({ seriesColumnData?.count, formatPropertyValueForDisplay, renderCount, - seriesColumnData.color + seriesColumnData?.color ) }, }) diff --git a/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.scss b/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.scss index 9c105fcc2632a..996dd47bb8056 100644 --- a/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.scss +++ b/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.scss @@ -1,12 +1,9 @@ .legacy-ph-graph-tooltip { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); font-size: 14px; overflow-x: hidden; z-index: var(--z-graph-tooltip); transition: all 0.4s; -} - -.legacy-ph-graph-tooltip { max-width: 480px; padding: 8px 12px; border: 1px solid var(--border); @@ -41,6 +38,7 @@ display: flex; justify-content: center; align-items: center; + svg { font-size: 1.4em; margin-right: 4px; @@ -56,6 +54,7 @@ align-items: center; margin-top: 2px; font-style: italic; + svg { margin-left: 2px; margin-right: 6px; diff --git a/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.tsx b/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.tsx index 9d39b3c637dbd..80dfb1d18c0df 100644 --- a/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.tsx +++ b/frontend/src/scenes/insights/InsightTooltip/LEGACY_InsightTooltip.tsx @@ -1,7 +1,9 @@ +import './LEGACY_InsightTooltip.scss' + import { DateDisplay } from 'lib/components/DateDisplay' import { IconHandClick } from 'lib/lemon-ui/icons' + import { IntervalType } from '~/types' -import './LEGACY_InsightTooltip.scss' interface BodyLine { id?: string | number diff --git a/frontend/src/scenes/insights/InsightTooltip/insightTooltipUtils.tsx b/frontend/src/scenes/insights/InsightTooltip/insightTooltipUtils.tsx index 387a00ad9fd84..21018d4162656 100644 --- a/frontend/src/scenes/insights/InsightTooltip/insightTooltipUtils.tsx +++ b/frontend/src/scenes/insights/InsightTooltip/insightTooltipUtils.tsx @@ -1,10 +1,12 @@ import { dayjs } from 'lib/dayjs' -import { ActionFilter, CompareLabelType, FilterType, IntervalType } from '~/types' import { capitalizeFirstLetter, midEllipsis, pluralize } from 'lib/utils' +import { isTrendsFilter } from 'scenes/insights/sharedUtils' + import { cohortsModel } from '~/models/cohortsModel' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { ActionFilter, CompareLabelType, FilterType, IntervalType } from '~/types' + import { formatBreakdownLabel } from '../utils' -import { isTrendsFilter } from 'scenes/insights/sharedUtils' export interface SeriesDatum { id: number // determines order that series will be displayed in @@ -128,6 +130,9 @@ export function invertDataSource(seriesData: SeriesDatum[]): InvertedSeriesDatum const datumKey = `${s.breakdown_value}-${s.compare_label}` if (datumKey in flattenedData) { flattenedData[datumKey].seriesData.push(s) + flattenedData[datumKey].seriesData = flattenedData[datumKey].seriesData.sort( + (a, b) => (b.action?.order ?? b.dataIndex) - (a.action?.order ?? a.dataIndex) + ) } else { flattenedData[datumKey] = { id: datumKey, diff --git a/frontend/src/scenes/insights/Insights.stories.tsx b/frontend/src/scenes/insights/Insights.stories.tsx index e21a3342167e4..ea0532b199365 100644 --- a/frontend/src/scenes/insights/Insights.stories.tsx +++ b/frontend/src/scenes/insights/Insights.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryObj } from '@storybook/react' -import { mswDecorator } from '~/mocks/browser' -import { samplePersonProperties, sampleRetentionPeopleResponse } from 'scenes/insights/__mocks__/insight.mocks' -import { createInsightStory } from 'scenes/insights/__mocks__/createInsightScene' import { App } from 'scenes/App' +import { createInsightStory } from 'scenes/insights/__mocks__/createInsightScene' +import { samplePersonProperties, sampleRetentionPeopleResponse } from 'scenes/insights/__mocks__/insight.mocks' + +import { mswDecorator } from '~/mocks/browser' type Story = StoryObj const meta: Meta = { @@ -84,6 +85,15 @@ TrendsLineBreakdownEdit.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } +export const TrendsLineBreakdownLabels: Story = createInsightStory( + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLineBreakdown.json'), + 'view', + true +) +TrendsLineBreakdownLabels.parameters = { + testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, +} + export const TrendsBar: Story = createInsightStory( require('../../mocks/fixtures/api/projects/team_id/insights/trendsBar.json') ) @@ -222,6 +232,13 @@ TrendsPieBreakdownEdit.parameters = { testOptions: { waitForSelector: '[data-attr=trend-pie-graph] > canvas' }, } +export const TrendsPieBreakdownLabels: Story = createInsightStory( + require('../../mocks/fixtures/api/projects/team_id/insights/trendsPieBreakdown.json'), + 'view', + true +) +TrendsPieBreakdownLabels.parameters = { testOptions: { waitForSelector: '[data-attr=trend-pie-graph] > canvas' } } + export const TrendsWorldMap: Story = createInsightStory( require('../../mocks/fixtures/api/projects/team_id/insights/trendsWorldMap.json') ) diff --git a/frontend/src/scenes/insights/RetentionDatePicker.tsx b/frontend/src/scenes/insights/RetentionDatePicker.tsx index 4af9318d3078d..19ff93bfa9060 100644 --- a/frontend/src/scenes/insights/RetentionDatePicker.tsx +++ b/frontend/src/scenes/insights/RetentionDatePicker.tsx @@ -1,8 +1,8 @@ import { useActions, useValues } from 'kea' +import { DatePicker } from 'lib/components/DatePicker' +import { dayjs } from 'lib/dayjs' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { insightLogic } from 'scenes/insights/insightLogic' -import { dayjs } from 'lib/dayjs' -import { DatePicker } from 'lib/components/DatePicker' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' export function RetentionDatePicker(): JSX.Element { diff --git a/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx b/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx index 7326a262550eb..95cca49b7cc7f 100644 --- a/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx +++ b/frontend/src/scenes/insights/__mocks__/createInsightScene.tsx @@ -1,15 +1,17 @@ -import { InsightModel } from '~/types' -import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' -import { useEffect } from 'react' +import { StoryFn } from '@storybook/react' import { router } from 'kea-router' -import { App } from 'scenes/App' import { FEATURE_FLAGS } from 'lib/constants' -import { StoryFn } from '@storybook/react' +import { useEffect } from 'react' +import { App } from 'scenes/App' + +import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' +import { InsightModel } from '~/types' let shortCounter = 0 export function createInsightStory( insight: Partial, - mode: 'view' | 'edit' = 'view' + mode: 'view' | 'edit' = 'view', + showLegend: boolean = false ): StoryFn { const count = shortCounter++ return function InsightStory() { @@ -21,7 +23,15 @@ export function createInsightStory( ctx.json({ count: 1, results: [ - { ...insight, short_id: `${insight.short_id}${count}`, id: (insight.id ?? 0) + 1 + count }, + { + ...insight, + short_id: `${insight.short_id}${count}`, + id: (insight.id ?? 0) + 1 + count, + filters: { + ...insight.filters, + show_legend: showLegend, + }, + }, ], }), ], diff --git a/frontend/src/scenes/insights/aggregationAxisFormat.ts b/frontend/src/scenes/insights/aggregationAxisFormat.ts index 0ffa78d829b4f..55aa3b48a2bb2 100644 --- a/frontend/src/scenes/insights/aggregationAxisFormat.ts +++ b/frontend/src/scenes/insights/aggregationAxisFormat.ts @@ -1,5 +1,6 @@ import { LemonSelectOptionLeaf } from 'lib/lemon-ui/LemonSelect' import { humanFriendlyDuration, humanFriendlyNumber, percentage } from 'lib/utils' + import { TrendsFilter } from '~/queries/schema' import { ChartDisplayType, TrendsFilterType } from '~/types' diff --git a/frontend/src/scenes/insights/aggregationAxisFormats.test.ts b/frontend/src/scenes/insights/aggregationAxisFormats.test.ts index 7989f851c3396..56db792c0a3e3 100644 --- a/frontend/src/scenes/insights/aggregationAxisFormats.test.ts +++ b/frontend/src/scenes/insights/aggregationAxisFormats.test.ts @@ -1,4 +1,5 @@ import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' + import { FilterType } from '~/types' describe('formatAggregationAxisValue', () => { @@ -25,7 +26,9 @@ describe('formatAggregationAxisValue', () => { }, ] formatTestcases.forEach((testcase) => { - it(`correctly formats "${testcase.candidate}" as ${testcase.expected} when filters are ${testcase.filters}`, () => { + it(`correctly formats "${testcase.candidate}" as ${testcase.expected} when filters are ${JSON.stringify( + testcase.filters + )}`, () => { expect(formatAggregationAxisValue(testcase.filters as Partial, testcase.candidate)).toEqual( testcase.expected ) diff --git a/frontend/src/scenes/insights/common.tsx b/frontend/src/scenes/insights/common.tsx index ac5bf6b2319b7..4c50d2bfeb146 100644 --- a/frontend/src/scenes/insights/common.tsx +++ b/frontend/src/scenes/insights/common.tsx @@ -1,7 +1,8 @@ -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import clsx from 'clsx' import '../../lib/components/PropertyGroupFilters/PropertyGroupFilters.scss' + +import clsx from 'clsx' import { IconInfo } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' export function GlobalFiltersTitle({ unit = 'series', diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.scss b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.scss index 78ef6f4aa2e32..129c9a28765eb 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.scss +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.scss @@ -16,7 +16,7 @@ margin-top: 0; .ActionFilterRow-content { - margin-bottom: 0px; + margin-bottom: 0; padding: 1rem; border-bottom: 1px solid var(--border); } diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.stories.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.stories.tsx index d626ed305f84d..227749eee54cd 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.stories.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.stories.tsx @@ -1,16 +1,18 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { useMountedLogic, useValues } from 'kea' +import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' +import { alphabet, uuid } from 'lib/utils' import { useRef, useState } from 'react' -import { ActionFilter, ActionFilterProps } from './ActionFilter' +import { isFilterWithDisplay, isLifecycleFilter } from 'scenes/insights/sharedUtils' + import { cohortsModel } from '~/models/cohortsModel' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { taxonomicFilterMocksDecorator } from 'lib/components/TaxonomicFilter/__mocks__/taxonomicFilterMocksDecorator' -import { useMountedLogic, useValues } from 'kea' +import { groupsModel } from '~/models/groupsModel' import { FilterType, InsightType } from '~/types' + +import { ActionFilter, ActionFilterProps } from './ActionFilter' import { MathAvailability } from './ActionFilterRow/ActionFilterRow' -import { groupsModel } from '~/models/groupsModel' -import { alphabet, uuid } from 'lib/utils' -import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' -import { isFilterWithDisplay, isLifecycleFilter } from 'scenes/insights/sharedUtils' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx index adcbb55787bb9..9768b5a9114da 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx @@ -1,20 +1,23 @@ import './ActionFilter.scss' -import React, { useEffect } from 'react' + +import { DndContext } from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' -import { entityFilterLogic, toFilters, LocalFilter } from './entityFilterLogic' -import { ActionFilterRow, MathAvailability } from './ActionFilterRow/ActionFilterRow' -import { ActionFilter as ActionFilterType, FilterType, FunnelExclusion, InsightType, Optional } from '~/types' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { RenameModal } from 'scenes/insights/filters/ActionFilter/RenameModal' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from '../../../teamLogic' -import clsx from 'clsx' -import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { IconPlusMini } from 'lib/lemon-ui/icons' -import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' -import { DndContext } from '@dnd-kit/core' -import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { verticalSortableListCollisionDetection } from 'lib/sortable' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import React, { useEffect } from 'react' +import { RenameModal } from 'scenes/insights/filters/ActionFilter/RenameModal' + +import { ActionFilter as ActionFilterType, FilterType, FunnelExclusion, InsightType, Optional } from '~/types' + +import { teamLogic } from '../../../teamLogic' +import { ActionFilterRow, MathAvailability } from './ActionFilterRow/ActionFilterRow' +import { entityFilterLogic, LocalFilter, toFilters } from './entityFilterLogic' export interface ActionFilterProps { setFilters: (filters: FilterType) => void diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 26b736f8e28f3..528b9fab025d2 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -1,23 +1,26 @@ +import './ActionFilterRow.scss' + +import { DraggableSyntheticListeners } from '@dnd-kit/core' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' import { BuiltLogic, useActions, useValues } from 'kea' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { - ActionFilter as ActionFilterType, - ActionFilter, - EntityType, - EntityTypes, - FunnelExclusion, - PropertyFilterValue, - BaseMathType, - PropertyMathType, - CountPerActorMathType, - HogQLMathType, -} from '~/types' +import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { getEventNamesForAction } from 'lib/utils' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SeriesGlyph, SeriesLetter } from 'lib/components/SeriesGlyph' -import './ActionFilterRow.scss' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' +import { TaxonomicPopover, TaxonomicStringPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' +import { IconCopy, IconDelete, IconEdit, IconFilter, IconWithCount } from 'lib/lemon-ui/icons' +import { SortableDragIcon } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { getEventNamesForAction } from 'lib/utils' +import { useState } from 'react' +import { GroupIntroductionFooter } from 'scenes/groups/GroupsIntroduction' +import { isAllEventsEntityFilter } from 'scenes/insights/utils' import { apiValueToMathType, COUNT_PER_ACTOR_MATH_DEFINITIONS, @@ -26,23 +29,23 @@ import { mathTypeToApiValues, PROPERTY_MATH_DEFINITIONS, } from 'scenes/trends/mathsLogic' + import { actionsModel } from '~/models/actionsModel' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { TaxonomicPopover, TaxonomicStringPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' -import { IconCopy, IconDelete, IconEdit, IconFilter, IconWithCount } from 'lib/lemon-ui/icons' -import { SortableDragIcon } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' -import { useState } from 'react' -import { GroupIntroductionFooter } from 'scenes/groups/GroupsIntroduction' -import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' -import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' -import { entityFilterLogicType } from '../entityFilterLogicType' -import { isAllEventsEntityFilter } from 'scenes/insights/utils' -import { useSortable } from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' +import { + ActionFilter, + ActionFilter as ActionFilterType, + BaseMathType, + CountPerActorMathType, + EntityType, + EntityTypes, + FunnelExclusion, + HogQLMathType, + PropertyFilterValue, + PropertyMathType, +} from '~/types' + import { LocalFilter } from '../entityFilterLogic' -import { DraggableSyntheticListeners } from '@dnd-kit/core' +import { entityFilterLogicType } from '../entityFilterLogicType' const DragHandle = (props: DraggableSyntheticListeners | undefined): JSX.Element => ( diff --git a/frontend/src/scenes/insights/filters/ActionFilter/RenameModal.tsx b/frontend/src/scenes/insights/filters/ActionFilter/RenameModal.tsx index 8e98a5fb83159..30609a83af527 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/RenameModal.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/RenameModal.tsx @@ -1,9 +1,10 @@ +import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' -import { InsightType } from '~/types' -import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' import { renameModalLogic } from 'scenes/insights/filters/ActionFilter/renameModalLogic' -import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' +import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' + +import { InsightType } from '~/types' interface RenameModalProps { typeKey: string diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts index dfeb640ded0a8..d4f6cf1840f2d 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts @@ -1,11 +1,13 @@ -import { entityFilterLogic, toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { expectLogic } from 'kea-test-utils' +import * as libUtils from 'lib/utils' +import { entityFilterLogic, toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' + +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import filtersJson from './__mocks__/filters.json' -import eventDefinitionsJson from './__mocks__/event_definitions.json' import { FilterType } from '~/types' -import { useMocks } from '~/mocks/jest' -import * as libUtils from 'lib/utils' + +import eventDefinitionsJson from './__mocks__/event_definitions.json' +import filtersJson from './__mocks__/filters.json' describe('entityFilterLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index 9c59d413c9a21..0d5e88b045137 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -1,8 +1,11 @@ -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import { EntityTypes, FilterType, Entity, EntityType, ActionFilter, EntityFilter, AnyPropertyFilter } from '~/types' -import type { entityFilterLogicType } from './entityFilterLogicType' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' +import { uuid } from 'lib/utils' import { eventUsageLogic, GraphSeriesAddedSource } from 'lib/utils/eventUsageLogic' -import { convertPropertyGroupToProperties, uuid } from 'lib/utils' + +import { ActionFilter, AnyPropertyFilter, Entity, EntityFilter, EntityType, EntityTypes, FilterType } from '~/types' + +import type { entityFilterLogicType } from './entityFilterLogicType' export type LocalFilter = ActionFilter & { order: number @@ -113,7 +116,7 @@ export const entityFilterLogic = kea([ }, ], localFilters: [ - toLocalFilters(props.filters ?? {}) as LocalFilter[], + toLocalFilters(props.filters ?? {}), { setLocalFilters: (_, { filters }) => toLocalFilters(filters), }, @@ -176,9 +179,7 @@ export const entityFilterLogic = kea([ }, updateFilterProperty: async ({ properties, index }) => { actions.setFilters( - values.localFilters.map( - (filter, i) => (i === index ? { ...filter, properties } : filter) as LocalFilter - ) + values.localFilters.map((filter, i) => (i === index ? { ...filter, properties } : filter)) ) }, updateFilterMath: async ({ index, ...mathProperties }) => { diff --git a/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.test.ts b/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.test.ts index 3b62100791455..e6bcec0dafe4c 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.test.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.test.ts @@ -1,10 +1,12 @@ -import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { expectLogic } from 'kea-test-utils' -import filtersJson from './__mocks__/filters.json' -import { EntityFilter } from '~/types' +import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { renameModalLogic } from 'scenes/insights/filters/ActionFilter/renameModalLogic' import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' + import { initKeaTests } from '~/test/init' +import { EntityFilter } from '~/types' + +import filtersJson from './__mocks__/filters.json' describe('renameModalLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.ts index 82e1a81421993..54e868661cf86 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/renameModalLogic.ts @@ -1,8 +1,10 @@ -import { kea, props, key, path, connect, actions, reducers } from 'kea' -import type { renameModalLogicType } from './renameModalLogicType' -import { EntityFilterTypes } from '~/types' -import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' +import { actions, connect, kea, key, path, props, reducers } from 'kea' import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' +import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' + +import { EntityFilterTypes } from '~/types' + +import type { renameModalLogicType } from './renameModalLogicType' export interface RenameModalProps { filter: EntityFilterTypes diff --git a/frontend/src/scenes/insights/filters/AggregationSelect.tsx b/frontend/src/scenes/insights/filters/AggregationSelect.tsx index 766860da847a8..67b8872c065c9 100644 --- a/frontend/src/scenes/insights/filters/AggregationSelect.tsx +++ b/frontend/src/scenes/insights/filters/AggregationSelect.tsx @@ -1,13 +1,14 @@ -import { useActions, useValues } from 'kea' -import { groupsModel } from '~/models/groupsModel' import { LemonSelect, LemonSelectSection } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' import { GroupIntroductionFooter } from 'scenes/groups/GroupsIntroduction' -import { InsightLogicProps } from '~/types' -import { isFunnelsQuery, isInsightQueryNode } from '~/queries/utils' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + +import { groupsModel } from '~/models/groupsModel' import { FunnelsQuery } from '~/queries/schema' -import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' +import { isFunnelsQuery, isInsightQueryNode, isLifecycleQuery, isStickinessQuery } from '~/queries/utils' +import { InsightLogicProps } from '~/types' function getHogQLValue(groupIndex?: number, aggregationQuery?: string): string { if (groupIndex !== undefined) { @@ -51,7 +52,9 @@ export function AggregationSelect({ } const value = getHogQLValue( - querySource.aggregation_group_type_index, + isLifecycleQuery(querySource) || isStickinessQuery(querySource) + ? undefined + : querySource.aggregation_group_type_index, isFunnelsQuery(querySource) ? querySource.funnelsFilter?.funnel_aggregate_by_hogql : undefined ) const onChange = (value: string): void => { diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTag.tsx b/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTag.tsx index 8437268b15c9a..045b06d4865b5 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTag.tsx +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTag.tsx @@ -1,19 +1,20 @@ -import { useState } from 'react' -import { BindLogic, useActions, useValues } from 'kea' +import './BreakdownTag.scss' import { LemonTag, LemonTagProps } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' +import { HoqQLPropertyInfo } from 'lib/components/HoqQLPropertyInfo' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { breakdownTagLogic } from './breakdownTagLogic' -import { BreakdownTagMenu } from './BreakdownTagMenu' -import { BreakdownType } from '~/types' -import { TaxonomicBreakdownPopover } from './TaxonomicBreakdownPopover' import { PopoverReferenceContext } from 'lib/lemon-ui/Popover/Popover' -import { HoqQLPropertyInfo } from 'lib/components/HoqQLPropertyInfo' +import { useState } from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' + import { cohortsModel } from '~/models/cohortsModel' -import { isAllCohort, isCohort } from './taxonomicBreakdownFilterUtils' +import { BreakdownType } from '~/types' -import './BreakdownTag.scss' -import { insightLogic } from 'scenes/insights/insightLogic' +import { breakdownTagLogic } from './breakdownTagLogic' +import { BreakdownTagMenu } from './BreakdownTagMenu' +import { isAllCohort, isCohort } from './taxonomicBreakdownFilterUtils' +import { TaxonomicBreakdownPopover } from './TaxonomicBreakdownPopover' type EditableBreakdownTagProps = { breakdown: string | number diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTagMenu.tsx b/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTagMenu.tsx index 49cbafae798c7..ae89f81f09966 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTagMenu.tsx +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/BreakdownTagMenu.tsx @@ -1,14 +1,13 @@ -import { useActions, useValues } from 'kea' +import './BreakdownTagMenu.scss' import { LemonButton, LemonDivider, LemonInput, LemonSwitch } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useActions, useValues } from 'kea' import { IconInfo } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { breakdownTagLogic } from './breakdownTagLogic' import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' -import './BreakdownTagMenu.scss' - export const BreakdownTagMenu = (): JSX.Element => { const { isHistogramable, isNormalizeable } = useValues(breakdownTagLogic) const { removeBreakdown } = useActions(breakdownTagLogic) diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownButton.tsx b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownButton.tsx index d61cb0bf2b47d..b4d3f7b920026 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownButton.tsx +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownButton.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' -import { useValues } from 'kea' import { LemonButton } from '@posthog/lemon-ui' +import { useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { IconPlusMini } from 'lib/lemon-ui/icons' +import { useState } from 'react' + import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' import { TaxonomicBreakdownPopover } from './TaxonomicBreakdownPopover' diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownFilter.tsx b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownFilter.tsx index 0bd1856e2205a..6b98d27118390 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownFilter.tsx +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownFilter.tsx @@ -1,7 +1,9 @@ import { BindLogic, useValues } from 'kea' import { TaxonomicBreakdownButton } from 'scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownButton' + import { BreakdownFilter } from '~/queries/schema' import { ChartDisplayType, InsightLogicProps } from '~/types' + import { EditableBreakdownTag } from './BreakdownTag' import { taxonomicBreakdownFilterLogic, TaxonomicBreakdownFilterLogicProps } from './taxonomicBreakdownFilterLogic' diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownPopover.tsx b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownPopover.tsx index fe3f64a0baa54..24be984cf9b84 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownPopover.tsx +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/TaxonomicBreakdownPopover.tsx @@ -1,12 +1,12 @@ import { useActions, useValues } from 'kea' +import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { insightLogic } from 'scenes/insights/insightLogic' import { groupsModel } from '~/models/groupsModel' -import { insightLogic } from 'scenes/insights/insightLogic' -import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' -import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' type TaxonomicBreakdownPopoverProps = { open: boolean diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/breakdownTagLogic.ts b/frontend/src/scenes/insights/filters/BreakdownFilter/breakdownTagLogic.ts index 14f98e93a4883..f7dc476e70840 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/breakdownTagLogic.ts +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/breakdownTagLogic.ts @@ -1,13 +1,14 @@ import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea' +import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { InsightLogicProps } from '~/types' import type { breakdownTagLogicType } from './breakdownTagLogicType' import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' import { isURLNormalizeable } from './taxonomicBreakdownFilterUtils' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { cohortsModel } from '~/models/cohortsModel' -import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils' -import { InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' export interface BreakdownTagLogicProps { insightProps: InsightLogicProps diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts index e2ae3a3e84ba1..93540de467b96 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.test.ts @@ -1,11 +1,11 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' - import { TaxonomicFilterGroup, TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' +import { initKeaTests } from '~/test/init' import { InsightLogicProps } from '~/types' +import { taxonomicBreakdownFilterLogic } from './taxonomicBreakdownFilterLogic' + const taxonomicGroupFor = ( type: TaxonomicFilterGroupType, groupTypeIndex: number | undefined = undefined diff --git a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts index b6583ebda2cbd..ef802a4a6495e 100644 --- a/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterLogic.ts @@ -9,13 +9,14 @@ import { TaxonomicFilterGroupType, TaxonomicFilterValue, } from 'lib/components/TaxonomicFilter/types' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { BreakdownFilter } from '~/queries/schema' -import { isCohortBreakdown, isURLNormalizeable } from './taxonomicBreakdownFilterUtils' import { BreakdownType, ChartDisplayType, InsightLogicProps } from '~/types' import type { taxonomicBreakdownFilterLogicType } from './taxonomicBreakdownFilterLogicType' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { isCohortBreakdown, isURLNormalizeable } from './taxonomicBreakdownFilterUtils' export type TaxonomicBreakdownFilterLogicProps = { insightProps: InsightLogicProps diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx index f77d781c56ee5..50609fa5cfeef 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx @@ -1,15 +1,16 @@ +import { LemonButton } from '@posthog/lemon-ui' import { Select } from 'antd' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { ANTD_TOOLTIP_PLACEMENTS } from 'lib/utils' -import { FunnelExclusion, ActionFilter as ActionFilterType, FunnelsFilterType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonButton } from '@posthog/lemon-ui' import { IconDelete } from 'lib/lemon-ui/icons' -import { FunnelsQuery } from '~/queries/schema' +import { ANTD_TOOLTIP_PLACEMENTS } from 'lib/utils' import { getClampedStepRangeFilterDataExploration } from 'scenes/funnels/funnelUtils' -import clsx from 'clsx' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { FunnelsQuery } from '~/queries/schema' +import { ActionFilter as ActionFilterType, FunnelExclusion, FunnelsFilterType } from '~/types' + type ExclusionRowSuffixComponentBaseProps = { filter: ActionFilterType | FunnelExclusion index: number diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx index bcaa3a2c846fa..a9ce4de510ff4 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx @@ -1,15 +1,17 @@ -import { useRef } from 'react' -import { useActions, useValues } from 'kea' import useSize from '@react-hook/size' -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { useActions, useValues } from 'kea' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { FunnelExclusion, EntityTypes, FilterType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' +import { useRef } from 'react' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { ExclusionRowSuffix } from './ExclusionRowSuffix' -import { ExclusionRow } from './ExclusionRow' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { EntityTypes, FilterType, FunnelExclusion } from '~/types' + +import { ExclusionRow } from './ExclusionRow' +import { ExclusionRowSuffix } from './ExclusionRowSuffix' export function FunnelExclusionsFilter(): JSX.Element { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx b/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx index c9cf20baf7c5c..b8b9cedf1f362 100644 --- a/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx +++ b/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx @@ -1,11 +1,10 @@ +import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' -import { FunnelStepReference } from '~/types' -import { LemonSelect } from '@posthog/lemon-ui' import { FunnelsFilter } from '~/queries/schema' +import { FunnelStepReference } from '~/types' export function FunnelStepReferencePicker(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx b/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx index 904f630ac42b6..c4b81e4a667ad 100644 --- a/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx +++ b/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx @@ -1,9 +1,9 @@ -import { useValues, useActions } from 'kea' +import { IconCalendar, IconInfo } from '@posthog/icons' +import { Tooltip } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { insightLogic } from 'scenes/insights/insightLogic' -import { IconCalendar, IconInfo } from '@posthog/icons' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { Tooltip } from '@posthog/lemon-ui' type InsightDateFilterProps = { disabled: boolean diff --git a/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts b/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts index 76c8b4da0321c..c4449574549a0 100644 --- a/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts @@ -1,8 +1,10 @@ -import { kea, props, key, path, connect, actions, selectors, listeners } from 'kea' -import type { insightDateFilterLogicType } from './insightDateFilterLogicType' -import { InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { InsightLogicProps } from '~/types' + +import type { insightDateFilterLogicType } from './insightDateFilterLogicType' export const insightDateFilterLogic = kea([ props({} as InsightLogicProps), diff --git a/frontend/src/scenes/insights/filters/PathCleaningFilter.tsx b/frontend/src/scenes/insights/filters/PathCleaningFilter.tsx index 067a9ccb87850..5c482f83e0528 100644 --- a/frontend/src/scenes/insights/filters/PathCleaningFilter.tsx +++ b/frontend/src/scenes/insights/filters/PathCleaningFilter.tsx @@ -1,12 +1,11 @@ +import { LemonSwitch } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' - +import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' import { teamLogic } from 'scenes/teamLogic' import { EditorFilterProps } from '~/types' -import { LemonSwitch } from '@posthog/lemon-ui' -import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' -import { Tooltip } from 'lib/lemon-ui/Tooltip' export function PathCleaningFilter({ insightProps }: EditorFilterProps): JSX.Element { const { pathsFilter } = useValues(pathsDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx b/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx index a5f9d72f2b649..f185ce8f755cd 100644 --- a/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx +++ b/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx @@ -1,8 +1,8 @@ -import { Select } from 'antd' // eslint-disable-next-line no-restricted-imports import { PercentageOutlined } from '@ant-design/icons' -import { insightLogic } from 'scenes/insights/insightLogic' +import { Select } from 'antd' import { useActions, useValues } from 'kea' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' export function RetentionReferencePicker(): JSX.Element { diff --git a/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx b/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx index 19eeb45ef7347..b71637cfcb728 100644 --- a/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx +++ b/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx @@ -1,11 +1,12 @@ import { useActions, useValues } from 'kea' -import { FilterType } from '~/types' -import { teamLogic } from 'scenes/teamLogic' -import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconSettings } from 'lib/lemon-ui/icons' -import { urls } from 'scenes/urls' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { FilterType } from '~/types' export function TestAccountFilter({ filters, diff --git a/frontend/src/scenes/insights/insightCommandLogic.ts b/frontend/src/scenes/insights/insightCommandLogic.ts index c94449d881249..12d70300e2a0a 100644 --- a/frontend/src/scenes/insights/insightCommandLogic.ts +++ b/frontend/src/scenes/insights/insightCommandLogic.ts @@ -1,12 +1,13 @@ +import { connect, events, kea, key, path, props } from 'kea' import { Command, commandPaletteLogic } from 'lib/components/CommandPalette/commandPaletteLogic' -import { kea, props, key, path, connect, events } from 'kea' -import type { insightCommandLogicType } from './insightCommandLogicType' -import { compareFilterLogic } from 'lib/components/CompareFilter/compareFilterLogic' +import { IconTrendingUp } from 'lib/lemon-ui/icons' import { dateMapping } from 'lib/utils' -import { InsightLogicProps } from '~/types' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { InsightLogicProps } from '~/types' + +import type { insightCommandLogicType } from './insightCommandLogicType' import { insightVizDataLogic } from './insightVizDataLogic' -import { IconTrendingUp } from 'lib/lemon-ui/icons' const INSIGHT_COMMAND_SCOPE = 'insights' @@ -15,7 +16,7 @@ export const insightCommandLogic = kea([ key(keyForInsightLogicProps('new')), path((key) => ['scenes', 'insights', 'insightCommandLogic', key]), - connect((props: InsightLogicProps) => [commandPaletteLogic, compareFilterLogic(props), insightVizDataLogic(props)]), + connect((props: InsightLogicProps) => [commandPaletteLogic, insightVizDataLogic(props)]), events(({ props }) => ({ afterMount: () => { const funnelCommands: Command[] = [ @@ -26,7 +27,8 @@ export const insightCommandLogic = kea([ icon: IconTrendingUp, display: 'Toggle "Compare Previous" on Graph', executor: () => { - compareFilterLogic(props).actions.toggleCompare() + const compare = insightVizDataLogic(props).values.compare + insightVizDataLogic(props).actions.updateInsightFilter({ compare: !compare }) }, }, ...dateMapping.map(({ key, values }) => ({ diff --git a/frontend/src/scenes/insights/insightDataLogic.test.ts b/frontend/src/scenes/insights/insightDataLogic.test.ts index 52c99c600f902..acc5b30da8093 100644 --- a/frontend/src/scenes/insights/insightDataLogic.test.ts +++ b/frontend/src/scenes/insights/insightDataLogic.test.ts @@ -1,14 +1,14 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { insightLogic } from 'scenes/insights/insightLogic' +import { useMocks } from '~/mocks/jest' +import { examples } from '~/queries/examples' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { NodeKind, TrendsQuery } from '~/queries/schema' +import { initKeaTests } from '~/test/init' import { InsightShortId } from '~/types' import { insightDataLogic } from './insightDataLogic' -import { NodeKind, TrendsQuery } from '~/queries/schema' -import { useMocks } from '~/mocks/jest' -import { insightLogic } from 'scenes/insights/insightLogic' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { examples } from '~/queries/examples' const Insight123 = '123' as InsightShortId diff --git a/frontend/src/scenes/insights/insightDataLogic.ts b/frontend/src/scenes/insights/insightDataLogic.ts index 89e2dec570f32..a392c583e8c58 100644 --- a/frontend/src/scenes/insights/insightDataLogic.ts +++ b/frontend/src/scenes/insights/insightDataLogic.ts @@ -1,23 +1,24 @@ import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' -import { FilterType, InsightLogicProps, InsightType } from '~/types' +import { objectsEqual } from 'lib/utils' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' +import { teamLogic } from 'scenes/teamLogic' + +import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' +import { insightTypeToDefaultQuery, nodeKindToDefaultQuery } from '~/queries/nodes/InsightQuery/defaults' +import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' +import { queryExportContext } from '~/queries/query' import { InsightNodeKind, InsightVizNode, Node, NodeKind } from '~/queries/schema' +import { isInsightVizNode } from '~/queries/utils' +import { FilterType, InsightLogicProps, InsightType } from '~/types' import type { insightDataLogicType } from './insightDataLogicType' +import { insightDataTimingLogic } from './insightDataTimingLogic' import { insightLogic } from './insightLogic' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { isInsightVizNode } from '~/queries/utils' import { cleanFilters, setTestAccountFilterForNewInsight } from './utils/cleanFilters' -import { insightTypeToDefaultQuery, nodeKindToDefaultQuery } from '~/queries/nodes/InsightQuery/defaults' -import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' -import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' -import { queryExportContext } from '~/queries/query' -import { objectsEqual } from 'lib/utils' import { compareFilters } from './utils/compareFilters' -import { insightDataTimingLogic } from './insightDataTimingLogic' -import { teamLogic } from 'scenes/teamLogic' -import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' const queryFromFilters = (filters: Partial): InsightVizNode => ({ kind: NodeKind.InsightVizNode, diff --git a/frontend/src/scenes/insights/insightDataTimingLogic.ts b/frontend/src/scenes/insights/insightDataTimingLogic.ts index d84ce7b8e240f..22d6558d43b42 100644 --- a/frontend/src/scenes/insights/insightDataTimingLogic.ts +++ b/frontend/src/scenes/insights/insightDataTimingLogic.ts @@ -1,12 +1,13 @@ -import { kea, props, key, path, connect, listeners, reducers, actions } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea' +import { captureTimeToSeeData } from 'lib/internalMetrics' +import { teamLogic } from 'scenes/teamLogic' + import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' import { InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from './sharedUtils' import type { insightDataTimingLogicType } from './insightDataTimingLogicType' -import { teamLogic } from 'scenes/teamLogic' -import { captureTimeToSeeData } from 'lib/internalMetrics' +import { keyForInsightLogicProps } from './sharedUtils' export const insightDataTimingLogic = kea([ props({} as InsightLogicProps), @@ -51,8 +52,7 @@ export const insightDataTimingLogic = kea([ } const duration = performance.now() - values.queryStartTimes[payload.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: payload.queryId, @@ -66,6 +66,7 @@ export const insightDataTimingLogic = kea([ insight: values.query.kind, is_primary_interaction: true, }) + actions.removeQuery(payload.queryId) }, loadDataFailure: ({ errorObject }) => { @@ -75,8 +76,7 @@ export const insightDataTimingLogic = kea([ } const duration = performance.now() - values.queryStartTimes[errorObject.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: errorObject.queryId, @@ -90,12 +90,12 @@ export const insightDataTimingLogic = kea([ insight: values.query.kind, is_primary_interaction: true, }) + actions.removeQuery(errorObject.queryId) }, loadDataCancellation: (payload) => { const duration = performance.now() - values.queryStartTimes[payload.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: payload.queryId, @@ -107,6 +107,7 @@ export const insightDataTimingLogic = kea([ api_response_bytes: 0, insight: values.query.kind, }) + actions.removeQuery(payload.queryId) }, })), diff --git a/frontend/src/scenes/insights/insightLogic.test.ts b/frontend/src/scenes/insights/insightLogic.test.ts index 038762fdfc77b..3856ef09974a3 100644 --- a/frontend/src/scenes/insights/insightLogic.test.ts +++ b/frontend/src/scenes/insights/insightLogic.test.ts @@ -1,6 +1,22 @@ +import { combineUrl, router } from 'kea-router' import { expectLogic, partial, truth } from 'kea-test-utils' +import api from 'lib/api' +import { MOCK_DEFAULT_TEAM, MOCK_TEAM_ID } from 'lib/api.mock' +import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' +import { useAvailableFeatures } from '~/mocks/features' +import { useMocks } from '~/mocks/jest' +import { dashboardsModel } from '~/models/dashboardsModel' +import { insightsModel } from '~/models/insightsModel' +import { DataTableNode, NodeKind } from '~/queries/schema' import { initKeaTests } from '~/test/init' -import { createEmptyInsight, insightLogic } from './insightLogic' import { AnyPropertyFilter, AvailableFeature, @@ -18,22 +34,8 @@ import { PropertyGroupFilter, PropertyOperator, } from '~/types' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { combineUrl, router } from 'kea-router' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { teamLogic } from 'scenes/teamLogic' -import { urls } from 'scenes/urls' -import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' -import { useMocks } from '~/mocks/jest' -import { useAvailableFeatures } from '~/mocks/features' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' -import { MOCK_DEFAULT_TEAM, MOCK_TEAM_ID } from 'lib/api.mock' -import { dashboardsModel } from '~/models/dashboardsModel' -import { insightsModel } from '~/models/insightsModel' -import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants' -import api from 'lib/api' -import { DataTableNode, NodeKind } from '~/queries/schema' + +import { createEmptyInsight, insightLogic } from './insightLogic' const API_FILTERS: Partial = { insight: InsightType.TRENDS as InsightType, diff --git a/frontend/src/scenes/insights/insightLogic.ts b/frontend/src/scenes/insights/insightLogic.ts index 825d1a1237265..75649d9bcbde3 100644 --- a/frontend/src/scenes/insights/insightLogic.ts +++ b/frontend/src/scenes/insights/insightLogic.ts @@ -1,22 +1,17 @@ -import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { promptLogic } from 'lib/logic/promptLogic' -import { getEventNamesForAction, objectsEqual, sum, toParams } from 'lib/utils' -import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic' -import type { insightLogicType } from './insightLogicType' import { captureException } from '@sentry/react' -import { - ActionType, - FilterType, - InsightLogicProps, - InsightModel, - InsightShortId, - ItemMode, - SetInsightOptions, - TrendsFilterType, -} from '~/types' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import { router } from 'kea-router' import api from 'lib/api' +import { TriggerExportProps } from 'lib/components/ExportButton/exporter' +import { parseProperties } from 'lib/components/PropertyFilters/utils' +import { DashboardPrivilegeLevel } from 'lib/constants' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { promptLogic } from 'lib/logic/promptLogic' +import { getEventNamesForAction, objectsEqual, sum, toParams } from 'lib/utils' +import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic' +import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' +import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { filterTrendsClientSideParams, isFilterWithHiddenLegendKeys, @@ -28,30 +23,37 @@ import { isTrendsFilter, keyForInsightLogicProps, } from 'scenes/insights/sharedUtils' +import { summarizeInsight } from 'scenes/insights/summarizeInsight' import { cleanFilters } from 'scenes/insights/utils/cleanFilters' -import { dashboardsModel } from '~/models/dashboardsModel' -import { extractObjectDiffKeys, findInsightFromMountedLogic, getInsightId } from './utils' -import { teamLogic } from '../teamLogic' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + import { actionsModel } from '~/models/actionsModel' -import { DashboardPrivilegeLevel } from 'lib/constants' -import { groupsModel } from '~/models/groupsModel' import { cohortsModel } from '~/models/cohortsModel' -import { mathsLogic } from 'scenes/trends/mathsLogic' -import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { TriggerExportProps } from 'lib/components/ExportButton/exporter' -import { parseProperties } from 'lib/components/PropertyFilters/utils' +import { dashboardsModel } from '~/models/dashboardsModel' +import { groupsModel } from '~/models/groupsModel' import { insightsModel } from '~/models/insightsModel' -import { toLocalFilters } from './filters/ActionFilter/entityFilterLogic' -import { loaders } from 'kea-loaders' -import { queryExportContext } from '~/queries/query' import { tagsModel } from '~/models/tagsModel' -import { isInsightVizNode } from '~/queries/utils' -import { userLogic } from 'scenes/userLogic' -import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' -import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { queryExportContext } from '~/queries/query' import { InsightVizNode } from '~/queries/schema' +import { isInsightVizNode } from '~/queries/utils' +import { + ActionType, + FilterType, + InsightLogicProps, + InsightModel, + InsightShortId, + ItemMode, + SetInsightOptions, + TrendsFilterType, +} from '~/types' + +import { teamLogic } from '../teamLogic' +import { toLocalFilters } from './filters/ActionFilter/entityFilterLogic' +import type { insightLogicType } from './insightLogicType' +import { extractObjectDiffKeys, findInsightFromMountedLogic, getInsightId } from './utils' const IS_TEST_MODE = process.env.NODE_ENV === 'test' export const UNSAVED_INSIGHT_MIN_REFRESH_INTERVAL_MINUTES = 3 diff --git a/frontend/src/scenes/insights/insightSceneLogic.test.ts b/frontend/src/scenes/insights/insightSceneLogic.test.ts index 15749a0d4caf4..1d3b68fa2300d 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.test.ts +++ b/frontend/src/scenes/insights/insightSceneLogic.test.ts @@ -1,10 +1,11 @@ import { combineUrl, router } from 'kea-router' -import { urls } from 'scenes/urls' import { expectLogic, partial } from 'kea-test-utils' -import { InsightShortId, InsightType, ItemMode } from '~/types' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { initKeaTests } from '~/test/init' +import { urls } from 'scenes/urls' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { InsightShortId, InsightType, ItemMode } from '~/types' const Insight12 = '12' as InsightShortId const Insight42 = '42' as InsightShortId @@ -49,7 +50,7 @@ describe('insightSceneLogic', () => { location: partial({ pathname: urls.insightNew(), search: '', hash: '' }), }) - await expect(logic.values.insightLogicRef?.logic.values.filters.insight).toEqual(InsightType.FUNNELS) + expect(logic.values.insightLogicRef?.logic.values.filters.insight).toEqual(InsightType.FUNNELS) }) it('persists edit mode in the url', async () => { diff --git a/frontend/src/scenes/insights/insightSceneLogic.tsx b/frontend/src/scenes/insights/insightSceneLogic.tsx index 3ff71b3e53720..eea9fe3ca0279 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.tsx +++ b/frontend/src/scenes/insights/insightSceneLogic.tsx @@ -1,19 +1,21 @@ import { actions, BuiltLogic, connect, kea, listeners, path, reducers, selectors, sharedListeners } from 'kea' -import { Breadcrumb, FilterType, InsightShortId, InsightType, ItemMode } from '~/types' -import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic' import { actionToUrl, beforeUnload, router, urlToAction } from 'kea-router' -import type { insightSceneLogicType } from './insightSceneLogicType' -import { urls } from 'scenes/urls' -import { insightLogicType } from 'scenes/insights/insightLogicType' -import { createEmptyInsight, insightLogic } from 'scenes/insights/insightLogic' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic' +import { createEmptyInsight, insightLogic } from 'scenes/insights/insightLogic' +import { insightLogicType } from 'scenes/insights/insightLogicType' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { Breadcrumb, FilterType, InsightShortId, InsightType, ItemMode } from '~/types' + import { insightDataLogic } from './insightDataLogic' import { insightDataLogicType } from './insightDataLogicType' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import type { insightSceneLogicType } from './insightSceneLogicType' export const insightSceneLogic = kea([ path(['scenes', 'insights', 'insightSceneLogic']), @@ -85,14 +87,19 @@ export const insightSceneLogic = kea([ insightSelector: [(s) => [s.insightLogicRef], (insightLogicRef) => insightLogicRef?.logic.selectors.insight], insight: [(s) => [(state, props) => s.insightSelector?.(state, props)?.(state, props)], (insight) => insight], breadcrumbs: [ - (s) => [s.insight], - (insight): Breadcrumb[] => [ + (s) => [s.insight, s.insightLogicRef], + (insight, insightLogicRef): Breadcrumb[] => [ { - name: 'Insights', + key: Scene.SavedInsights, + name: 'Product analytics', path: urls.savedInsights(), }, { + key: insight?.short_id || 'new', name: insight?.name || insight?.derived_name || 'Unnamed', + onRename: async (name: string) => { + await insightLogicRef?.logic.asyncActions.setInsightMetadata({ name }) + }, }, ], ], diff --git a/frontend/src/scenes/insights/insightVizDataLogic.test.ts b/frontend/src/scenes/insights/insightVizDataLogic.test.ts index 260b7dcd112f2..065160e2f2c88 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.test.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.test.ts @@ -1,16 +1,16 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { FunnelLayout } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { funnelInvalidExclusionError, funnelResult } from 'scenes/funnels/__mocks__/funnelDataLogicMocks' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { useMocks } from '~/mocks/jest' +import { funnelsQueryDefault, trendsQueryDefault } from '~/queries/nodes/InsightQuery/defaults' +import { ActionsNode, EventsNode, FunnelsQuery, InsightQueryNode, NodeKind, TrendsQuery } from '~/queries/schema' +import { initKeaTests } from '~/test/init' import { BaseMathType, ChartDisplayType, InsightModel, InsightShortId, InsightType } from '~/types' import { insightDataLogic } from './insightDataLogic' -import { useMocks } from '~/mocks/jest' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { trendsQueryDefault, funnelsQueryDefault } from '~/queries/nodes/InsightQuery/defaults' -import { ActionsNode, EventsNode, FunnelsQuery, InsightQueryNode, NodeKind, TrendsQuery } from '~/queries/schema' -import { FunnelLayout } from 'lib/constants' -import { funnelInvalidExclusionError, funnelResult } from 'scenes/funnels/__mocks__/funnelDataLogicMocks' const Insight123 = '123' as InsightShortId diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index c5540a155b360..7a0b2a85f8079 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -1,30 +1,46 @@ -import posthog from 'posthog-js' -import { actions, connect, kea, key, listeners, path, props, selectors, reducers } from 'kea' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { DISPLAY_TYPES_WITHOUT_LEGEND } from 'lib/components/InsightLegend/utils' +import { Intervals, intervals } from 'lib/components/IntervalFilter/intervals' +import { parseProperties } from 'lib/components/PropertyFilters/utils' import { - BaseMathType, - ChartDisplayType, - FilterType, - FunnelExclusion, - InsightLogicProps, - IntervalType, - TrendsFilterType, -} from '~/types' + NON_TIME_SERIES_DISPLAY_TYPES, + NON_VALUES_ON_SERIES_DISPLAY_TYPES, + PERCENT_STACK_VIEW_DISPLAY_TYPE, +} from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { dateMapping } from 'lib/utils' +import posthog from 'posthog-js' +import { insightDataLogic, queryFromKind } from 'scenes/insights/insightDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { sceneLogic } from 'scenes/sceneLogic' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' +import { BASE_MATH_DEFINITIONS } from 'scenes/trends/mathsLogic' + +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { + getBreakdown, + getCompare, + getDisplay, + getFormula, + getInterval, + getSeries, + getShowLabelsOnSeries, + getShowLegend, + getShowPercentStackView, + getShowValueOnSeries, +} from '~/queries/nodes/InsightViz/utils' import { BreakdownFilter, DateRange, FunnelsQuery, InsightFilter, InsightQueryNode, - InsightVizNode, Node, NodeKind, TrendsFilter, TrendsQuery, } from '~/queries/schema' - -import { insightLogic } from './insightLogic' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { filterForQuery, filterKeyForQuery, @@ -39,31 +55,18 @@ import { isTrendsQuery, nodeKindToFilterProperty, } from '~/queries/utils' -import { NON_TIME_SERIES_DISPLAY_TYPES, PERCENT_STACK_VIEW_DISPLAY_TYPE } from 'lib/constants' import { - getBreakdown, - getCompare, - getDisplay, - getFormula, - getInterval, - getSeries, - getShowLegend, - getShowPercentStackView, - getShowValueOnSeries, -} from '~/queries/nodes/InsightViz/utils' -import { DISPLAY_TYPES_WITHOUT_LEGEND } from 'lib/components/InsightLegend/utils' -import { Intervals, intervals } from 'lib/components/IntervalFilter/intervals' -import { insightDataLogic, queryFromKind } from 'scenes/insights/insightDataLogic' - -import { sceneLogic } from 'scenes/sceneLogic' + BaseMathType, + ChartDisplayType, + FilterType, + FunnelExclusion, + InsightLogicProps, + IntervalType, + TrendsFilterType, +} from '~/types' +import { insightLogic } from './insightLogic' import type { insightVizDataLogicType } from './insightVizDataLogicType' -import { parseProperties } from 'lib/components/PropertyFilters/utils' -import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' -import { BASE_MATH_DEFINITIONS } from 'scenes/trends/mathsLogic' -import { lemonToast } from '@posthog/lemon-ui' -import { dayjs } from 'lib/dayjs' -import { dateMapping } from 'lib/utils' const SHOW_TIMEOUT_MESSAGE_AFTER = 5000 @@ -127,13 +130,31 @@ export const insightVizDataLogic = kea([ isLifecycle: [(s) => [s.querySource], (q) => isLifecycleQuery(q)], isTrendsLike: [(s) => [s.querySource], (q) => isTrendsQuery(q) || isLifecycleQuery(q) || isStickinessQuery(q)], supportsDisplay: [(s) => [s.querySource], (q) => isTrendsQuery(q) || isStickinessQuery(q)], - supportsCompare: [(s) => [s.querySource], (q) => isTrendsQuery(q) || isStickinessQuery(q)], + supportsCompare: [ + (s) => [s.querySource, s.display, s.dateRange], + (q, display, dateRange) => + (isTrendsQuery(q) || isStickinessQuery(q)) && + display !== ChartDisplayType.WorldMap && + dateRange?.date_from !== 'all', + ], supportsPercentStackView: [ (s) => [s.querySource, s.display], (q, display) => isTrendsQuery(q) && PERCENT_STACK_VIEW_DISPLAY_TYPE.includes(display || ChartDisplayType.ActionsLineGraph), ], + supportsValueOnSeries: [ + (s) => [s.isTrends, s.isStickiness, s.isLifecycle, s.display], + (isTrends, isStickiness, isLifecycle, display) => { + if (isTrends || isStickiness) { + return !NON_VALUES_ON_SERIES_DISPLAY_TYPES.includes(display || ChartDisplayType.ActionsLineGraph) + } else if (isLifecycle) { + return true + } else { + return false + } + }, + ], dateRange: [(s) => [s.querySource], (q) => (q ? q.dateRange : null)], breakdown: [(s) => [s.querySource], (q) => (q ? getBreakdown(q) : null)], @@ -146,8 +167,9 @@ export const insightVizDataLogic = kea([ samplingFactor: [(s) => [s.querySource], (q) => (q ? q.samplingFactor : null)], showLegend: [(s) => [s.querySource], (q) => (q ? getShowLegend(q) : null)], showValueOnSeries: [(s) => [s.querySource], (q) => (q ? getShowValueOnSeries(q) : null)], + showLabelOnSeries: [(s) => [s.querySource], (q) => (q ? getShowLabelsOnSeries(q) : null)], showPercentStackView: [(s) => [s.querySource], (q) => (q ? getShowPercentStackView(q) : null)], - + vizSpecificOptions: [(s) => [s.query], (q: Node) => (isInsightVizNode(q) ? q.vizSpecificOptions : null)], insightFilter: [(s) => [s.querySource], (q) => (q ? filterForQuery(q) : null)], trendsFilter: [(s) => [s.querySource], (q) => (isTrendsQuery(q) ? q.trendsFilter : null)], funnelsFilter: [(s) => [s.querySource], (q) => (isFunnelsQuery(q) ? q.funnelsFilter : null)], @@ -321,7 +343,7 @@ export const insightVizDataLogic = kea([ setQuery: ({ query }) => { if (isInsightVizNode(query)) { if (props.setQuery) { - props.setQuery(query as InsightVizNode) + props.setQuery(query) } const querySource = query.source diff --git a/frontend/src/scenes/insights/sharedUtils.test.ts b/frontend/src/scenes/insights/sharedUtils.test.ts index 7c3e9a4000660..610d918bd5171 100644 --- a/frontend/src/scenes/insights/sharedUtils.test.ts +++ b/frontend/src/scenes/insights/sharedUtils.test.ts @@ -1,4 +1,5 @@ import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + import { InsightShortId } from '~/types' const Insight123 = '123' as InsightShortId diff --git a/frontend/src/scenes/insights/sharedUtils.ts b/frontend/src/scenes/insights/sharedUtils.ts index 109a77e398aed..7dcc6e3c105c4 100644 --- a/frontend/src/scenes/insights/sharedUtils.ts +++ b/frontend/src/scenes/insights/sharedUtils.ts @@ -1,6 +1,7 @@ // This is separate from utils.ts because here we don't include `funnelLogic`, `retentionLogic`, etc import { + ChartDisplayType, FilterType, FunnelsFilterType, InsightLogicProps, @@ -10,7 +11,6 @@ import { RetentionFilterType, StickinessFilterType, TrendsFilterType, - ChartDisplayType, } from '~/types' /** @@ -45,7 +45,7 @@ export function filterTrendsClientSideParams( return newFilters } -export function isTrendsInsight(insight?: InsightType | InsightType): boolean { +export function isTrendsInsight(insight?: InsightType): boolean { return insight === InsightType.TRENDS || insight === InsightType.LIFECYCLE || insight === InsightType.STICKINESS } diff --git a/frontend/src/scenes/insights/summarizeInsight.test.ts b/frontend/src/scenes/insights/summarizeInsight.test.ts index 994a38cddd821..8bc060941642f 100644 --- a/frontend/src/scenes/insights/summarizeInsight.test.ts +++ b/frontend/src/scenes/insights/summarizeInsight.test.ts @@ -1,17 +1,5 @@ -import { Noun } from '~/models/groupsModel' -import { - BaseMathType, - CohortType, - CountPerActorMathType, - FilterLogicalOperator, - GroupMathType, - InsightType, - PathsFilterType, - PathType, - PropertyMathType, - RetentionFilterType, - TrendsFilterType, -} from '~/types' +import { RETENTION_FIRST_TIME, RETENTION_RECURRING } from 'lib/constants' +import { summarizeInsight, SummaryContext } from 'scenes/insights/summarizeInsight' import { BASE_MATH_DEFINITIONS, COUNT_PER_ACTOR_MATH_DEFINITIONS, @@ -20,7 +8,8 @@ import { MathDefinition, PROPERTY_MATH_DEFINITIONS, } from 'scenes/trends/mathsLogic' -import { RETENTION_FIRST_TIME, RETENTION_RECURRING } from 'lib/constants' + +import { Noun } from '~/models/groupsModel' import { DataTableNode, FunnelsQuery, @@ -33,7 +22,19 @@ import { TimeToSeeDataWaterfallNode, TrendsQuery, } from '~/queries/schema' -import { summarizeInsight, SummaryContext } from 'scenes/insights/summarizeInsight' +import { + BaseMathType, + CohortType, + CountPerActorMathType, + FilterLogicalOperator, + GroupMathType, + InsightType, + PathsFilterType, + PathType, + PropertyMathType, + RetentionFilterType, + TrendsFilterType, +} from '~/types' const aggregationLabel = (groupTypeIndex: number | null | undefined): Noun => groupTypeIndex != undefined diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts index 2ab9196c52862..a0f4aca384486 100644 --- a/frontend/src/scenes/insights/summarizeInsight.ts +++ b/frontend/src/scenes/insights/summarizeInsight.ts @@ -1,6 +1,6 @@ -import { AnyPartialFilterType, EntityFilter, FilterType, FunnelVizType, StepOrderValue } from '~/types' -import { BreakdownFilter, InsightQueryNode, Node, StickinessQuery } from '~/queries/schema' +import { RETENTION_FIRST_TIME } from 'lib/constants' import { KEY_MAPPING } from 'lib/taxonomy' +import { alphabet, capitalizeFirstLetter } from 'lib/utils' import { toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { isFunnelsFilter, @@ -10,10 +10,19 @@ import { isStickinessFilter, isTrendsFilter, } from 'scenes/insights/sharedUtils' +import { + getDisplayNameFromEntityFilter, + getDisplayNameFromEntityNode, + humanizePathsEventTypes, +} from 'scenes/insights/utils' import { retentionOptions } from 'scenes/retention/constants' -import { RETENTION_FIRST_TIME } from 'lib/constants' -import { alphabet, capitalizeFirstLetter } from 'lib/utils' import { apiValueToMathType, MathCategory, MathDefinition } from 'scenes/trends/mathsLogic' +import { mathsLogicType } from 'scenes/trends/mathsLogicType' + +import { cohortsModelType } from '~/models/cohortsModelType' +import { groupsModelType } from '~/models/groupsModelType' +import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' +import { BreakdownFilter, InsightQueryNode, Node } from '~/queries/schema' import { isDataTableNode, isEventsQuery, @@ -29,15 +38,7 @@ import { isTimeToSeeDataSessionsQuery, isTrendsQuery, } from '~/queries/utils' -import { groupsModelType } from '~/models/groupsModelType' -import { cohortsModelType } from '~/models/cohortsModelType' -import { mathsLogicType } from 'scenes/trends/mathsLogicType' -import { - getDisplayNameFromEntityFilter, - getDisplayNameFromEntityNode, - humanizePathsEventTypes, -} from 'scenes/insights/utils' -import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' +import { AnyPartialFilterType, EntityFilter, FilterType, FunnelVizType, StepOrderValue } from '~/types' function summarizeBreakdown(filters: Partial | BreakdownFilter, context: SummaryContext): string | null { const { breakdown_type, breakdown, breakdown_group_type_index } = filters @@ -272,7 +273,7 @@ function summarizeInsightQuery(query: InsightQueryNode, context: SummaryContext) return summary } else if (isStickinessQuery(query)) { return capitalizeFirstLetter( - (query as StickinessQuery).series + query.series .map((s) => { const actor = context.aggregationLabel(s.math_group_type_index, true).singular return `${actor} stickiness based on ${getDisplayNameFromEntityNode(s)}` diff --git a/frontend/src/scenes/insights/utils.test.ts b/frontend/src/scenes/insights/utils.test.ts index 441e69d0e5967..94bad45af4386 100644 --- a/frontend/src/scenes/insights/utils.test.ts +++ b/frontend/src/scenes/insights/utils.test.ts @@ -1,4 +1,4 @@ -import { Entity, EntityFilter, FilterType, InsightType } from '~/types' +import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' import { extractObjectDiffKeys, formatAggregationValue, @@ -7,9 +7,10 @@ import { getDisplayNameFromEntityFilter, getDisplayNameFromEntityNode, } from 'scenes/insights/utils' -import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' + import { ActionsNode, BreakdownFilter, EventsNode, NodeKind } from '~/queries/schema' import { isEventsNode } from '~/queries/utils' +import { Entity, EntityFilter, FilterType, InsightType } from '~/types' const createFilter = (id?: Entity['id'], name?: string, custom_name?: string): EntityFilter => { return { diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index 272b2bf031ba4..35e2b04b251f8 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -1,32 +1,37 @@ +import api from 'lib/api' +import { dayjs } from 'lib/dayjs' +import { KEY_MAPPING } from 'lib/taxonomy' +import { ensureStringIsNotBlank, humanFriendlyNumber, objectsEqual } from 'lib/utils' +import { getCurrentTeamId } from 'lib/utils/logics' +import { ReactNode } from 'react' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { urls } from 'scenes/urls' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { FormatPropertyValueForDisplayFunction } from '~/models/propertyDefinitionsModel' +import { examples } from '~/queries/examples' +import { ActionsNode, BreakdownFilter, EventsNode } from '~/queries/schema' +import { isEventsNode } from '~/queries/utils' import { ActionFilter, AnyPartialFilterType, BreakdownKeyType, BreakdownType, + ChartDisplayType, CohortType, EntityFilter, EntityTypes, + EventType, InsightModel, InsightShortId, InsightType, PathsFilterType, PathType, + TrendsFilterType, } from '~/types' -import { ensureStringIsNotBlank, humanFriendlyNumber, objectsEqual } from 'lib/utils' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { KEY_MAPPING } from 'lib/taxonomy' -import api from 'lib/api' -import { dayjs } from 'lib/dayjs' -import { getCurrentTeamId } from 'lib/utils/logics' -import { dashboardsModel } from '~/models/dashboardsModel' + import { insightLogic } from './insightLogic' -import { FormatPropertyValueForDisplayFunction } from '~/models/propertyDefinitionsModel' -import { ReactNode } from 'react' -import { ActionsNode, BreakdownFilter, EventsNode } from '~/queries/schema' -import { isEventsNode } from '~/queries/utils' -import { urls } from 'scenes/urls' -import { examples } from '~/queries/examples' export const isAllEventsEntityFilter = (filter: EntityFilter | ActionFilter | null): boolean => { return ( @@ -296,3 +301,48 @@ export function concatWithPunctuation(phrases: string[]): string { return `${phrases.slice(0, phrases.length - 1).join(', ')}, and ${phrases[phrases.length - 1]}` } } + +export function insightUrlForEvent(event: Pick): string | undefined { + let insightParams: Partial | undefined + if (event.event === '$pageview') { + insightParams = { + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [], + events: [ + { + id: '$pageview', + name: '$pageview', + type: 'events', + order: 0, + properties: [ + { + key: '$current_url', + value: event.properties.$current_url, + type: 'event', + }, + ], + }, + ], + } + } else if (event.event !== '$autocapture') { + insightParams = { + insight: InsightType.TRENDS, + interval: 'day', + display: ChartDisplayType.ActionsLineGraph, + actions: [], + events: [ + { + id: event.event, + name: event.event, + type: 'events', + order: 0, + properties: [], + }, + ], + } + } + + return insightParams ? urls.insightNew(insightParams) : undefined +} diff --git a/frontend/src/scenes/insights/utils/cleanFilters.test.ts b/frontend/src/scenes/insights/utils/cleanFilters.test.ts index f3cb5e2495157..d49c8d03ff1cf 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.test.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.test.ts @@ -1,4 +1,5 @@ -import { cleanFilters } from './cleanFilters' +import { NON_VALUES_ON_SERIES_DISPLAY_TYPES, ShownAsValue } from 'lib/constants' + import { ChartDisplayType, FilterType, @@ -8,7 +9,8 @@ import { InsightType, TrendsFilterType, } from '~/types' -import { NON_VALUES_ON_SERIES_DISPLAY_TYPES, ShownAsValue } from 'lib/constants' + +import { cleanFilters } from './cleanFilters' describe('cleanFilters', () => { it('removes shownas from trends insights', () => { diff --git a/frontend/src/scenes/insights/utils/cleanFilters.ts b/frontend/src/scenes/insights/utils/cleanFilters.ts index bd16e3ca1ba79..2d5a7c12582a5 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.ts @@ -1,3 +1,25 @@ +import { smoothingOptions } from 'lib/components/SmoothingFilter/smoothings' +import { + BIN_COUNT_AUTO, + NON_TIME_SERIES_DISPLAY_TYPES, + NON_VALUES_ON_SERIES_DISPLAY_TYPES, + PERCENT_STACK_VIEW_DISPLAY_TYPE, + RETENTION_FIRST_TIME, + ShownAsValue, +} from 'lib/constants' +import { getDefaultEventName } from 'lib/utils/getAppContext' +import { deepCleanFunnelExclusionEvents, getClampedStepRangeFilter, isStepsUndefined } from 'scenes/funnels/funnelUtils' +import { isURLNormalizeable } from 'scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterUtils' +import { + isFunnelsFilter, + isLifecycleFilter, + isPathsFilter, + isRetentionFilter, + isStickinessFilter, + isTrendsFilter, +} from 'scenes/insights/sharedUtils' +import { DEFAULT_STEP_LIMIT } from 'scenes/paths/pathsDataLogic' + import { AnyFilterType, ChartDisplayType, @@ -7,6 +29,7 @@ import { FunnelsFilterType, FunnelVizType, InsightType, + IntervalType, LifecycleFilterType, PathsFilterType, PathType, @@ -15,28 +38,8 @@ import { StickinessFilterType, TrendsFilterType, } from '~/types' -import { deepCleanFunnelExclusionEvents, getClampedStepRangeFilter, isStepsUndefined } from 'scenes/funnels/funnelUtils' -import { getDefaultEventName } from 'lib/utils/getAppContext' -import { - BIN_COUNT_AUTO, - NON_VALUES_ON_SERIES_DISPLAY_TYPES, - PERCENT_STACK_VIEW_DISPLAY_TYPE, - RETENTION_FIRST_TIME, - ShownAsValue, -} from 'lib/constants' -import { autocorrectInterval } from 'lib/utils' -import { DEFAULT_STEP_LIMIT } from 'scenes/paths/pathsDataLogic' -import { smoothingOptions } from 'lib/components/SmoothingFilter/smoothings' + import { LocalFilter, toLocalFilters } from '../filters/ActionFilter/entityFilterLogic' -import { - isFunnelsFilter, - isLifecycleFilter, - isPathsFilter, - isRetentionFilter, - isStickinessFilter, - isTrendsFilter, -} from 'scenes/insights/sharedUtils' -import { isURLNormalizeable } from 'scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterUtils' export function getDefaultEvent(): Entity { const event = getDefaultEventName() @@ -165,6 +168,46 @@ export const setTestAccountFilterForNewInsight = ( } } +const disableHourFor: Record = { + dStart: false, + '-1d': false, + '-7d': false, + '-14d': false, + '-30d': false, + '-90d': true, + mStart: false, + '-1mStart': false, + yStart: true, + all: true, + other: false, +} + +export function autocorrectInterval(filters: Partial): IntervalType | undefined { + if ('display' in filters && filters.display && NON_TIME_SERIES_DISPLAY_TYPES.includes(filters.display)) { + // Non-time-series insights should not have an interval + return undefined + } + if (isFunnelsFilter(filters) && filters.funnel_viz_type !== FunnelVizType.Trends) { + // Only trend funnels support intervals + return undefined + } + if (!filters.interval) { + return 'day' + } + + // @ts-expect-error - Old legacy interval support + const minute_disabled = filters.interval === 'minute' + const hour_disabled = disableHourFor[filters.date_from || 'other'] && filters.interval === 'hour' + + if (minute_disabled) { + return 'hour' + } else if (hour_disabled) { + return 'day' + } else { + return filters.interval + } +} + export function cleanFilters( filters: Partial, test_account_filters_default_checked?: boolean diff --git a/frontend/src/scenes/insights/utils/compareFilters.ts b/frontend/src/scenes/insights/utils/compareFilters.ts index e2ccf3c14ba97..93afde2296b6b 100644 --- a/frontend/src/scenes/insights/utils/compareFilters.ts +++ b/frontend/src/scenes/insights/utils/compareFilters.ts @@ -1,6 +1,7 @@ -import { AnyFilterType } from '~/types' import { objectCleanWithEmpty, objectsEqual } from 'lib/utils' +import { AnyFilterType } from '~/types' + import { cleanFilters } from './cleanFilters' /** clean filters so that we can check for semantic equality with a deep equality check */ diff --git a/frontend/src/scenes/insights/utils/compareInsightQuery.ts b/frontend/src/scenes/insights/utils/compareInsightQuery.ts index 95039cabecf93..ab3ca5962722c 100644 --- a/frontend/src/scenes/insights/utils/compareInsightQuery.ts +++ b/frontend/src/scenes/insights/utils/compareInsightQuery.ts @@ -1,6 +1,6 @@ -import { InsightQueryNode } from '~/queries/schema' - import { objectCleanWithEmpty, objectsEqual } from 'lib/utils' + +import { InsightQueryNode } from '~/queries/schema' import { filterForQuery, filterKeyForQuery, diff --git a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.scss b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.scss index d16bb9a54d8c6..510dc8b1bceb4 100644 --- a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.scss +++ b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.scss @@ -2,36 +2,30 @@ .BoldNumber { width: 100%; - padding: 2rem 0 3rem; - text-align: center; - - @include screen($md) { - padding: 3rem 0 5rem; - .InsightCard & { - padding: 2rem 0; - } - } -} - -.BoldNumber__value { - width: 100%; - text-align: center; - padding: 0 2rem; - margin: 0 auto; - line-height: 1; - font-weight: 700; - letter-spacing: -0.025em; + padding: 2rem 3rem 3rem; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + flex: 1; .InsightCard & { - padding: 0 1rem; + padding: 1rem; } @include screen($md) { - padding: 0 5rem; + padding: 3rem 5rem 5rem; + .InsightCard & { - padding: 0 2rem; + padding: 2rem; } } + + .BoldNumber__value { + font-weight: 700; + width: 100%; + letter-spacing: -0.025em; + } } .BoldNumber__comparison { diff --git a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx index c3f0156684166..f4b79b2a4c8b7 100644 --- a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx +++ b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx @@ -1,25 +1,27 @@ -import { useValues } from 'kea' -import { useLayoutEffect, useRef, useState } from 'react' -import clsx from 'clsx' - -import { insightLogic } from '../../insightLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import './BoldNumber.scss' +import './BoldNumber.scss' -import { ChartParams, TrendResult } from '~/types' -import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' -import { ensureTooltip } from '../LineGraph/LineGraph' -import { groupsModel } from '~/models/groupsModel' -import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' -import { IconFlare, IconTrendingDown, IconTrendingFlat, IconTrendingUp } from 'lib/lemon-ui/icons' import { LemonRow, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useValues } from 'kea' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { IconFlare, IconTrendingDown, IconTrendingFlat, IconTrendingUp } from 'lib/lemon-ui/icons' import { percentage } from 'lib/utils' +import { useLayoutEffect, useRef, useState } from 'react' +import { useEffect } from 'react' +import React from 'react' +import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' import { InsightEmptyState } from 'scenes/insights/EmptyStates' +import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import './BoldNumber.scss' -import { useEffect } from 'react' -import Textfit from './Textfit' +import { groupsModel } from '~/models/groupsModel' +import { ChartParams, TrendResult } from '~/types' + +import { insightLogic } from '../../insightLogic' +import { ensureTooltip } from '../LineGraph/LineGraph' +import { Textfit } from './Textfit' /** The tooltip is offset by a few pixels from the cursor to give it some breathing room. */ const BOLD_NUMBER_TOOLTIP_OFFSET_PX = 8 @@ -94,29 +96,29 @@ export function BoldNumber({ showPersonsModal = true }: ChartParams): JSX.Elemen return resultSeries ? (
    - -
    { - if (resultSeries.persons?.url) { - openPersonsModal({ - url: resultSeries.persons?.url, - title: , - }) - } +
    { + if (resultSeries.persons?.url) { + openPersonsModal({ + url: resultSeries.persons?.url, + title: , + }) } - : undefined - } - onMouseLeave={() => setIsTooltipShown(false)} - ref={valueRef} - onMouseEnter={() => setIsTooltipShown(true)} - > + } + : undefined + } + onMouseLeave={() => setIsTooltipShown(false)} + ref={valueRef} + onMouseEnter={() => setIsTooltipShown(true)} + > + {formatAggregationAxisValue(trendsFilter, resultSeries.aggregated_value)} -
    - + +
    {showComparison && }
    ) : ( diff --git a/frontend/src/scenes/insights/views/BoldNumber/Textfit.stories.tsx b/frontend/src/scenes/insights/views/BoldNumber/Textfit.stories.tsx new file mode 100644 index 0000000000000..69d3a534095dd --- /dev/null +++ b/frontend/src/scenes/insights/views/BoldNumber/Textfit.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react' + +import { Textfit } from './Textfit' + +type Story = StoryObj +const meta: Meta = { + title: 'Lemon UI/TextFit', + component: Textfit, + tags: ['autodocs'], + args: { + min: 20, + max: 150, + children: '10000000', + }, +} +export default meta + +const Template: StoryFn = (props) => { + return ( +
    + +
    + ) +} + +export const Basic: Story = Template.bind({}) diff --git a/frontend/src/scenes/insights/views/BoldNumber/Textfit.tsx b/frontend/src/scenes/insights/views/BoldNumber/Textfit.tsx index 8d05cea3b4e59..b76f20f920ea3 100644 --- a/frontend/src/scenes/insights/views/BoldNumber/Textfit.tsx +++ b/frontend/src/scenes/insights/views/BoldNumber/Textfit.tsx @@ -1,83 +1,77 @@ -// Adapted from https://github.com/malte-wessel/react-textfit -// which is no longer maintained and does not support React 18 +import { useRef } from 'react' +import useResizeObserver from 'use-resize-observer' -import { useEffect, useRef, useState } from 'react' - -// Calculate width without padding. -const innerWidth = (el: HTMLDivElement): number => { - const style = window.getComputedStyle(el, null) - // Hidden iframe in Firefox returns null, https://github.com/malte-wessel/react-textfit/pull/34 - if (!style) { - return el.clientWidth - } - - return ( - el.clientWidth - - parseInt(style.getPropertyValue('padding-left'), 10) - - parseInt(style.getPropertyValue('padding-right'), 10) - ) +export type TextfitProps = { + min: number + max: number + children: string } -const assertElementFitsWidth = (el: HTMLDivElement, width: number): boolean => el.scrollWidth - 1 <= width - -const Textfit = ({ min, max, children }: { min: number; max: number; children: React.ReactNode }): JSX.Element => { +export const Textfit = ({ min, max, children }: TextfitProps): JSX.Element => { const parentRef = useRef(null) const childRef = useRef(null) - - const [fontSize, setFontSize] = useState() + const fontSizeRef = useRef(min) let resizeTimer: NodeJS.Timeout - const handleWindowResize = (): void => { + const updateFontSize = (size: number): void => { + fontSizeRef.current = size + childRef.current!.style.fontSize = `${size}px` + } + + const handleResize = (): void => { clearTimeout(resizeTimer) resizeTimer = setTimeout(() => { - const el = parentRef.current - const wrapper = childRef.current + const parent = parentRef.current + const child = childRef.current + + if (!parent || !child) { + return + } - if (el && wrapper) { - const originalWidth = innerWidth(el) + let mid + let low = min + let high = max - let mid - let low = min - let high = max + while (low <= high) { + mid = Math.floor((low + high) / 2) + updateFontSize(mid) + const childRect = child.getBoundingClientRect() + const parentRect = parent.getBoundingClientRect() - while (low <= high) { - mid = Math.floor((low + high) / 2) - setFontSize(mid) + const childFitsParent = childRect.width <= parentRect.width && childRect.height <= parentRect.height - if (assertElementFitsWidth(wrapper, originalWidth)) { - low = mid + 1 - } else { - high = mid - 1 - } + if (childFitsParent) { + low = mid + 1 + } else { + high = mid - 1 } - mid = Math.min(low, high) + } + mid = Math.min(low, high) - // Ensure we hit the user-supplied limits - mid = Math.max(mid, min) - mid = Math.min(mid, max) + // Ensure we hit the user-supplied limits + mid = Math.max(mid, min) + mid = Math.min(mid, max) - setFontSize(mid) - } - }, 10) + updateFontSize(mid) + }, 50) } - useEffect(() => { - window.addEventListener('resize', handleWindowResize) - return () => window.removeEventListener('resize', handleWindowResize) - }, []) - - useEffect(() => handleWindowResize(), [parentRef, childRef]) + useResizeObserver({ + ref: parentRef, + onResize: () => handleResize(), + }) return ( - // eslint-disable-next-line react/forbid-dom-props -
    - {/* eslint-disable-next-line react/forbid-dom-props */} -
    +
    +
    {children}
    ) } - -export default Textfit diff --git a/frontend/src/scenes/insights/views/Funnels/CorrelationActionsCell.tsx b/frontend/src/scenes/insights/views/Funnels/CorrelationActionsCell.tsx index bd4b4dcaba5ba..f346115de4a0e 100644 --- a/frontend/src/scenes/insights/views/Funnels/CorrelationActionsCell.tsx +++ b/frontend/src/scenes/insights/views/Funnels/CorrelationActionsCell.tsx @@ -1,15 +1,14 @@ -import { useState } from 'react' import { useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelCorrelationLogic } from 'scenes/funnels/funnelCorrelationLogic' +import { IconEllipsis } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { useState } from 'react' import { funnelCorrelationDetailsLogic } from 'scenes/funnels/funnelCorrelationDetailsLogic' +import { funnelCorrelationLogic } from 'scenes/funnels/funnelCorrelationLogic' import { funnelPropertyCorrelationLogic } from 'scenes/funnels/funnelPropertyCorrelationLogic' +import { insightLogic } from 'scenes/insights/insightLogic' import { FunnelCorrelation, FunnelCorrelationResultsType } from '~/types' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' -import { IconEllipsis } from 'lib/lemon-ui/icons' export const EventCorrelationActionsCell = ({ record }: { record: FunnelCorrelation }): JSX.Element => { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.scss b/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.scss index e6a802681fce9..a315676b55406 100644 --- a/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.scss +++ b/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.scss @@ -3,12 +3,13 @@ display: flex; justify-content: center; } + .correlation-table-wrapper { table { border-radius: var(--radius); border: 1px solid var(--border); border-collapse: separate; - border-spacing: 0px; + border-spacing: 0; margin: 0 auto; td { @@ -21,6 +22,7 @@ font-weight: bold; padding-bottom: 0.25rem; } + &:first-child { border-left: none; } @@ -28,6 +30,7 @@ .table-title { color: var(--muted-alt); + td { border-top: none; } diff --git a/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.tsx b/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.tsx index 4506930a29828..9e8a922f250f3 100644 --- a/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.tsx +++ b/frontend/src/scenes/insights/views/Funnels/CorrelationMatrix.tsx @@ -1,13 +1,9 @@ -import { Button, Modal } from 'antd' import './CorrelationMatrix.scss' + +import { Button, Modal } from 'antd' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { insightLogic } from 'scenes/insights/insightLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { capitalizeFirstLetter, percentage, pluralize } from 'lib/utils' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { Link } from 'lib/lemon-ui/Link' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' import { IconCancel, IconCheckCircleOutline, @@ -16,11 +12,17 @@ import { IconTrendingFlatDown, } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import clsx from 'clsx' -import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' -import { funnelCorrelationLogic } from 'scenes/funnels/funnelCorrelationLogic' +import { Link } from 'lib/lemon-ui/Link' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter, percentage, pluralize } from 'lib/utils' import { funnelCorrelationDetailsLogic } from 'scenes/funnels/funnelCorrelationDetailsLogic' +import { funnelCorrelationLogic } from 'scenes/funnels/funnelCorrelationLogic' import { funnelPersonsModalLogic } from 'scenes/funnels/funnelPersonsModalLogic' +import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' export function CorrelationMatrix(): JSX.Element { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx index a3971c891a7de..b10dd04e1e45a 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx @@ -1,12 +1,13 @@ -import { useActions, useValues } from 'kea' -import { BIN_COUNT_AUTO } from 'lib/constants' import { InputNumber, Select } from 'antd' -import { BinCountValue } from '~/types' import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { BIN_COUNT_AUTO } from 'lib/constants' +import { IconBarChart } from 'lib/lemon-ui/icons' import { ANTD_TOOLTIP_PLACEMENTS } from 'lib/utils' -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { IconBarChart } from 'lib/lemon-ui/icons' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { BinCountValue } from '~/types' // Constraints as defined in funnel_time_to_convert.py:34 const MIN = 1 diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx index 59deda6801480..e938e03c69eb6 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx @@ -1,13 +1,14 @@ -import { capitalizeFirstLetter, pluralize } from 'lib/utils' -import { useState } from 'react' -import { EditorFilterProps, FunnelConversionWindow, FunnelConversionWindowTimeUnit } from '~/types' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { useDebouncedCallback } from 'use-debounce' +import { IconInfo } from '@posthog/icons' import { LemonInput, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter, pluralize } from 'lib/utils' +import { useState } from 'react' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { useDebouncedCallback } from 'use-debounce' + import { FunnelsFilter } from '~/queries/schema' -import { IconInfo } from '@posthog/icons' +import { EditorFilterProps, FunnelConversionWindow, FunnelConversionWindowTimeUnit } from '~/types' const TIME_INTERVAL_BOUNDS: Record = { [FunnelConversionWindowTimeUnit.Second]: [1, 3600], diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.tsx index 55718cd3ffc33..1933f49b96a03 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelation.tsx @@ -1,17 +1,17 @@ -import { useMountedLogic, useValues } from 'kea' +import './FunnelCorrelation.scss' -import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { useMountedLogic, useValues } from 'kea' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { funnelCorrelationUsageLogic } from 'scenes/funnels/funnelCorrelationUsageLogic' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { AvailableFeature } from '~/types' + +import { FunnelCorrelationFeedbackForm } from './FunnelCorrelationFeedbackForm' import { FunnelCorrelationSkewWarning } from './FunnelCorrelationSkewWarning' import { FunnelCorrelationTable } from './FunnelCorrelationTable' -import { FunnelCorrelationFeedbackForm } from './FunnelCorrelationFeedbackForm' import { FunnelPropertyCorrelationTable } from './FunnelPropertyCorrelationTable' -import { AvailableFeature } from '~/types' - -import './FunnelCorrelation.scss' export const FunnelCorrelation = (): JSX.Element | null => { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx index d2838054e3e80..7399a0e979fc3 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx @@ -1,11 +1,9 @@ -import { useRef } from 'react' -import { useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelCorrelationFeedbackLogic } from 'scenes/funnels/funnelCorrelationFeedbackLogic' - import { LemonButton, LemonTextArea } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { IconClose, IconComment } from 'lib/lemon-ui/icons' +import { useRef } from 'react' +import { funnelCorrelationFeedbackLogic } from 'scenes/funnels/funnelCorrelationFeedbackLogic' +import { insightLogic } from 'scenes/insights/insightLogic' export const FunnelCorrelationFeedbackForm = (): JSX.Element | null => { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx index 587c33aefa508..6e4cc1416ffe0 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx @@ -1,12 +1,10 @@ -import { useActions, useValues } from 'kea' -import { Card } from 'antd' - -import { insightLogic } from 'scenes/insights/insightLogic' - -import { IconFeedback } from 'lib/lemon-ui/icons' // eslint-disable-next-line no-restricted-imports import { CloseOutlined } from '@ant-design/icons' +import { Card } from 'antd' +import { useActions, useValues } from 'kea' +import { IconFeedback } from 'lib/lemon-ui/icons' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' export const FunnelCorrelationSkewWarning = (): JSX.Element | null => { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.scss b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.scss index e7e5a26f6c823..41934f4a6aa43 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.scss +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.scss @@ -16,8 +16,7 @@ align-items: center; align-self: stretch; padding: 0.25rem 0.5rem; - justify-content: space-between; - align-content: space-between; + place-content: space-between space-between; border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); @@ -45,6 +44,7 @@ font-weight: 600; font-size: 11px; line-height: 16px; + /* identical to box height, or 145% */ display: flex; @@ -52,7 +52,6 @@ letter-spacing: 0.02em; text-transform: uppercase; margin: 5px; - color: var(--muted); } @@ -60,6 +59,7 @@ border-radius: var(--radius) 0 0 var(--radius); border-right: none; } + .LemonCheckbox:last-child label { border-radius: 0 var(--radius) var(--radius) 0; } @@ -76,6 +76,7 @@ margin: 1rem; border: 1px solid var(--border); border-radius: var(--radius); + thead th { border-bottom: 1px solid var(--border); } diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx index 1c3a022249323..0501b62fe4b70 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx @@ -1,29 +1,31 @@ -import { useEffect } from 'react' -import { ConfigProvider, Table, Empty } from 'antd' +import './FunnelCorrelationTable.scss' + +import { IconInfo } from '@posthog/icons' +import { LemonCheckbox } from '@posthog/lemon-ui' +import { ConfigProvider, Empty, Table } from 'antd' import Column from 'antd/lib/table/Column' import { useActions, useValues } from 'kea' -import { IconSelectEvents, IconTrendUp, IconTrendingDown, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import './FunnelCorrelationTable.scss' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { VisibilitySensor } from 'lib/components/VisibilitySensor/VisibilitySensor' +import { IconSelectEvents, IconTrendingDown, IconTrendUp, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { CorrelationMatrix } from './CorrelationMatrix' -import { capitalizeFirstLetter } from 'lib/utils' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { EventCorrelationActionsCell } from './CorrelationActionsCell' import { Link } from 'lib/lemon-ui/Link' -import { funnelCorrelationUsageLogic } from 'scenes/funnels/funnelCorrelationUsageLogic' - +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter } from 'lib/utils' +import { useEffect } from 'react' import { funnelCorrelationLogic } from 'scenes/funnels/funnelCorrelationLogic' +import { funnelCorrelationUsageLogic } from 'scenes/funnels/funnelCorrelationUsageLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' -import { LemonCheckbox } from '@posthog/lemon-ui' import { funnelPersonsModalLogic } from 'scenes/funnels/funnelPersonsModalLogic' -import { IconInfo } from '@posthog/icons' +import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' +import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' + +import { EventCorrelationActionsCell } from './CorrelationActionsCell' +import { CorrelationMatrix } from './CorrelationMatrix' export function FunnelCorrelationTable(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelDisplayLayoutPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelDisplayLayoutPicker.tsx index 3b4c95d14d886..1a21527301fad 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelDisplayLayoutPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelDisplayLayoutPicker.tsx @@ -1,9 +1,9 @@ +import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { FunnelLayout } from 'lib/constants' -import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonSelect } from '@posthog/lemon-ui' import { IconFunnelHorizontal, IconFunnelVertical } from 'lib/lemon-ui/icons' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' export function FunnelDisplayLayoutPicker(): JSX.Element { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelInsight.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelInsight.tsx deleted file mode 100644 index 6e44fe2dee2b6..0000000000000 --- a/frontend/src/scenes/insights/views/Funnels/FunnelInsight.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import clsx from 'clsx' -import { useValues } from 'kea' -import { Funnel } from 'scenes/funnels/Funnel' -import { insightLogic } from 'scenes/insights/insightLogic' -import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' - -export function FunnelInsight(): JSX.Element { - const { insightLoading, insightProps } = useValues(insightLogic) - const { isFunnelWithEnoughSteps } = useValues(insightVizDataLogic(insightProps)) - const { hasFunnelResults } = useValues(funnelDataLogic(insightProps)) - - const nonEmptyState = (hasFunnelResults && isFunnelWithEnoughSteps) || insightLoading - - return ( -
    - -
    - ) -} diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx index 3325b103ba438..fbede9951a858 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx @@ -1,28 +1,31 @@ -import { useEffect } from 'react' -import { Link } from 'lib/lemon-ui/Link' -import { Col, ConfigProvider, Row, Table, Empty } from 'antd' +import './FunnelCorrelationTable.scss' + +import { IconInfo } from '@posthog/icons' +import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +import { Col, ConfigProvider, Empty, Row, Table } from 'antd' import Column from 'antd/lib/table/Column' import { useActions, useValues } from 'kea' -import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' +import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { IconSelectProperties, IconTrendingDown, IconTrendingUp } from 'lib/lemon-ui/icons' -import './FunnelCorrelationTable.scss' import { VisibilitySensor } from 'lib/components/VisibilitySensor/VisibilitySensor' +import { IconSelectProperties, IconTrendingDown, IconTrendingUp } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { Popover } from 'lib/lemon-ui/Popover' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { capitalizeFirstLetter } from 'lib/utils' -import { PropertyCorrelationActionsCell } from './CorrelationActionsCell' +import { useEffect } from 'react' +import { useState } from 'react' import { funnelCorrelationUsageLogic } from 'scenes/funnels/funnelCorrelationUsageLogic' -import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' -import { funnelPropertyCorrelationLogic } from 'scenes/funnels/funnelPropertyCorrelationLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { Popover } from 'lib/lemon-ui/Popover' -import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' -import { useState } from 'react' -import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' import { funnelPersonsModalLogic } from 'scenes/funnels/funnelPersonsModalLogic' -import { IconInfo } from '@posthog/icons' +import { funnelPropertyCorrelationLogic } from 'scenes/funnels/funnelPropertyCorrelationLogic' +import { parseDisplayNameForCorrelation } from 'scenes/funnels/funnelUtils' +import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { FunnelCorrelation, FunnelCorrelationResultsType, FunnelCorrelationType } from '~/types' + +import { PropertyCorrelationActionsCell } from './CorrelationActionsCell' export function FunnelPropertyCorrelationTable(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx index 54bc248662a91..615fc8bfa1a77 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx @@ -1,11 +1,10 @@ +import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' - -import { insightLogic } from 'scenes/insights/insightLogic' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' -import { StepOrderValue } from '~/types' -import { LemonSelect } from '@posthog/lemon-ui' import { FunnelsFilter } from '~/queries/schema' +import { StepOrderValue } from '~/types' interface StepOption { key?: string diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx index ba9f0b36b07a4..c875b1cec5355 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx @@ -1,11 +1,11 @@ +import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' - import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonSelect, LemonSelectOptions, LemonSelectOption } from '@posthog/lemon-ui' -import { seriesNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { seriesNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' + export function FunnelStepsPicker(): JSX.Element | null { const { insightProps } = useValues(insightLogic) const { series, isFunnelWithEnoughSteps, funnelsFilter } = useValues(insightVizDataLogic(insightProps)) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelStepsTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelStepsTable.tsx index 0d2c3471ba6ee..ab8bae047fc37 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelStepsTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelStepsTable.tsx @@ -1,23 +1,25 @@ import { useActions, useValues } from 'kea' -import { insightLogic } from 'scenes/insights/insightLogic' -import { LemonTable, LemonTableColumn, LemonTableColumnGroup } from 'lib/lemon-ui/LemonTable' -import { FlattenedFunnelStepByBreakdown } from '~/types' +import { getSeriesColor } from 'lib/colors' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { getVisibilityKey } from 'scenes/funnels/funnelUtils' -import { getActionFilterFromFunnelStep, getSignificanceFromBreakdownStep } from './funnelStepTableUtils' -import { cohortsModel } from '~/models/cohortsModel' +import { IconFlag } from 'lib/lemon-ui/icons' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' import { LemonRow } from 'lib/lemon-ui/LemonRow' +import { LemonTable, LemonTableColumn, LemonTableColumnGroup } from 'lib/lemon-ui/LemonTable' +import { Lettermark, LettermarkColor } from 'lib/lemon-ui/Lettermark' import { humanFriendlyDuration, humanFriendlyNumber, percentage } from 'lib/utils' -import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' -import { getSeriesColor } from 'lib/colors' -import { IconFlag } from 'lib/lemon-ui/icons' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatBreakdownLabel } from 'scenes/insights/utils' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { funnelPersonsModalLogic } from 'scenes/funnels/funnelPersonsModalLogic' +import { getVisibilityKey } from 'scenes/funnels/funnelUtils' +import { ValueInspectorButton } from 'scenes/funnels/ValueInspectorButton' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { formatBreakdownLabel } from 'scenes/insights/utils' + +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { FlattenedFunnelStepByBreakdown } from '~/types' + +import { getActionFilterFromFunnelStep, getSignificanceFromBreakdownStep } from './funnelStepTableUtils' export function FunnelStepsTable(): JSX.Element | null { const { insightProps, insightLoading } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx index 3f613df7d8a10..9850c935468c6 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx @@ -1,12 +1,10 @@ +import { IconClock, IconFilter, IconTrending } from '@posthog/icons' import { useActions, useValues } from 'kea' - +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' -import { FunnelVizType as VizType, EditorFilterProps } from '~/types' import { FunnelsFilter } from '~/queries/schema' -import { IconFilter, IconClock, IconTrending } from '@posthog/icons' - -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { EditorFilterProps, FunnelVizType as VizType } from '~/types' type LabelProps = { icon: JSX.Element diff --git a/frontend/src/scenes/insights/views/Histogram/Histogram.scss b/frontend/src/scenes/insights/views/Histogram/Histogram.scss index 27bac7fe2ab0d..2742347145d09 100644 --- a/frontend/src/scenes/insights/views/Histogram/Histogram.scss +++ b/frontend/src/scenes/insights/views/Histogram/Histogram.scss @@ -61,7 +61,7 @@ * Bars */ g#bars { - fill: var(--funnel-default); + fill: var(--primary-3000); } g#labels { @@ -69,9 +69,9 @@ // same as chart-js font-size: 12px; font-weight: normal; - font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; - + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; fill: var(--white); + &.outside { fill: #0f0f0f; } diff --git a/frontend/src/scenes/insights/views/Histogram/Histogram.tsx b/frontend/src/scenes/insights/views/Histogram/Histogram.tsx index 11671e289ecae..52e807a65deb2 100644 --- a/frontend/src/scenes/insights/views/Histogram/Histogram.tsx +++ b/frontend/src/scenes/insights/views/Histogram/Histogram.tsx @@ -1,14 +1,15 @@ -import { useEffect } from 'react' +import './Histogram.scss' + import * as d3 from 'd3' -import { D3Selector, D3Transition, useD3 } from 'lib/hooks/useD3' +import { useActions, useValues } from 'kea' import { FunnelLayout } from 'lib/constants' -import { createRoundedRectPath, D3HistogramDatum, getConfig, INITIAL_CONFIG } from './histogramUtils' +import { D3Selector, D3Transition, useD3 } from 'lib/hooks/useD3' import { animate, getOrCreateEl, wrap } from 'lib/utils/d3Utils' - -import './Histogram.scss' -import { useActions, useValues } from 'kea' +import { useEffect } from 'react' import { histogramLogic } from 'scenes/insights/views/Histogram/histogramLogic' +import { createRoundedRectPath, D3HistogramDatum, getConfig, INITIAL_CONFIG } from './histogramUtils' + export interface HistogramDatum { id: string | number bin0: number diff --git a/frontend/src/scenes/insights/views/Histogram/histogramLogic.test.ts b/frontend/src/scenes/insights/views/Histogram/histogramLogic.test.ts index 5dee7f5acfce8..254d7ad4e5ddc 100644 --- a/frontend/src/scenes/insights/views/Histogram/histogramLogic.test.ts +++ b/frontend/src/scenes/insights/views/Histogram/histogramLogic.test.ts @@ -1,7 +1,9 @@ -import { histogramLogic } from './histogramLogic' -import { initKeaTests } from '~/test/init' -import { getConfig } from 'scenes/insights/views/Histogram/histogramUtils' import { FunnelLayout } from 'lib/constants' +import { getConfig } from 'scenes/insights/views/Histogram/histogramUtils' + +import { initKeaTests } from '~/test/init' + +import { histogramLogic } from './histogramLogic' describe('histogramLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/insights/views/Histogram/histogramLogic.ts b/frontend/src/scenes/insights/views/Histogram/histogramLogic.ts index a3bcd68aeaca9..2993434027fec 100644 --- a/frontend/src/scenes/insights/views/Histogram/histogramLogic.ts +++ b/frontend/src/scenes/insights/views/Histogram/histogramLogic.ts @@ -1,7 +1,8 @@ -import { kea, path, actions, reducers } from 'kea' +import { actions, kea, path, reducers } from 'kea' +import { FunnelLayout } from 'lib/constants' import { getConfig, HistogramConfig } from 'scenes/insights/views/Histogram/histogramUtils' + import type { histogramLogicType } from './histogramLogicType' -import { FunnelLayout } from 'lib/constants' export const histogramLogic = kea([ path(['scenes', 'insights', 'Histogram', 'histogramLogic']), diff --git a/frontend/src/scenes/insights/views/InsightsTable/DashboardInsightsTable.tsx b/frontend/src/scenes/insights/views/InsightsTable/DashboardInsightsTable.tsx index 6ed7c8f0c9df7..f01085a698529 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/DashboardInsightsTable.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/DashboardInsightsTable.tsx @@ -1,5 +1,4 @@ import { useValues } from 'kea' - import { insightLogic } from 'scenes/insights/insightLogic' import { InsightsTable } from './InsightsTable' diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.scss b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.scss index be47ead478a54..4158e729c4915 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.scss +++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.scss @@ -1,17 +1,9 @@ -.insights-table.LemonTable--embedded { - border-top: 1px solid var(--border); -} -.InsightCard .insights-table { - min-height: 100%; - border-top: none; -} - .series-name-wrapper-col { display: flex; align-items: center; .edit-icon { - color: var(--primary); + color: var(--primary-3000); cursor: pointer; font-size: 1rem; } @@ -19,8 +11,9 @@ .insights-label { &.editable { cursor: pointer; + .EntityFilterInfo { - color: var(--primary); + color: var(--primary-3000); font-weight: 500; } } diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx index 5b3283417f019..0d658d852b08f 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx @@ -1,16 +1,15 @@ -import { useState } from 'react' -import { BindLogic } from 'kea' import { Meta, StoryFn, StoryObj } from '@storybook/react' - +import { BindLogic } from 'kea' +import { useState } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' -import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' + import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' - +import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' +import { getCachedResults } from '~/queries/nodes/InsightViz/utils' import { BaseMathType, InsightLogicProps } from '~/types' import { InsightsTable } from './InsightsTable' -import { getCachedResults } from '~/queries/nodes/InsightViz/utils' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx index bf0c656c7135e..004bf853b61ca 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx @@ -1,27 +1,29 @@ -import { useActions, useValues } from 'kea' -import { cohortsModel } from '~/models/cohortsModel' -import { ChartDisplayType, ItemMode } from '~/types' -import { CalcColumnState } from './insightsTableLogic' -import { IndexedTrendResult } from 'scenes/trends/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { entityFilterLogic } from '../../filters/ActionFilter/entityFilterLogic' import './InsightsTable.scss' + +import { useActions, useValues } from 'kea' +import { getSeriesColor } from 'lib/colors' import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { countryCodeToName } from '../WorldMap' -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatBreakdownLabel } from 'scenes/insights/utils' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { isTrendsFilter } from 'scenes/insights/sharedUtils' +import { formatBreakdownLabel } from 'scenes/insights/utils' +import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' +import { IndexedTrendResult } from 'scenes/trends/types' -import { SeriesCheckColumnTitle, SeriesCheckColumnItem } from './columns/SeriesCheckColumn' -import { SeriesColumnItem } from './columns/SeriesColumn' -import { BreakdownColumnTitle, BreakdownColumnItem } from './columns/BreakdownColumn' -import { WorldMapColumnTitle, WorldMapColumnItem } from './columns/WorldMapColumn' +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { ChartDisplayType, ItemMode } from '~/types' + +import { entityFilterLogic } from '../../filters/ActionFilter/entityFilterLogic' +import { countryCodeToName } from '../WorldMap' import { AggregationColumnItem, AggregationColumnTitle } from './columns/AggregationColumn' +import { BreakdownColumnItem, BreakdownColumnTitle } from './columns/BreakdownColumn' +import { SeriesCheckColumnItem, SeriesCheckColumnTitle } from './columns/SeriesCheckColumn' +import { SeriesColumnItem } from './columns/SeriesColumn' import { ValueColumnItem, ValueColumnTitle } from './columns/ValueColumn' +import { WorldMapColumnItem, WorldMapColumnTitle } from './columns/WorldMapColumn' import { AggregationType, insightsTableDataLogic } from './insightsTableDataLogic' -import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' -import { getSeriesColor } from 'lib/colors' +import { CalcColumnState } from './insightsTableLogic' export interface InsightsTableProps { /** Whether this is just a legend instead of standalone insight viz. Default: false. */ @@ -229,7 +231,6 @@ export function InsightsTable({ loading={insightDataLoading} emptyState="No insight results" data-attr="insights-table-graph" - className="insights-table" useURLForSorting={insightMode !== ItemMode.Edit} rowRibbonColor={isLegend ? (item) => getSeriesColor(item.seriesIndex, compare || false) : undefined} firstColumnSticky diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx index ba2ec9f539329..17c37aa67e073 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx @@ -1,19 +1,18 @@ -import { useValues, useActions } from 'kea' -import { Dropdown, Menu } from 'antd' // eslint-disable-next-line no-restricted-imports import { DownOutlined } from '@ant-design/icons' - -import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' - +import { Dropdown, Menu } from 'antd' +import { useActions, useValues } from 'kea' import { average, median } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { formatAggregationValue } from 'scenes/insights/utils' import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' +import { formatAggregationValue } from 'scenes/insights/utils' import { IndexedTrendResult } from 'scenes/trends/types' -import { CalcColumnState } from '../insightsTableLogic' -import { TrendsFilterType } from '~/types' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { TrendsFilter } from '~/queries/schema' +import { TrendsFilterType } from '~/types' + +import { CalcColumnState } from '../insightsTableLogic' const CALC_COLUMN_LABELS: Record = { total: 'Total Sum', diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/BreakdownColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/BreakdownColumn.tsx index b64d677c8f975..a81efe34981b7 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/BreakdownColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/BreakdownColumn.tsx @@ -1,8 +1,9 @@ -import { BreakdownFilter } from '~/queries/schema' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import stringWithWBR from 'lib/utils/stringWithWBR' -import { IndexedTrendResult } from 'scenes/trends/types' import { formatBreakdownType } from 'scenes/insights/utils' +import { IndexedTrendResult } from 'scenes/trends/types' + +import { BreakdownFilter } from '~/queries/schema' type BreakdownColumnTitleProps = { breakdownFilter: BreakdownFilter } diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/SeriesColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/SeriesColumn.tsx index 785744697466f..81896e494e251 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/SeriesColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/SeriesColumn.tsx @@ -1,11 +1,12 @@ import clsx from 'clsx' import { getSeriesColor } from 'lib/colors' -import { IndexedTrendResult } from 'scenes/trends/types' import { InsightLabel } from 'lib/components/InsightLabel' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconEdit } from 'lib/lemon-ui/icons' -import { TrendResult } from '~/types' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { capitalizeFirstLetter } from 'lib/utils' +import { IndexedTrendResult } from 'scenes/trends/types' + +import { TrendResult } from '~/types' type SeriesColumnItemProps = { item: IndexedTrendResult diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx index 1273048ef0b1e..26b225cae477e 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx @@ -1,11 +1,12 @@ import { useValues } from 'kea' -import { IndexedTrendResult } from 'scenes/trends/types' import { DateDisplay } from 'lib/components/DateDisplay' -import { IntervalType, TrendsFilterType } from '~/types' -import { formatAggregationValue } from 'scenes/insights/utils' import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' +import { formatAggregationValue } from 'scenes/insights/utils' +import { IndexedTrendResult } from 'scenes/trends/types' + import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { TrendsFilter } from '~/queries/schema' +import { IntervalType, TrendsFilterType } from '~/types' type ValueColumnTitleProps = { index: number diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/WorldMapColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/WorldMapColumn.tsx index 7efdf58de5844..a05e2d00717cf 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/WorldMapColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/WorldMapColumn.tsx @@ -1,5 +1,6 @@ -import { IndexedTrendResult } from 'scenes/trends/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { IndexedTrendResult } from 'scenes/trends/types' + import { countryCodeToName } from '../../WorldMap' export function WorldMapColumnTitle(): JSX.Element { diff --git a/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.test.ts b/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.test.ts index 46357caafc616..30ddcd27e2a4a 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.test.ts +++ b/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.test.ts @@ -1,11 +1,11 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { BaseMathType, ChartDisplayType, InsightShortId, PropertyMathType } from '~/types' import { NodeKind, TrendsQuery } from '~/queries/schema' +import { initKeaTests } from '~/test/init' +import { BaseMathType, ChartDisplayType, InsightShortId, PropertyMathType } from '~/types' -import { insightsTableDataLogic, AggregationType } from './insightsTableDataLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { AggregationType, insightsTableDataLogic } from './insightsTableDataLogic' const Insight123 = '123' as InsightShortId diff --git a/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.ts b/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.ts index cb40c623122b6..1d9a2806444c9 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.ts +++ b/frontend/src/scenes/insights/views/InsightsTable/insightsTableDataLogic.ts @@ -1,11 +1,10 @@ -import { kea, props, key, path, connect, actions, reducers, selectors } from 'kea' +import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { ChartDisplayType, InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' - import type { insightsTableDataLogicType } from './insightsTableDataLogicType' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' export enum AggregationType { Total = 'total', diff --git a/frontend/src/scenes/insights/views/InsightsTable/insightsTableLogic.ts b/frontend/src/scenes/insights/views/InsightsTable/insightsTableLogic.ts index a4c1c8962a0f3..04d17c2c5a031 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/insightsTableLogic.ts +++ b/frontend/src/scenes/insights/views/InsightsTable/insightsTableLogic.ts @@ -1,7 +1,9 @@ -import { kea, props, path, actions, reducers, selectors } from 'kea' +import { actions, kea, path, props, reducers, selectors } from 'kea' +import { isTrendsFilter } from 'scenes/insights/sharedUtils' + import { ChartDisplayType, FilterType } from '~/types' + import type { insightsTableLogicType } from './insightsTableLogicType' -import { isTrendsFilter } from 'scenes/insights/sharedUtils' export type CalcColumnState = 'total' | 'average' | 'median' diff --git a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx index 51576c2496ec9..88bc33ee22fea 100644 --- a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx +++ b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx @@ -1,5 +1,8 @@ -import { useEffect, useRef, useState } from 'react' -import { Root, createRoot } from 'react-dom/client' +import 'chartjs-adapter-dayjs-3' + +import ChartDataLabels from 'chartjs-plugin-datalabels' +import ChartjsPluginStacked100, { ExtendedChartData } from 'chartjs-plugin-stacked100' +import clsx from 'clsx' import { useValues } from 'kea' import { ActiveElement, @@ -10,33 +13,33 @@ import { ChartOptions, ChartType, Color, + GridLineOptions, InteractionItem, + ScriptableLineSegmentContext, TickOptions, - GridLineOptions, TooltipModel, TooltipOptions, - ScriptableLineSegmentContext, } from 'lib/Chart' -import ChartDataLabels from 'chartjs-plugin-datalabels' -import 'chartjs-adapter-dayjs-3' -import { areObjectValuesEmpty, lightenDarkenColor, hexToRGBA } from '~/lib/utils' import { getBarColorFromStatus, getGraphColors, getSeriesColor } from 'lib/colors' import { AnnotationsOverlay } from 'lib/components/AnnotationsOverlay' -import { GraphDataset, GraphPoint, GraphPointPayload, GraphType } from '~/types' -import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' -import { lineGraphLogic } from 'scenes/insights/views/LineGraph/lineGraphLogic' -import { TooltipConfig } from 'scenes/insights/InsightTooltip/insightTooltipUtils' -import { groupsModel } from '~/models/groupsModel' -import { ErrorBoundary } from '~/layout/ErrorBoundary' +import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { useResizeObserver } from 'lib/hooks/useResizeObserver' +import { useEffect, useRef, useState } from 'react' +import { createRoot, Root } from 'react-dom/client' import { formatAggregationAxisValue, formatPercentStackAxisValue } from 'scenes/insights/aggregationAxisFormat' import { insightLogic } from 'scenes/insights/insightLogic' -import { useResizeObserver } from 'lib/hooks/useResizeObserver' +import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' +import { TooltipConfig } from 'scenes/insights/InsightTooltip/insightTooltipUtils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { lineGraphLogic } from 'scenes/insights/views/LineGraph/lineGraphLogic' import { PieChart } from 'scenes/insights/views/LineGraph/PieChart' + +import { ErrorBoundary } from '~/layout/ErrorBoundary' import { themeLogic } from '~/layout/navigation-3000/themeLogic' -import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { areObjectValuesEmpty, hexToRGBA, lightenDarkenColor } from '~/lib/utils' +import { groupsModel } from '~/models/groupsModel' import { TrendsFilter } from '~/queries/schema' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import ChartjsPluginStacked100, { ExtendedChartData } from 'chartjs-plugin-stacked100' +import { GraphDataset, GraphPoint, GraphPointPayload, GraphType } from '~/types' let tooltipRoot: Root @@ -181,7 +184,7 @@ export const filterNestedDataset = ( }) } -function createPinstripePattern(color: string): CanvasPattern { +function createPinstripePattern(color: string, isDarkMode: boolean): CanvasPattern { const stripeWidth = 8 // 0.5rem const stripeAngle = -22.5 @@ -189,19 +192,19 @@ function createPinstripePattern(color: string): CanvasPattern { const canvas = document.createElement('canvas') canvas.width = 1 canvas.height = stripeWidth * 2 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ctx = canvas.getContext('2d')! // fill the canvas with given color ctx.fillStyle = color ctx.fillRect(0, 0, canvas.width, canvas.height) - // overlay half-transparent white stripe - ctx.fillStyle = 'rgba(255, 255, 255, 0.5)' + // overlay half-transparent black / white stripes + ctx.fillStyle = isDarkMode ? 'rgba(35, 36, 41, 0.5)' : 'rgba(255, 255, 255, 0.5)' ctx.fillRect(0, stripeWidth, 1, 2 * stripeWidth) // create a canvas pattern and rotate it - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const pattern = ctx.createPattern(canvas, 'repeat')! const xAx = Math.cos(stripeAngle) const xAy = Math.sin(stripeAngle) @@ -221,7 +224,6 @@ export interface LineGraphProps { inSharedMode?: boolean showPersonsModal?: boolean tooltip?: TooltipConfig - inCardView?: boolean inSurveyView?: boolean isArea?: boolean incompletenessOffsetFromEnd?: number // Number of data points at end of dataset to replace with a dotted line. Only used in line graphs. @@ -255,7 +257,6 @@ export function LineGraph_({ ['data-attr']: dataAttr, showPersonsModal = true, compare = false, - inCardView, inSurveyView, isArea = false, incompletenessOffsetFromEnd = -1, @@ -297,7 +298,6 @@ export function LineGraph_({ const isBackgroundBasedGraphType = [GraphType.Bar, GraphType.HorizontalBar].includes(type) const isPercentStackView = !!supportsPercentStackView && !!showPercentStackView const showAnnotations = isTrends && !isHorizontal && !hideAnnotations - const shouldAutoResize = isHorizontal && !inCardView // Remove tooltip element on unmount useEffect(() => { @@ -313,7 +313,7 @@ export function LineGraph_({ : getSeriesColor(dataset.id, compare && !isArea) const hoverColor = dataset?.status ? getBarColorFromStatus(dataset.status, true) : mainColor const areaBackgroundColor = hexToRGBA(mainColor, 0.5) - const areaIncompletePattern = createPinstripePattern(areaBackgroundColor) + const areaIncompletePattern = createPinstripePattern(areaBackgroundColor, isDarkModeOn) let backgroundColor: string | undefined = undefined if (isBackgroundBasedGraphType) { backgroundColor = mainColor @@ -388,7 +388,9 @@ export function LineGraph_({ }, } const gridOptions: Partial = { - borderColor: colors.axisLine as string, + color: colors.axisLine as Color, + borderColor: colors.axisLine as Color, + tickColor: colors.axisLine as Color, borderDash: [4, 2], } @@ -689,14 +691,6 @@ export function LineGraph_({ const height = dynamicHeight const parentNode: any = scale.chart?.canvas?.parentNode parentNode.style.height = `${height}px` - } else if (shouldAutoResize) { - // automatically resize the chart container to fit the number of rows - const MIN_HEIGHT = 575 - const ROW_HEIGHT = 16 - const dynamicHeight = scale.ticks.length * ROW_HEIGHT - const height = Math.max(dynamicHeight, MIN_HEIGHT) - const parentNode: any = scale.chart?.canvas?.parentNode - parentNode.style.height = `${height}px` } else { // display only as many bars, as we can fit labels scale.max = scale.ticks.length @@ -706,13 +700,21 @@ export function LineGraph_({ ticks: { ...tickOptions, precision, - autoSkip: !shouldAutoResize, + autoSkip: true, callback: function _renderYLabel(_, i) { - const labelDescriptors = [ - datasets?.[0]?.actions?.[i]?.custom_name ?? datasets?.[0]?.actions?.[i]?.name, // action name - datasets?.[0]?.breakdownValues?.[i], // breakdown value - datasets?.[0]?.compareLabels?.[i], // compare value - ].filter((l) => !!l) + const labelDescriptors = ( + datasets?.[0]?.labels?.[i] + ? [ + // prefer to use the label over the action name if it exists + datasets?.[0]?.labels?.[i], + datasets?.[0]?.compareLabels?.[i], + ] + : [ + datasets?.[0]?.actions?.[i]?.custom_name ?? datasets?.[0]?.actions?.[i]?.name, // action name + datasets?.[0]?.breakdownValues?.[i], // breakdown value + datasets?.[0]?.compareLabels?.[i], // compare value + ] + ).filter((l) => !!l) return labelDescriptors.join(' - ') }, }, @@ -737,7 +739,7 @@ export function LineGraph_({ return (
    diff --git a/frontend/src/scenes/insights/views/LineGraph/PieChart.tsx b/frontend/src/scenes/insights/views/LineGraph/PieChart.tsx index f58924882f82d..d3424bbcb8fc2 100644 --- a/frontend/src/scenes/insights/views/LineGraph/PieChart.tsx +++ b/frontend/src/scenes/insights/views/LineGraph/PieChart.tsx @@ -1,19 +1,24 @@ -import { useEffect, useRef } from 'react' +import 'chartjs-adapter-dayjs-3' + +import ChartDataLabels, { Context } from 'chartjs-plugin-datalabels' +import { useActions, useValues } from 'kea' import { ActiveElement, Chart, + ChartDataset, ChartEvent, ChartItem, - ChartType, - TooltipModel, ChartOptions, - ChartDataset, + ChartType, Plugin, + TooltipModel, } from 'lib/Chart' -import 'chartjs-adapter-dayjs-3' -import { areObjectValuesEmpty } from '~/lib/utils' -import { GraphType } from '~/types' +import { SeriesLetter } from 'lib/components/SeriesGlyph' +import { useEffect, useRef } from 'react' import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' +import { insightLogic } from 'scenes/insights/insightLogic' +import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' +import { SeriesDatum } from 'scenes/insights/InsightTooltip/insightTooltipUtils' import { ensureTooltip, filterNestedDataset, @@ -21,14 +26,11 @@ import { onChartClick, onChartHover, } from 'scenes/insights/views/LineGraph/LineGraph' -import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' -import { useActions, useValues } from 'kea' -import { groupsModel } from '~/models/groupsModel' import { lineGraphLogic } from 'scenes/insights/views/LineGraph/lineGraphLogic' -import { insightLogic } from 'scenes/insights/insightLogic' -import { SeriesDatum } from 'scenes/insights/InsightTooltip/insightTooltipUtils' -import { SeriesLetter } from 'lib/components/SeriesGlyph' -import ChartDataLabels, { Context } from 'chartjs-plugin-datalabels' + +import { areObjectValuesEmpty } from '~/lib/utils' +import { groupsModel } from '~/models/groupsModel' +import { GraphType } from '~/types' let timer: NodeJS.Timeout | null = null @@ -50,6 +52,11 @@ function getPercentageForDataPoint(context: Context): number { return ((context.dataset.data[context.dataIndex] as number) / total) * 100 } +export interface PieChartProps extends LineGraphProps { + showLabelOnSeries?: boolean | null + disableHoverOffset?: boolean | null +} + export function PieChart({ datasets: _datasets, hiddenLegendKeys, @@ -60,12 +67,14 @@ export function PieChart({ trendsFilter, formula, showValueOnSeries, + showLabelOnSeries, supportsPercentStackView, showPercentStackView, tooltip: tooltipConfig, showPersonsModal = true, labelGroupType, -}: LineGraphProps): JSX.Element { + disableHoverOffset, +}: PieChartProps): JSX.Element { const isPie = type === GraphType.Pie const isPercentStackView = !!supportsPercentStackView && !!showPercentStackView @@ -114,12 +123,14 @@ export function PieChart({ layout: { padding: { top: 12, // 12 px so that the label isn't cropped + left: 20, + right: 20, bottom: 20, // 12 px so that the label isn't cropped + 8 px of padding against the number below }, }, borderWidth: 0, borderRadius: 0, - hoverOffset: onlyOneValue ? 0 : 16, // don't offset hovered segment if it is 100% + hoverOffset: onlyOneValue || disableHoverOffset ? 0 : 16, // don't offset hovered segment if it is 100% onHover(event: ChartEvent, _: ActiveElement[], chart: Chart) { onChartHover(event, chart, onClick) }, @@ -135,7 +146,8 @@ export function PieChart({ }, display: (context) => { const percentage = getPercentageForDataPoint(context) - return showValueOnSeries !== false && // show if true or unset + return (showValueOnSeries !== false || // show if true or unset + showLabelOnSeries) && context.dataset.data.length > 1 && percentage > 5 ? 'auto' @@ -149,6 +161,10 @@ export function PieChart({ return { top: paddingY, bottom: paddingY, left: paddingX, right: paddingX } }, formatter: (value: number, context) => { + if (showLabelOnSeries) { + // cast to any as it seems like TypeScript types are wrong + return (context.dataset as any).labels?.[context.dataIndex] + } if (isPercentStackView) { const percentage = getPercentageForDataPoint(context) return `${percentage.toFixed(1)}%` diff --git a/frontend/src/scenes/insights/views/LineGraph/lineGraphLogic.ts b/frontend/src/scenes/insights/views/LineGraph/lineGraphLogic.ts index e7473601a28b7..a5df4b88f6c79 100644 --- a/frontend/src/scenes/insights/views/LineGraph/lineGraphLogic.ts +++ b/frontend/src/scenes/insights/views/LineGraph/lineGraphLogic.ts @@ -1,7 +1,9 @@ -import { kea, path, selectors } from 'kea' import { TooltipItem } from 'chart.js' -import { GraphDataset } from '~/types' +import { kea, path, selectors } from 'kea' import { SeriesDatum } from 'scenes/insights/InsightTooltip/insightTooltipUtils' + +import { GraphDataset } from '~/types' + import type { lineGraphLogicType } from './lineGraphLogicType' // TODO: Eventually we should move all state from LineGraph into this logic diff --git a/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx b/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx index 74fcf78ea4515..a618625785b41 100644 --- a/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx +++ b/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx @@ -1,15 +1,14 @@ -import { useActions, useValues } from 'kea' -import { Select } from 'antd' // eslint-disable-next-line no-restricted-imports import { BarsOutlined } from '@ant-design/icons' +import { Select } from 'antd' +import { useActions, useValues } from 'kea' import { ANTD_TOOLTIP_PLACEMENTS } from 'lib/utils' - +import { insightLogic } from 'scenes/insights/insightLogic' import { DEFAULT_STEP_LIMIT } from 'scenes/paths/pathsDataLogic' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' import { userLogic } from 'scenes/userLogic' import { AvailableFeature } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' interface StepOption { label: string diff --git a/frontend/src/scenes/insights/views/Trends/FunnelsCue.tsx b/frontend/src/scenes/insights/views/Trends/FunnelsCue.tsx index 263a633226135..c7f9433a99bf6 100644 --- a/frontend/src/scenes/insights/views/Trends/FunnelsCue.tsx +++ b/frontend/src/scenes/insights/views/Trends/FunnelsCue.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' -import { funnelsCueLogic } from 'scenes/insights/views/Trends/funnelsCueLogic' -import { insightLogic } from 'scenes/insights/insightLogic' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { insightLogic } from 'scenes/insights/insightLogic' +import { funnelsCueLogic } from 'scenes/insights/views/Trends/funnelsCueLogic' export function FunnelsCue(): JSX.Element | null { const { insightProps } = useValues(insightLogic) diff --git a/frontend/src/scenes/insights/views/Trends/funnelsCueLogic.tsx b/frontend/src/scenes/insights/views/Trends/funnelsCueLogic.tsx index cd1ac529b56f5..fce1c5c8c6f04 100644 --- a/frontend/src/scenes/insights/views/Trends/funnelsCueLogic.tsx +++ b/frontend/src/scenes/insights/views/Trends/funnelsCueLogic.tsx @@ -1,14 +1,16 @@ -import { kea, props, key, path, connect, actions, reducers, listeners, selectors, events } from 'kea' -import { InsightLogicProps } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { insightLogic } from 'scenes/insights/insightLogic' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import posthog from 'posthog-js' -import { FEATURE_FLAGS } from 'lib/constants' -import type { funnelsCueLogicType } from './funnelsCueLogicType' +import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { isFunnelsQuery, isInsightVizNode, isTrendsQuery } from '~/queries/utils' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + import { InsightVizNode, NodeKind } from '~/queries/schema' +import { isFunnelsQuery, isInsightVizNode, isTrendsQuery } from '~/queries/utils' +import { InsightLogicProps } from '~/types' + +import type { funnelsCueLogicType } from './funnelsCueLogicType' export const funnelsCueLogic = kea([ props({} as InsightLogicProps), @@ -89,7 +91,7 @@ export const funnelsCueLogic = kea([ ], }), events(({ actions, values }) => ({ - afterMount: async () => { + afterMount: () => { if (values.featureFlags[FEATURE_FLAGS.FUNNELS_CUE_OPT_OUT]) { actions.setPermanentOptOut() } diff --git a/frontend/src/scenes/insights/views/WorldMap/WorldMap.scss b/frontend/src/scenes/insights/views/WorldMap/WorldMap.scss index aca29481c5b97..e4ae155aede2f 100644 --- a/frontend/src/scenes/insights/views/WorldMap/WorldMap.scss +++ b/frontend/src/scenes/insights/views/WorldMap/WorldMap.scss @@ -2,13 +2,15 @@ padding: 1rem 0; width: 100%; color: var(--border); + .landxx { fill: currentColor; stroke: var(--bg-light); stroke-width: 0.125rem; fill-rule: evenodd; + &:hover { - color: var(--primary-dark) !important; + color: var(--primary-3000-hover) !important; } } } diff --git a/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx b/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx index 472ad00a1aaeb..5085cfd419acd 100644 --- a/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx +++ b/frontend/src/scenes/insights/views/WorldMap/WorldMap.tsx @@ -1,18 +1,21 @@ -import { useValues, useActions } from 'kea' +import './WorldMap.scss' + +import { useActions, useValues } from 'kea' +import { BRAND_BLUE_HSL, gradateColor } from 'lib/colors' import React, { HTMLProps, useEffect, useRef } from 'react' +import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' import { insightLogic } from 'scenes/insights/insightLogic' -import { ChartDisplayType, ChartParams, TrendResult } from '~/types' -import './WorldMap.scss' import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip' +import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' + +import { groupsModel } from '~/models/groupsModel' +import { ChartDisplayType, ChartParams, TrendResult } from '~/types' + import { SeriesDatum } from '../../InsightTooltip/insightTooltipUtils' import { ensureTooltip } from '../LineGraph/LineGraph' -import { worldMapLogic } from './worldMapLogic' import { countryCodeToFlag, countryCodeToName } from './countryCodes' import { countryVectors } from './countryVectors' -import { groupsModel } from '~/models/groupsModel' -import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' -import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' -import { BRAND_BLUE_HSL, gradateColor } from 'lib/colors' +import { worldMapLogic } from './worldMapLogic' /** The saturation of a country is proportional to its value BUT the saturation has a floor to improve visibility. */ const SATURATION_FLOOR = 0.2 diff --git a/frontend/src/scenes/insights/views/WorldMap/index.ts b/frontend/src/scenes/insights/views/WorldMap/index.ts index 3b4866cf21dca..ad0fc13da1f54 100644 --- a/frontend/src/scenes/insights/views/WorldMap/index.ts +++ b/frontend/src/scenes/insights/views/WorldMap/index.ts @@ -1,2 +1,2 @@ -export { WorldMap } from './WorldMap' export { countryCodeToFlag, countryCodeToName } from './countryCodes' +export { WorldMap } from './WorldMap' diff --git a/frontend/src/scenes/insights/views/WorldMap/worldMapLogic.tsx b/frontend/src/scenes/insights/views/WorldMap/worldMapLogic.tsx index 755c7f1e22445..534e66560ca4b 100644 --- a/frontend/src/scenes/insights/views/WorldMap/worldMapLogic.tsx +++ b/frontend/src/scenes/insights/views/WorldMap/worldMapLogic.tsx @@ -1,6 +1,8 @@ -import { kea, props, key, path, connect, actions, reducers, selectors } from 'kea' +import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + import { InsightLogicProps, TrendResult } from '~/types' + import { keyForInsightLogicProps } from '../../sharedUtils' import type { worldMapLogicType } from './worldMapLogicType' diff --git a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationDetails.tsx b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationDetails.tsx index 1d14c6223dea7..b0e523bef5788 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationDetails.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationDetails.tsx @@ -1,10 +1,11 @@ -import { AsyncMigration, AsyncMigrationError, asyncMigrationsLogic } from './asyncMigrationsLogic' -import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { useActions, useValues } from 'kea' +import { IconRefresh } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { humanFriendlyDetailedTime } from 'lib/utils' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconRefresh } from 'lib/lemon-ui/icons' + +import { AsyncMigration, AsyncMigrationError, asyncMigrationsLogic } from './asyncMigrationsLogic' export function AsyncMigrationDetails({ asyncMigration }: { asyncMigration: AsyncMigration }): JSX.Element { const { asyncMigrationErrorsLoading, asyncMigrationErrors } = useValues(asyncMigrationsLogic) diff --git a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationParametersModal.tsx b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationParametersModal.tsx index a0deb2a283041..16dbf65a8ce9d 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationParametersModal.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrationParametersModal.tsx @@ -1,13 +1,13 @@ -import { useState } from 'react' +import { Link } from '@posthog/lemon-ui' import { useActions } from 'kea' -import { AsyncMigrationModalProps, asyncMigrationsLogic } from 'scenes/instance/AsyncMigrations/asyncMigrationsLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { asyncMigrationParameterFormLogic } from 'scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic' import { Field, Form } from 'kea-forms' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { Link } from '@posthog/lemon-ui' +import { useState } from 'react' +import { asyncMigrationParameterFormLogic } from 'scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic' +import { AsyncMigrationModalProps, asyncMigrationsLogic } from 'scenes/instance/AsyncMigrations/asyncMigrationsLogic' export function AsyncMigrationParametersModal(props: AsyncMigrationModalProps): JSX.Element { const { closeAsyncMigrationsModal } = useActions(asyncMigrationsLogic) diff --git a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx index dc1be09924ebc..4bc890c80f38a 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx @@ -1,29 +1,30 @@ -import { useEffect } from 'react' -import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' +import { Link } from '@posthog/lemon-ui' import { Button, Progress } from 'antd' import { useActions, useValues } from 'kea' +import { PageHeader } from 'lib/components/PageHeader' +import { IconPlayCircle, IconRefresh, IconReplay } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { LemonTag, LemonTagType } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { humanFriendlyDetailedTime } from 'lib/utils' +import { useEffect } from 'react' +import { AsyncMigrationParametersModal } from 'scenes/instance/AsyncMigrations/AsyncMigrationParametersModal' +import { SceneExport } from 'scenes/sceneTypes' +import { userLogic } from 'scenes/userLogic' + +import { AsyncMigrationDetails } from './AsyncMigrationDetails' import { AsyncMigration, - migrationStatusNumberToMessage, asyncMigrationsLogic, AsyncMigrationsTab, AsyncMigrationStatus, + migrationStatusNumberToMessage, } from './asyncMigrationsLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { userLogic } from 'scenes/userLogic' import { SettingUpdateField } from './SettingUpdateField' -import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { AsyncMigrationDetails } from './AsyncMigrationDetails' -import { humanFriendlyDetailedTime } from 'lib/utils' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonTag, LemonTagType } from 'lib/lemon-ui/LemonTag/LemonTag' -import { IconPlayCircle, IconRefresh, IconReplay } from 'lib/lemon-ui/icons' -import { AsyncMigrationParametersModal } from 'scenes/instance/AsyncMigrations/AsyncMigrationParametersModal' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { Link } from '@posthog/lemon-ui' export const scene: SceneExport = { component: AsyncMigrations, diff --git a/frontend/src/scenes/instance/AsyncMigrations/SettingUpdateField.tsx b/frontend/src/scenes/instance/AsyncMigrations/SettingUpdateField.tsx index 20bd8b9065626..415d832354f3e 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/SettingUpdateField.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/SettingUpdateField.tsx @@ -1,9 +1,11 @@ -import { useState } from 'react' import { Button, Col, Divider, Input, Row } from 'antd' import { useActions } from 'kea' -import { asyncMigrationsLogic } from './asyncMigrationsLogic' +import { useState } from 'react' + import { InstanceSetting } from '~/types' +import { asyncMigrationsLogic } from './asyncMigrationsLogic' + export function SettingUpdateField({ setting }: { setting: InstanceSetting }): JSX.Element { const { updateSetting } = useActions(asyncMigrationsLogic) diff --git a/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic.ts b/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic.ts index 449af49a92dfc..8cee7669a49f5 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic.ts +++ b/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationParameterFormLogic.ts @@ -1,4 +1,4 @@ -import { kea, key, props, path } from 'kea' +import { kea, key, path, props } from 'kea' import { forms } from 'kea-forms' import { AsyncMigrationModalProps, asyncMigrationsLogic } from 'scenes/instance/AsyncMigrations/asyncMigrationsLogic' diff --git a/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationsLogic.ts b/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationsLogic.ts index b87588dff5198..9966096c8a6d0 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationsLogic.ts +++ b/frontend/src/scenes/instance/AsyncMigrations/asyncMigrationsLogic.ts @@ -1,14 +1,15 @@ +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { actionToUrl, urlToAction } from 'kea-router' import api from 'lib/api' -import { kea, path, connect, actions, events, listeners, selectors, reducers } from 'kea' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { systemStatusLogic } from 'scenes/instance/SystemStatus/systemStatusLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { userLogic } from 'scenes/userLogic' -import type { asyncMigrationsLogicType } from './asyncMigrationsLogicType' -import { systemStatusLogic } from 'scenes/instance/SystemStatus/systemStatusLogic' import { InstanceSetting } from '~/types' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { loaders } from 'kea-loaders' -import { actionToUrl, urlToAction } from 'kea-router' + +import type { asyncMigrationsLogicType } from './asyncMigrationsLogicType' export type TabName = 'overview' | 'internal_metrics' // keep in sync with MigrationStatus in posthog/models/async_migration.py diff --git a/frontend/src/scenes/instance/DeadLetterQueue/DeadLetterQueue.tsx b/frontend/src/scenes/instance/DeadLetterQueue/DeadLetterQueue.tsx index fa516dea91558..6b8a6933c39f6 100644 --- a/frontend/src/scenes/instance/DeadLetterQueue/DeadLetterQueue.tsx +++ b/frontend/src/scenes/instance/DeadLetterQueue/DeadLetterQueue.tsx @@ -1,10 +1,11 @@ +import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { SceneExport } from 'scenes/sceneTypes' -import { useValues, useActions } from 'kea' -import { deadLetterQueueLogic, DeadLetterQueueTab } from './deadLetterQueueLogic' import { userLogic } from 'scenes/userLogic' + +import { deadLetterQueueLogic, DeadLetterQueueTab } from './deadLetterQueueLogic' import { MetricsTab } from './MetricsTab' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' export const scene: SceneExport = { component: DeadLetterQueue, diff --git a/frontend/src/scenes/instance/DeadLetterQueue/MetricsTab.tsx b/frontend/src/scenes/instance/DeadLetterQueue/MetricsTab.tsx index 901500d92a6c4..430c80e85cdbf 100644 --- a/frontend/src/scenes/instance/DeadLetterQueue/MetricsTab.tsx +++ b/frontend/src/scenes/instance/DeadLetterQueue/MetricsTab.tsx @@ -1,11 +1,12 @@ import { Button, Col, Divider, Row, Statistic } from 'antd' -import { useValues, useActions } from 'kea' -import { deadLetterQueueLogic } from './deadLetterQueueLogic' -import { userLogic } from 'scenes/userLogic' -import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { useActions, useValues } from 'kea' +import { IconRefresh } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonTable } from 'lib/lemon-ui/LemonTable' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { IconRefresh } from 'lib/lemon-ui/icons' +import { userLogic } from 'scenes/userLogic' + +import { deadLetterQueueLogic } from './deadLetterQueueLogic' // keep in sync with posthog/api/dead_letter_queue.py const ROWS_LIMIT = 10 diff --git a/frontend/src/scenes/instance/DeadLetterQueue/deadLetterQueueLogic.ts b/frontend/src/scenes/instance/DeadLetterQueue/deadLetterQueueLogic.ts index 4bd84df2e226d..2135874059cdc 100644 --- a/frontend/src/scenes/instance/DeadLetterQueue/deadLetterQueueLogic.ts +++ b/frontend/src/scenes/instance/DeadLetterQueue/deadLetterQueueLogic.ts @@ -1,10 +1,10 @@ -import { SystemStatusRow } from './../../../types' -import api from 'lib/api' import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' import { userLogic } from 'scenes/userLogic' +import { SystemStatusRow } from './../../../types' import type { deadLetterQueueLogicType } from './deadLetterQueueLogicType' -import { loaders } from 'kea-loaders' export type TabName = 'overview' | 'internal_metrics' export enum DeadLetterQueueTab { diff --git a/frontend/src/scenes/instance/SystemStatus/InstanceConfigSaveModal.tsx b/frontend/src/scenes/instance/SystemStatus/InstanceConfigSaveModal.tsx index 4bb150b48d68e..79d221898a27a 100644 --- a/frontend/src/scenes/instance/SystemStatus/InstanceConfigSaveModal.tsx +++ b/frontend/src/scenes/instance/SystemStatus/InstanceConfigSaveModal.tsx @@ -2,7 +2,9 @@ import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { pluralize } from 'lib/utils' + import { SystemStatusRow } from '~/types' + import { RenderMetricValue } from './RenderMetricValue' import { systemStatusLogic } from './systemStatusLogic' diff --git a/frontend/src/scenes/instance/SystemStatus/InstanceConfigTab.tsx b/frontend/src/scenes/instance/SystemStatus/InstanceConfigTab.tsx index 6924ec179ffad..9d9f0eff4a507 100644 --- a/frontend/src/scenes/instance/SystemStatus/InstanceConfigTab.tsx +++ b/frontend/src/scenes/instance/SystemStatus/InstanceConfigTab.tsx @@ -1,16 +1,18 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' import { IconOpenInNew, IconWarning } from 'lib/lemon-ui/icons' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' +import { pluralize } from 'lib/utils' +import { useEffect } from 'react' import { EnvironmentConfigOption, preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { InstanceSetting } from '~/types' + +import { InstanceConfigSaveModal } from './InstanceConfigSaveModal' import { MetricValue, RenderMetricValue } from './RenderMetricValue' import { RenderMetricValueEdit } from './RenderMetricValueEdit' import { ConfigMode, systemStatusLogic } from './systemStatusLogic' -import { InstanceConfigSaveModal } from './InstanceConfigSaveModal' -import { pluralize } from 'lib/utils' -import { LemonButton, Link } from '@posthog/lemon-ui' -import { useEffect } from 'react' export function InstanceConfigTab(): JSX.Element { const { configOptions, preflightLoading } = useValues(preflightLogic) diff --git a/frontend/src/scenes/instance/SystemStatus/InternalMetricsTab.tsx b/frontend/src/scenes/instance/SystemStatus/InternalMetricsTab.tsx index a53f8afe10a5e..7451de6a05d29 100644 --- a/frontend/src/scenes/instance/SystemStatus/InternalMetricsTab.tsx +++ b/frontend/src/scenes/instance/SystemStatus/InternalMetricsTab.tsx @@ -1,12 +1,13 @@ -import { useMemo, useState } from 'react' +import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' import { Table } from 'antd' +import { ColumnsType } from 'antd/lib/table' import { useActions, useValues } from 'kea' +import { IconRefresh } from 'lib/lemon-ui/icons' +import { LemonCollapse } from 'lib/lemon-ui/LemonCollapse' +import { useMemo, useState } from 'react' import { systemStatusLogic } from 'scenes/instance/SystemStatus/systemStatusLogic' + import { QuerySummary } from '~/types' -import { ColumnsType } from 'antd/lib/table' -import { LemonCollapse } from 'lib/lemon-ui/LemonCollapse' -import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' -import { IconRefresh } from 'lib/lemon-ui/icons' export function InternalMetricsTab(): JSX.Element { const { openSections, queries, queriesLoading } = useValues(systemStatusLogic) diff --git a/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx b/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx index a61d9eb7e20cb..cbf13c19dcf59 100644 --- a/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx +++ b/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx @@ -1,10 +1,11 @@ -import { Button, Col, Divider, Row } from 'antd' +import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { kafkaInspectorLogic } from './kafkaInspectorLogic' import { Field, Form } from 'kea-forms' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { kafkaInspectorLogic } from './kafkaInspectorLogic' + export function KafkaInspectorTab(): JSX.Element { const { kafkaMessage } = useValues(kafkaInspectorLogic) @@ -12,7 +13,7 @@ export function KafkaInspectorTab(): JSX.Element {

    Kafka Inspector

    Debug Kafka messages using the inspector tool.
    - +
    - -
    +
    +
    - + - -
    + +
    - - {' '} - -
    + + + +
    - - {' '} - - -
    - - - + + + +
    + + Fetch Message + +
    + diff --git a/frontend/src/scenes/instance/SystemStatus/OverviewTab.tsx b/frontend/src/scenes/instance/SystemStatus/OverviewTab.tsx index ab3984d675038..71ed29426d9ab 100644 --- a/frontend/src/scenes/instance/SystemStatus/OverviewTab.tsx +++ b/frontend/src/scenes/instance/SystemStatus/OverviewTab.tsx @@ -1,10 +1,12 @@ -import { systemStatusLogic } from './systemStatusLogic' +import { LemonTable } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { SystemStatusRow, SystemStatusSubrows } from '~/types' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { Link } from 'lib/lemon-ui/Link' + +import { SystemStatusRow, SystemStatusSubrows } from '~/types' + import { RenderMetricValue } from './RenderMetricValue' -import { LemonTable } from '@posthog/lemon-ui' +import { systemStatusLogic } from './systemStatusLogic' const METRIC_KEY_TO_INTERNAL_LINK = { async_migrations_ok: '/instance/async_migrations', diff --git a/frontend/src/scenes/instance/SystemStatus/RenderMetricValue.tsx b/frontend/src/scenes/instance/SystemStatus/RenderMetricValue.tsx index 89b3660a8d886..9d5b2a81083ab 100644 --- a/frontend/src/scenes/instance/SystemStatus/RenderMetricValue.tsx +++ b/frontend/src/scenes/instance/SystemStatus/RenderMetricValue.tsx @@ -1,7 +1,8 @@ +import { TZLabel } from '@posthog/apps-common' +import { IconLock } from 'lib/lemon-ui/icons' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' + import { InstanceSetting, SystemStatusRow } from '~/types' -import { IconLock } from 'lib/lemon-ui/icons' -import { TZLabel } from '@posthog/apps-common' const TIMESTAMP_VALUES = new Set(['last_event_ingested_timestamp']) diff --git a/frontend/src/scenes/instance/SystemStatus/RenderMetricValueEdit.tsx b/frontend/src/scenes/instance/SystemStatus/RenderMetricValueEdit.tsx index 20922721843a3..5f8f3acc9907c 100644 --- a/frontend/src/scenes/instance/SystemStatus/RenderMetricValueEdit.tsx +++ b/frontend/src/scenes/instance/SystemStatus/RenderMetricValueEdit.tsx @@ -1,5 +1,6 @@ import { LemonCheckbox, LemonInput } from '@posthog/lemon-ui' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' + import { MetricValue } from './RenderMetricValue' interface MetricValueEditInterface extends MetricValue { diff --git a/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx b/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx index 3605857912d5d..c9f0274320c1b 100644 --- a/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx +++ b/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx @@ -1,16 +1,18 @@ +import { Link } from '@posthog/lemon-ui' import { Divider, Modal } from 'antd' import { useActions, useValues } from 'kea' +import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { IconDelete } from 'lib/lemon-ui/icons' -import { LemonTableColumns, LemonTable } from 'lib/lemon-ui/LemonTable' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { userLogic } from 'scenes/userLogic' + import { UserType } from '~/types' + import { staffUsersLogic } from './staffUsersLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { userLogic } from 'scenes/userLogic' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' -import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' -import { Link } from '@posthog/lemon-ui' export function StaffUsersTab(): JSX.Element { const { user: myself } = useValues(userLogic) diff --git a/frontend/src/scenes/instance/SystemStatus/index.scss b/frontend/src/scenes/instance/SystemStatus/index.scss index d837403778489..b824861294f47 100644 --- a/frontend/src/scenes/instance/SystemStatus/index.scss +++ b/frontend/src/scenes/instance/SystemStatus/index.scss @@ -1,5 +1,6 @@ .system-status-scene { margin-bottom: 64px; + .metric-column { @media (min-width: 750px) { width: 33%; diff --git a/frontend/src/scenes/instance/SystemStatus/index.tsx b/frontend/src/scenes/instance/SystemStatus/index.tsx index 0c4e7d08eda42..69ccf9b44bc11 100644 --- a/frontend/src/scenes/instance/SystemStatus/index.tsx +++ b/frontend/src/scenes/instance/SystemStatus/index.tsx @@ -1,24 +1,24 @@ import './index.scss' -import { Alert } from 'antd' -import { systemStatusLogic, InstanceStatusTabName } from './systemStatusLogic' +import { LemonBanner, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { FEATURE_FLAGS } from 'lib/constants' import { IconInfo } from 'lib/lemon-ui/icons' -import { OverviewTab } from 'scenes/instance/SystemStatus/OverviewTab' +import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { InternalMetricsTab } from 'scenes/instance/SystemStatus/InternalMetricsTab' +import { OverviewTab } from 'scenes/instance/SystemStatus/OverviewTab' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' -import { InstanceConfigTab } from './InstanceConfigTab' import { userLogic } from 'scenes/userLogic' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { StaffUsersTab } from './StaffUsersTab' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' + +import { InstanceConfigTab } from './InstanceConfigTab' import { KafkaInspectorTab } from './KafkaInspectorTab' -import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Link } from '@posthog/lemon-ui' +import { StaffUsersTab } from './StaffUsersTab' +import { InstanceStatusTabName, systemStatusLogic } from './systemStatusLogic' export const scene: SceneExport = { component: SystemStatus, @@ -105,44 +105,35 @@ export function SystemStatus(): JSX.Element { } /> - {error && ( - An unknown error occurred. Please try again or contact us.} - type="error" - showIcon - /> - )} - {siteUrlMisconfigured && ( - - Your SITE_URL environment variable seems misconfigured. Your{' '} - SITE_URL is set to{' '} - - {preflight?.site_url} - {' '} - but you're currently browsing this page from{' '} - - {window.location.origin} - - . In order for PostHog to work properly, please set this to the origin where your instance - is hosted.{' '} - - Learn more - - - } - showIcon - type="warning" - style={{ marginBottom: 32 }} - /> - )} +
    + {error && ( + +
    Something went wrong
    +
    {error || 'An unknown error occurred. Please try again or contact us.'}
    +
    + )} + {siteUrlMisconfigured && ( + + Your SITE_URL environment variable seems misconfigured. Your SITE_URL{' '} + is set to{' '} + + {preflight?.site_url} + {' '} + but you're currently browsing this page from{' '} + + {window.location.origin} + + . In order for PostHog to work properly, please set this to the origin where your instance is + hosted. + + )} +
    diff --git a/frontend/src/scenes/instance/SystemStatus/kafkaInspectorLogic.ts b/frontend/src/scenes/instance/SystemStatus/kafkaInspectorLogic.ts index f10afcd1dd3e9..3fcb9422d906c 100644 --- a/frontend/src/scenes/instance/SystemStatus/kafkaInspectorLogic.ts +++ b/frontend/src/scenes/instance/SystemStatus/kafkaInspectorLogic.ts @@ -1,8 +1,9 @@ import { actions, kea, path } from 'kea' -import api from 'lib/api' -import type { kafkaInspectorLogicType } from './kafkaInspectorLogicType' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import type { kafkaInspectorLogicType } from './kafkaInspectorLogicType' export interface KafkaMessage { topic: string partition: number diff --git a/frontend/src/scenes/instance/SystemStatus/staffUsersLogic.ts b/frontend/src/scenes/instance/SystemStatus/staffUsersLogic.ts index 1f59748f42345..d246472c51228 100644 --- a/frontend/src/scenes/instance/SystemStatus/staffUsersLogic.ts +++ b/frontend/src/scenes/instance/SystemStatus/staffUsersLogic.ts @@ -1,10 +1,12 @@ +import { actions, connect, events, kea, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, events } from 'kea' import { router } from 'kea-router' import api from 'lib/api' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + import { UserType } from '~/types' + import type { staffUsersLogicType } from './staffUsersLogicType' export const staffUsersLogic = kea([ diff --git a/frontend/src/scenes/instance/SystemStatus/systemStatusLogic.ts b/frontend/src/scenes/instance/SystemStatus/systemStatusLogic.ts index 2a17736df79c8..a3fde5d7610ee 100644 --- a/frontend/src/scenes/instance/SystemStatus/systemStatusLogic.ts +++ b/frontend/src/scenes/instance/SystemStatus/systemStatusLogic.ts @@ -1,14 +1,16 @@ -import { actionToUrl, urlToAction } from 'kea-router' +import { actions, events, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' +import { actionToUrl, urlToAction } from 'kea-router' import api from 'lib/api' -import { kea, path, actions, reducers, selectors, listeners, events } from 'kea' -import type { systemStatusLogicType } from './systemStatusLogicType' -import { userLogic } from 'scenes/userLogic' -import { SystemStatus, SystemStatusRow, SystemStatusQueriesResult, InstanceSetting } from '~/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { isUserLoggedIn } from 'lib/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { isUserLoggedIn } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { InstanceSetting, SystemStatus, SystemStatusQueriesResult, SystemStatusRow } from '~/types' + +import type { systemStatusLogicType } from './systemStatusLogicType' export enum ConfigMode { View = 'view', diff --git a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.scss b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.scss index 2ee530fe2d87a..8f90c06671cf0 100644 --- a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.scss +++ b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.scss @@ -5,7 +5,7 @@ // Weird hack - this fixes chrome from not correctly identifying the bounds of the component for the drag preview // https://github.com/react-dnd/react-dnd/issues/832#issuecomment-442071628 transform: translate3d(0, 0, 0); - outline: 1px solid var(--primary); + outline: 1px solid var(--primary-3000); background-color: var(--bg-light); } diff --git a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx index 27544da05c561..a1f1fae053850 100644 --- a/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx +++ b/frontend/src/scenes/notebooks/AddToNotebook/DraggableToNotebook.tsx @@ -1,12 +1,15 @@ -import React, { useState } from 'react' -import { NotebookNodeType } from '~/types' import './DraggableToNotebook.scss' -import { useActions, useValues } from 'kea' + import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { useNotebookNode } from '../Nodes/notebookNodeLogic' +import React, { useState } from 'react' + +import { NotebookNodeType } from '~/types' + +import { useNotebookNode } from '../Nodes/NotebookNodeContext' import { notebookPanelLogic } from '../NotebookPanel/notebookPanelLogic' export type DraggableToNotebookBaseProps = { diff --git a/frontend/src/scenes/notebooks/IconNotebook.tsx b/frontend/src/scenes/notebooks/IconNotebook.tsx index a534044d3e6b7..bdc4a14becc46 100644 --- a/frontend/src/scenes/notebooks/IconNotebook.tsx +++ b/frontend/src/scenes/notebooks/IconNotebook.tsx @@ -1,6 +1,6 @@ +import { IconNotebook as IconNotebook3000 } from '@posthog/icons' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconNotebook as IconNotebookLegacy, LemonIconProps } from 'lib/lemon-ui/icons' -import { IconNotebook as IconNotebook3000 } from '@posthog/icons' export function IconNotebook(props: LemonIconProps): JSX.Element { const is3000 = useFeatureFlag('POSTHOG_3000') diff --git a/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx b/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx index f50e1290734a2..5af0f8ed1d7d6 100644 --- a/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx +++ b/frontend/src/scenes/notebooks/Marks/NotebookMarkLink.tsx @@ -1,7 +1,8 @@ -import { Mark, getMarkRange, mergeAttributes } from '@tiptap/core' -import { linkPasteRule } from '../Nodes/utils' +import { getMarkRange, Mark, mergeAttributes } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' +import { linkPasteRule } from '../Nodes/utils' + export const NotebookMarkLink = Mark.create({ name: 'link', priority: 1000, diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss index a3b1acc1db115..1d4fa2d2021a3 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss @@ -56,7 +56,7 @@ } &--selected { - --border-color: var(--primary-3000); + --border-color: var(--border-bold); } &--auto-hide-metadata { @@ -77,6 +77,7 @@ &:hover, &.NotebookNode--selected { border-color: var(--border-color); + .NotebookNode__meta { pointer-events: all; visibility: visible; @@ -108,6 +109,7 @@ &--editable { border-radius: var(--radius); transition: background-color 150ms linear; + &:hover { background-color: var(--border); } diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index 34f70d6c85f25..d2c96c06ee054 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -25,7 +25,7 @@ import { notebookLogic } from '../Notebook/notebookLogic' import { useInView } from 'react-intersection-observer' import { NotebookNodeResource } from '~/types' import { ErrorBoundary } from '~/layout/ErrorBoundary' -import { NotebookNodeContext, NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' +import { NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' import { posthogNodePasteRule, useSyncedAttributes } from './utils' import { KNOWN_NODES, @@ -39,6 +39,7 @@ import { NotebookNodeTitle } from './components/NotebookNodeTitle' import { notebookNodeLogicType } from './notebookNodeLogicType' import { SlashCommandsPopover } from '../Notebook/SlashCommands' import posthog from 'posthog-js' +import { NotebookNodeContext } from './NotebookNodeContext' import { IconGear } from '@posthog/icons' function NodeWrapper(props: NodeWrapperProps): JSX.Element { @@ -74,7 +75,17 @@ function NodeWrapper(props: NodeWrapperP // nodeId can start null, but should then immediately be generated const nodeLogic = useMountedLogic(notebookNodeLogic(logicProps)) const { resizeable, expanded, actions, nodeId } = useValues(nodeLogic) - const { setExpanded, deleteNode, toggleEditing, insertOrSelectNextLine } = useActions(nodeLogic) + const { setRef, setExpanded, deleteNode, toggleEditing, insertOrSelectNextLine } = useActions(nodeLogic) + + const { ref: inViewRef, inView } = useInView({ triggerOnce: true }) + + const setRefs = useCallback( + (node) => { + setRef(node) + inViewRef(node) + }, + [inViewRef] + ) useEffect(() => { // TRICKY: child nodes mount the parent logic so we need to control the mounting / unmounting directly in this component @@ -91,7 +102,6 @@ function NodeWrapper(props: NodeWrapperP mountedNotebookLogic, }) - const [ref, inView] = useInView({ triggerOnce: true }) const contentRef = useRef(null) // If resizeable is true then the node attr "height" is required @@ -135,7 +145,7 @@ function NodeWrapper(props: NodeWrapperP
    { > openNotebook(shortId, NotebookTarget.Popover)} + onClick={() => void openNotebook(shortId, NotebookTarget.Popover)} target={undefined} className="space-x-1" > diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx index 60fa028e0814a..fd5aad3420b5e 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeCohort.tsx @@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react' import clsx from 'clsx' import { NotFound } from 'lib/components/NotFound' import { cohortEditLogic } from 'scenes/cohorts/cohortEditLogic' -import { IconCohort, IconPerson, InsightsTrendsIcon } from 'lib/lemon-ui/icons' +import { IconPeople, IconPerson, IconTrends } from '@posthog/icons' import { Query } from '~/queries/Query/Query' import { LemonDivider, LemonTag } from '@posthog/lemon-ui' import { DataTableNode, NodeKind } from '~/queries/schema' @@ -71,7 +71,7 @@ const Component = ({ attributes }: NotebookNodeProps, + icon: , onClick: () => { setExpanded(false) insertAfter({ @@ -130,7 +130,7 @@ const Component = ({ attributes }: NotebookNodeProps ) : (
    - + {cohort.name} ({cohort.count} persons) {cohort.is_static ? 'Static' : 'Dynamic'} diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts b/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts new file mode 100644 index 0000000000000..d5db3c5035793 --- /dev/null +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeContext.ts @@ -0,0 +1,10 @@ +import { BuiltLogic } from 'kea' +import { createContext, useContext } from 'react' +import type { notebookNodeLogicType } from './notebookNodeLogicType' + +export const NotebookNodeContext = createContext | undefined>(undefined) + +// Currently there is no way to optionally get bound logics so this context allows us to maybe get a logic if it is "bound" via the provider +export const useNotebookNode = (): BuiltLogic | undefined => { + return useContext(NotebookNodeContext) +} diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx index 94304f7f7e2f4..1d220f64c2214 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx @@ -131,7 +131,7 @@ export const NotebookNodeEarlyAccessFeature = createPostHogWidgetNode { - return { id: match[1] as EarlyAccessFeatureLogicProps['id'] } + return { id: match[1] } }, }, }) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx index d54a338c14b5d..fa6c83a5c868b 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx @@ -92,7 +92,7 @@ const Component = ({ attributes }: NotebookNodeProps + )}
    ) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/EventIcon.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/EventIcon.tsx index abab74d367546..5551f28fe5962 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/EventIcon.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/EventIcon.tsx @@ -1,9 +1,9 @@ -import { EventType } from '~/types' - import { Tooltip } from '@posthog/lemon-ui' -import { IconAdsClick, IconExclamation, IconEyeHidden, IconEyeVisible, IconCode } from 'lib/lemon-ui/icons' +import { IconAdsClick, IconCode, IconExclamation, IconEyeHidden, IconEyeVisible } from 'lib/lemon-ui/icons' import { KEY_MAPPING } from 'lib/taxonomy' +import { EventType } from '~/types' + type EventIconProps = { event: EventType } export const EventIcon = ({ event }: EventIconProps): JSX.Element => { diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed.tsx index b5c4ee5c2fd71..6b226f53c41ef 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed.tsx @@ -1,10 +1,11 @@ -import { useValues } from 'kea' - import { LemonSkeleton } from '@posthog/lemon-ui' +import { useValues } from 'kea' import { NotFound } from 'lib/components/NotFound' -import { NotebookNodeType, PersonType } from '~/types' import { NotebookNodeProps } from 'scenes/notebooks/Notebook/utils' import { personLogic } from 'scenes/persons/personLogic' + +import { NotebookNodeType, PersonType } from '~/types' + import { createPostHogWidgetNode } from '../NodeWrapper' import { notebookNodePersonFeedLogic } from './notebookNodePersonFeedLogic' import { Session } from './Session' diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/Session.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/Session.tsx index d3f346ae2d728..799c9561c4180 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/Session.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/Session.tsx @@ -1,15 +1,16 @@ -import { useState } from 'react' -import { useActions, useValues } from 'kea' - -import { LemonButton } from '@posthog/lemon-ui' import { IconRewindPlay } from '@posthog/icons' +import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { dayjs } from 'lib/dayjs' -// import { TimelineEntry } from '~/queries/schema' -import { NotebookNodeType } from '~/types' import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { humanFriendlyDetailedTime, humanFriendlyDuration } from 'lib/utils' -import { SessionEvent } from './SessionEvent' +import { useState } from 'react' + +// import { TimelineEntry } from '~/queries/schema' +import { NotebookNodeType } from '~/types' + import { notebookNodeLogic } from '../notebookNodeLogic' +import { SessionEvent } from './SessionEvent' type SessionProps = { session: any // TimelineEntry diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/SessionEvent.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/SessionEvent.tsx index 00131544c5662..ddb16a428c1ce 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/SessionEvent.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/SessionEvent.tsx @@ -1,6 +1,8 @@ -import { EventType } from '~/types' -import { eventToDescription } from 'lib/utils' import { dayjs } from 'lib/dayjs' +import { eventToDescription } from 'lib/utils' + +import { EventType } from '~/types' + import { EventIcon } from './EventIcon' type SessionEventProps = { event: EventType } diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/notebookNodePersonFeedLogic.ts b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/notebookNodePersonFeedLogic.ts index 43649f6573a38..0f500ba932df8 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/notebookNodePersonFeedLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePersonFeed/notebookNodePersonFeedLogic.ts @@ -1,4 +1,4 @@ -import { kea, key, path, props, afterMount } from 'kea' +import { afterMount, kea, key, path, props } from 'kea' import { loaders } from 'kea-loaders' import { query } from '~/queries/query' diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.scss b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.scss deleted file mode 100644 index d9f57009fa37b..0000000000000 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import '../../../styles/mixins'; - -// Here we override based on NotebookNode the ph-query styling, so -// as to not change the global styling. We need the extra nesting to ensure we -// are more specific than the other insights css - -.NotebookNode.ph-query { - .insights-graph-container { - .ant-card-body { - padding: 0; - } - - .RetentionContainer { - .LineGraph { - position: relative; - } - } - } - - .funnel-insights-container { - &.non-empty-state { - min-height: initial; - } - } -} diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx index e567131b4d6d7..7f1035fab9bfa 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx @@ -11,7 +11,6 @@ import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' import { urls } from 'scenes/urls' -import './NotebookNodeQuery.scss' import { insightDataLogic } from 'scenes/insights/insightDataLogic' import { insightLogic } from 'scenes/insights/insightLogic' import { JSONContent } from '@tiptap/core' @@ -85,9 +84,7 @@ const Component = ({ attributes }: NotebookNodeProps +
    attrs.query.kind === NodeKind.DataTableNode, + resizeable: true, startExpanded: true, attributes: { query: { diff --git a/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx b/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx index 4e7f6e2a2b045..33a473a06edea 100644 --- a/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx +++ b/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx @@ -1,10 +1,12 @@ -import { KeyboardEvent } from 'react' +import { LemonInput, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { notebookNodeLogic } from '../notebookNodeLogic' +import posthog from 'posthog-js' +import { KeyboardEvent } from 'react' import { useEffect, useState } from 'react' -import { LemonInput, Tooltip } from '@posthog/lemon-ui' import { notebookLogic } from 'scenes/notebooks/Notebook/notebookLogic' +import { notebookNodeLogic } from '../notebookNodeLogic' + export function NotebookNodeTitle(): JSX.Element { const { isEditable } = useValues(notebookLogic) const { nodeAttributes, title, titlePlaceholder } = useValues(notebookNodeLogic) @@ -21,6 +23,10 @@ export function NotebookNodeTitle(): JSX.Element { title: newValue ?? undefined, }) + if (title != newValue) { + posthog.capture('notebook node title updated') + } + setEditing(false) } @@ -42,7 +48,10 @@ export function NotebookNodeTitle(): JSX.Element { setEditing(true)} + onDoubleClick={() => { + setEditing(true) + posthog.capture('notebook editing node title') + }} > {title} diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index e7b397f5e9d50..e49cd38e1042d 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -13,7 +13,6 @@ import { selectors, } from 'kea' import type { notebookNodeLogicType } from './notebookNodeLogicType' -import { createContext, useContext } from 'react' import { notebookLogicType } from '../Notebook/notebookLogicType' import { CustomNotebookNodeAttributes, @@ -64,6 +63,7 @@ export const notebookNodeLogic = kea([ initializeNode: true, setMessageListeners: (listeners: NotebookNodeMessagesListeners) => ({ listeners }), setTitlePlaceholder: (titlePlaceholder: string) => ({ titlePlaceholder }), + setRef: (ref: HTMLElement | null) => ({ ref }), }), connect((props: NotebookNodeLogicProps) => ({ @@ -72,6 +72,13 @@ export const notebookNodeLogic = kea([ })), reducers(({ props }) => ({ + ref: [ + null as HTMLElement | null, + { + setRef: (_, { ref }) => ref, + unregisterNodeLogic: () => null, + }, + ], expanded: [ props.startExpanded ?? true, { @@ -247,7 +254,9 @@ export const notebookNodeLogic = kea([ props.updateAttributes(attributes) }, toggleEditing: ({ visible }) => { - const shouldShowThis = typeof visible === 'boolean' ? visible : !values.notebookLogic.values.editingNodeId + const shouldShowThis = + typeof visible === 'boolean' ? visible : values.notebookLogic.values.editingNodeId !== values.nodeId + props.notebookLogic.actions.setEditingNodeId(shouldShowThis ? values.nodeId : null) }, initializeNode: () => { @@ -265,7 +274,7 @@ export const notebookNodeLogic = kea([ }, })), - afterMount(async (logic) => { + afterMount((logic) => { const { props, actions, values } = logic props.notebookLogic.actions.registerNodeLogic(values.nodeId, logic as any) @@ -281,10 +290,3 @@ export const notebookNodeLogic = kea([ props.notebookLogic.actions.unregisterNodeLogic(values.nodeId) }), ]) - -export const NotebookNodeContext = createContext | undefined>(undefined) - -// Currently there is no way to optionally get bound logics so this context allows us to maybe get a logic if it is "bound" via the provider -export const useNotebookNode = (): BuiltLogic | undefined => { - return useContext(NotebookNodeContext) -} diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx index bae565e55ce49..d60eb6ac8e827 100644 --- a/frontend/src/scenes/notebooks/Nodes/utils.tsx +++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx @@ -28,7 +28,8 @@ export function posthogNodePasteRule(options: { handler: ({ match, chain, range }) => { if (match.input) { chain().deleteRange(range).run() - Promise.resolve(options.getAttributes(match)).then((attributes) => { + + void Promise.resolve(options.getAttributes(match)).then((attributes) => { if (attributes) { options.editor.commands.insertContent({ type: options.type.name, diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index be73659b7c7e0..287351779a53f 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -1,47 +1,46 @@ -import posthog from 'posthog-js' -import { useActions, useValues } from 'kea' -import { useCallback, useMemo, useRef } from 'react' - +import { lemonToast } from '@posthog/lemon-ui' import { Editor as TTEditor } from '@tiptap/core' -import { EditorContent, useEditor } from '@tiptap/react' +import ExtensionDocument from '@tiptap/extension-document' import { FloatingMenu } from '@tiptap/extension-floating-menu' -import StarterKit from '@tiptap/starter-kit' import ExtensionPlaceholder from '@tiptap/extension-placeholder' -import ExtensionDocument from '@tiptap/extension-document' import TaskItem from '@tiptap/extension-task-item' import TaskList from '@tiptap/extension-task-list' +import { EditorContent, useEditor } from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import { useActions, useValues } from 'kea' +import { sampleOne } from 'lib/utils' +import posthog from 'posthog-js' +import { useCallback, useMemo, useRef } from 'react' -import { NotebookNodeFlagCodeExample } from '../Nodes/NotebookNodeFlagCodeExample' -import { NotebookNodeFlag } from '../Nodes/NotebookNodeFlag' -import { NotebookNodeExperiment } from '../Nodes/NotebookNodeExperiment' -import { NotebookNodeQuery } from '../Nodes/NotebookNodeQuery' -import { NotebookNodeRecording } from '../Nodes/NotebookNodeRecording' -import { NotebookNodePlaylist } from '../Nodes/NotebookNodePlaylist' -import { NotebookNodePerson } from '../Nodes/NotebookNodePerson' -import { NotebookNodeBacklink } from '../Nodes/NotebookNodeBacklink' -import { NotebookNodeReplayTimestamp } from '../Nodes/NotebookNodeReplayTimestamp' -import { NotebookMarkLink } from '../Marks/NotebookMarkLink' -import { insertionSuggestionsLogic } from '../Suggestions/insertionSuggestionsLogic' -import { FloatingSuggestions } from '../Suggestions/FloatingSuggestions' -import { lemonToast } from '@posthog/lemon-ui' import { NotebookNodeType } from '~/types' -import { NotebookNodeImage } from '../Nodes/NotebookNodeImage' -import { EditorFocusPosition, EditorRange, JSONContent, Node, textContent } from './utils' -import { SlashCommandsExtension } from './SlashCommands' +import { NotebookMarkLink } from '../Marks/NotebookMarkLink' +import { NotebookNodeBacklink } from '../Nodes/NotebookNodeBacklink' +import { NotebookNodeCohort } from '../Nodes/NotebookNodeCohort' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' -import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey' -import { InlineMenu } from './InlineMenu' -import { notebookLogic } from './notebookLogic' -import { sampleOne } from 'lib/utils' +import { NotebookNodeEmbed } from '../Nodes/NotebookNodeEmbed' +import { NotebookNodeExperiment } from '../Nodes/NotebookNodeExperiment' +import { NotebookNodeFlag } from '../Nodes/NotebookNodeFlag' +import { NotebookNodeFlagCodeExample } from '../Nodes/NotebookNodeFlagCodeExample' import { NotebookNodeGroup } from '../Nodes/NotebookNodeGroup' -import { NotebookNodeCohort } from '../Nodes/NotebookNodeCohort' +import { NotebookNodeImage } from '../Nodes/NotebookNodeImage' +import { NotebookNodeMap } from '../Nodes/NotebookNodeMap' +import { NotebookNodeMention } from '../Nodes/NotebookNodeMention' +import { NotebookNodePerson } from '../Nodes/NotebookNodePerson' import { NotebookNodePersonFeed } from '../Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed' +import { NotebookNodePlaylist } from '../Nodes/NotebookNodePlaylist' import { NotebookNodeProperties } from '../Nodes/NotebookNodeProperties' -import { NotebookNodeMap } from '../Nodes/NotebookNodeMap' +import { NotebookNodeQuery } from '../Nodes/NotebookNodeQuery' +import { NotebookNodeRecording } from '../Nodes/NotebookNodeRecording' +import { NotebookNodeReplayTimestamp } from '../Nodes/NotebookNodeReplayTimestamp' +import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey' +import { FloatingSuggestions } from '../Suggestions/FloatingSuggestions' +import { insertionSuggestionsLogic } from '../Suggestions/insertionSuggestionsLogic' +import { InlineMenu } from './InlineMenu' import { MentionsExtension } from './MentionsExtension' -import { NotebookNodeMention } from '../Nodes/NotebookNodeMention' -import { NotebookNodeEmbed } from '../Nodes/NotebookNodeEmbed' +import { notebookLogic } from './notebookLogic' +import { SlashCommandsExtension } from './SlashCommands' +import { EditorFocusPosition, EditorRange, JSONContent, Node, textContent } from './utils' const CustomDocument = ExtensionDocument.extend({ content: 'heading block*', diff --git a/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx b/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx index 1eb952e410ee1..d0d31cd04377c 100644 --- a/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx +++ b/frontend/src/scenes/notebooks/Notebook/InlineMenu.tsx @@ -4,6 +4,7 @@ import { BubbleMenu } from '@tiptap/react' import { IconBold, IconDelete, IconItalic, IconLink, IconOpenInNew } from 'lib/lemon-ui/icons' import { isURL } from 'lib/utils' import { useRef } from 'react' + import NotebookIconHeading from './NotebookIconHeading' export const InlineMenu = ({ editor }: { editor: Editor }): JSX.Element => { diff --git a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx index c6b90ee6a781b..d5c98de3f94ac 100644 --- a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx +++ b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx @@ -1,17 +1,18 @@ +import { LemonButton, ProfilePicture } from '@posthog/lemon-ui' import { Extension } from '@tiptap/core' -import Suggestion from '@tiptap/suggestion' - +import { PluginKey } from '@tiptap/pm/state' import { ReactRenderer } from '@tiptap/react' -import { LemonButton, ProfilePicture } from '@posthog/lemon-ui' -import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' -import { EditorRange } from './utils' -import { Popover } from 'lib/lemon-ui/Popover' +import Suggestion from '@tiptap/suggestion' import Fuse from 'fuse.js' import { useValues } from 'kea' -import { notebookLogic } from './notebookLogic' +import { Popover } from 'lib/lemon-ui/Popover' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { membersLogic } from 'scenes/organization/membersLogic' + import { NotebookNodeType, OrganizationMemberType } from '~/types' -import { PluginKey } from '@tiptap/pm/state' + +import { notebookLogic } from './notebookLogic' +import { EditorRange } from './utils' type MentionsProps = { range: EditorRange @@ -122,7 +123,7 @@ export const Mentions = forwardRef(function SlashCom status="primary-alt" icon={} active={index === selectedIndex} - onClick={async () => await execute(member)} + onClick={() => void execute(member)} > {`${member.user.first_name} <${member.user.email}>`} diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss index cd63e10b35fbe..9b0139e499d63 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss @@ -30,10 +30,12 @@ > .is-empty::before { content: attr(data-placeholder); float: left; - color: rgba(0, 0, 0, 0.2); + color: rgb(0 0 0 / 20%); + [theme='dark'] & { - color: rgba(255, 255, 255, 0.2); + color: rgb(255 255 255 / 20%); } + pointer-events: none; height: 0; } @@ -48,7 +50,7 @@ > ul[data-type='taskList'] { list-style-type: none; - padding-left: 0px; + padding-left: 0; li { display: flex; @@ -86,7 +88,7 @@ } > pre { - background-color: rgba(0, 0, 0, 0.05); + background-color: rgb(0 0 0 / 5%); border-radius: var(--radius); overflow-x: auto; margin-bottom: 0.5rem; @@ -95,7 +97,7 @@ > code, > p code { - background-color: rgba(0, 0, 0, 0.05); + background-color: rgb(0 0 0 / 5%); border-radius: var(--radius); padding: 0.2rem; } @@ -145,17 +147,9 @@ } } - &--editable { - .NotebookEditor .ProseMirror { - // Add some padding to help clicking below the last element - padding-bottom: 10rem; - flex: 1; - } - } - .NotebookColumn { position: relative; - width: 0px; + width: 0; transition: width var(--notebook-popover-transition-properties); --notebook-sidebar-height: calc(100vh - 9rem); @@ -163,8 +157,7 @@ .NotebookColumn__content { position: sticky; align-self: flex-start; - top: 0px; - + top: 0; transform: translateX(-100%); transition: transform var(--notebook-popover-transition-properties); } @@ -190,6 +183,11 @@ .NotebookColumn__content { width: var(--notebook-column-left-width); transform: translateX(-100%); + + > .LemonWidget .LemonWidget__content { + max-height: var(--notebook-sidebar-height); + overflow: auto; + } } } @@ -217,28 +215,36 @@ } } + &--editable { + .NotebookEditor .ProseMirror { + // Add some padding to help clicking below the last element + padding-bottom: 10rem; + flex: 1; + } + + .NotebookColumn--left.NotebookColumn--showing { + & + .NotebookEditor { + .ProseMirror { + // Add a lot of padding to allow the entire column to always be on screen + padding-bottom: 100vh; + } + } + } + } + .NotebookHistory { flex: 1; display: flex; flex-direction: column; - height: var(--notebook-sidebar-height); - overflow: hidden; } .NotebookInlineMenu { margin-bottom: -0.2rem; - box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 10px 0 rgb(0 0 0 / 10%); .LemonInput { - border: 0px; - min-height: 0px; - } - } - - .NotebookColumnLeft__widget { - > .LemonWidget__content { - max-height: calc(100vh - 220px); - overflow: auto; + border: 0; + min-height: 0; } } @@ -261,6 +267,15 @@ // overriding ::selection is necessary here because // antd makes it invisible otherwise span::selection { - color: var(--primary); + color: var(--primary-3000); + } + + // Overrides for insight controls + + .InsightVizDisplay { + .InsightDisplayConfig { + padding: 0; + border-bottom-width: 0; + } } } diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx index 6e00e37d2652e..e67b51d7ad639 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx @@ -1,12 +1,14 @@ import { Meta, StoryFn } from '@storybook/react' -import { useEffect } from 'react' -import { mswDecorator } from '~/mocks/browser' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' +import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' +import { NotebookType } from '~/types' + import notebook12345Json from './__mocks__/notebook-12345.json' import { notebookTestTemplate } from './__mocks__/notebook-template-for-snapshot' -import { NotebookType } from '~/types' // a list of test cases to run, showing different types of content in notebooks const testCases: Record = { diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.tsx index 9b65abb1ddf06..4b896c1cdb989 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.tsx @@ -1,23 +1,25 @@ -import { useEffect } from 'react' -import { NotebookLogicProps, notebookLogic } from 'scenes/notebooks/Notebook/notebookLogic' -import { BindLogic, useActions, useValues } from 'kea' import './Notebook.scss' -import { NotFound } from 'lib/components/NotFound' import clsx from 'clsx' -import { notebookSettingsLogic } from './notebookSettingsLogic' +import { BindLogic, useActions, useValues } from 'kea' +import { NotFound } from 'lib/components/NotFound' +import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { useWhyDidIRender } from 'lib/hooks/useWhyDidIRender' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { useEffect } from 'react' +import { notebookLogic, NotebookLogicProps } from 'scenes/notebooks/Notebook/notebookLogic' + +import { ErrorBoundary } from '~/layout/ErrorBoundary' import { SCRATCHPAD_NOTEBOOK } from '~/models/notebooksModel' -import { NotebookConflictWarning } from './NotebookConflictWarning' -import { NotebookLoadingState } from './NotebookLoadingState' + import { Editor } from './Editor' -import { EditorFocusPosition, JSONContent } from './utils' import { NotebookColumnLeft } from './NotebookColumnLeft' -import { ErrorBoundary } from '~/layout/ErrorBoundary' -import { NotebookHistoryWarning } from './NotebookHistory' -import { useWhyDidIRender } from 'lib/hooks/useWhyDidIRender' import { NotebookColumnRight } from './NotebookColumnRight' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { NotebookConflictWarning } from './NotebookConflictWarning' +import { NotebookHistoryWarning } from './NotebookHistory' +import { NotebookLoadingState } from './NotebookLoadingState' +import { notebookSettingsLogic } from './notebookSettingsLogic' +import { EditorFocusPosition, JSONContent } from './utils' export type NotebookProps = NotebookLogicProps & { initialAutofocus?: EditorFocusPosition diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx index e952c0cb8fd48..871f352760c89 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx @@ -1,11 +1,12 @@ -import { LemonWidget } from 'lib/lemon-ui/LemonWidget' -import { BuiltLogic, useActions, useValues } from 'kea' +import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' -import { notebookLogic } from './notebookLogic' +import { BuiltLogic, useActions, useValues } from 'kea' +import { LemonWidget } from 'lib/lemon-ui/LemonWidget' +import { useEffect, useRef, useState } from 'react' + import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' -import { LemonButton } from '@posthog/lemon-ui' -import { IconEyeVisible } from 'lib/lemon-ui/icons' import { NotebookHistory } from './NotebookHistory' +import { notebookLogic } from './notebookLogic' export const NotebookColumnLeft = (): JSX.Element | null => { const { editingNodeLogic, isShowingLeftColumn, showHistory } = useValues(notebookLogic) @@ -16,7 +17,7 @@ export const NotebookColumnLeft = (): JSX.Element | null => { 'NotebookColumn--showing': isShowingLeftColumn, })} > -
    + {editingNodeLogic ? : null}
    {isShowingLeftColumn ? ( editingNodeLogic ? ( @@ -30,6 +31,41 @@ export const NotebookColumnLeft = (): JSX.Element | null => { ) } +export const NotebookNodeSettingsOffset = ({ logic }: { logic: BuiltLogic }): JSX.Element => { + const { ref } = useValues(logic) + const offsetRef = useRef(null) + const [height, setHeight] = useState(0) + + useEffect(() => { + // Interval to check the relative positions of the node and the offset div + // updating the height so that it always is inline + const updateHeight = (): void => { + if (ref && offsetRef.current) { + const newHeight = ref.getBoundingClientRect().top - offsetRef.current.getBoundingClientRect().top + + if (height !== newHeight) { + setHeight(newHeight) + } + } + } + + const interval = setInterval(updateHeight, 100) + updateHeight() + + return () => clearInterval(interval) + }, [ref, offsetRef.current, height]) + + return ( +
    + ) +} + export const NotebookNodeSettingsWidget = ({ logic }: { logic: BuiltLogic }): JSX.Element => { const { setEditingNodeId } = useActions(notebookLogic) const { Settings, nodeAttributes, title } = useValues(logic) @@ -41,16 +77,21 @@ export const NotebookNodeSettingsWidget = ({ logic }: { logic: BuiltLogic - } size="small" status="primary" onClick={() => selectNode()} /> setEditingNodeId(null)}> Done } > - {Settings ? ( - - ) : null} +
    selectNode()}> + {Settings ? ( + + ) : null} +
    ) } diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookColumnRight.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookColumnRight.tsx index 6ccb797affb76..82304503766f2 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookColumnRight.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookColumnRight.tsx @@ -1,10 +1,11 @@ -import { BuiltLogic, useValues } from 'kea' import clsx from 'clsx' -import { notebookLogic } from './notebookLogic' -import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' -import { NotebookNodeChildRenderer } from '../Nodes/NodeWrapper' +import { BuiltLogic, useValues } from 'kea' import { uuid } from 'lib/utils' +import { NotebookNodeChildRenderer } from '../Nodes/NodeWrapper' +import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' +import { notebookLogic } from './notebookLogic' + export const NotebookColumnRight = (): JSX.Element | null => { const { isShowingLeftColumn, nodeLogicsWithChildren } = useValues(notebookLogic) const isShowing = nodeLogicsWithChildren.length && !isShowingLeftColumn diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookConflictWarning.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookConflictWarning.tsx index 5151f746a87e9..e68db1137ea5b 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookConflictWarning.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookConflictWarning.tsx @@ -1,5 +1,6 @@ import { useActions } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' + import { notebookLogic } from './notebookLogic' export function NotebookConflictWarning(): JSX.Element { diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx index 5837493dc6c91..2ce11249bf535 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookHistory.tsx @@ -1,21 +1,22 @@ -import { useActions, useValues } from 'kea' -import { notebookLogic } from './notebookLogic' -import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { TZLabel } from '@posthog/apps-common' import { LemonBanner, LemonButton, LemonSkeleton, + lemonToast, LemonWidget, PaginationControl, ProfilePicture, - lemonToast, usePagination, } from '@posthog/lemon-ui' import { JSONContent } from '@tiptap/core' +import { useActions, useValues } from 'kea' import { activityLogLogic } from 'lib/components/ActivityLog/activityLogLogic' -import { TZLabel } from '@posthog/apps-common' +import { ActivityLogItem, ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' import { useMemo } from 'react' +import { notebookLogic } from './notebookLogic' + const getFieldChange = (logItem: ActivityLogItem, field: string): any => { return logItem.detail.changes?.find((x) => x.field === field)?.after } diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookListMini.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookListMini.tsx index 987d55de03190..cc7f0e525e85e 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookListMini.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookListMini.tsx @@ -1,10 +1,12 @@ import { LemonButton } from '@posthog/lemon-ui' import { useValues } from 'kea' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' + import { notebooksModel } from '~/models/notebooksModel' import { NotebookListItemType } from '~/types' -import { NotebookSelectPopover } from '../NotebookSelectButton/NotebookSelectButton' + import { IconNotebook } from '../IconNotebook' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { NotebookSelectPopover } from '../NotebookSelectButton/NotebookSelectButton' export type NotebookListMiniProps = { selectedNotebookId?: string diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookMeta.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookMeta.tsx index a3da6c0b0c11c..ebd680cb47f35 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookMeta.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookMeta.tsx @@ -1,11 +1,13 @@ -import { NotebookLogicProps, notebookLogic } from './notebookLogic' +import { LemonButton, LemonButtonProps } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { IconDocumentExpand } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { useActions, useValues } from 'kea' import { useCallback, useEffect, useState } from 'react' + import { NotebookSyncStatus } from '~/types' -import { LemonButton, LemonButtonProps } from '@posthog/lemon-ui' -import { IconDocumentExpand } from 'lib/lemon-ui/icons' + +import { notebookLogic, NotebookLogicProps } from './notebookLogic' import { notebookSettingsLogic } from './notebookSettingsLogic' const syncStatusMap: Record = { diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx index c40031ad20dfc..5658ddbf7e5cb 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx @@ -2,7 +2,7 @@ import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui' import { combineUrl } from 'kea-router' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import posthog from 'posthog-js' import { useState } from 'react' import { urls } from 'scenes/urls' @@ -31,7 +31,7 @@ export function NotebookShare({ shortId }: NotebookShareProps): JSX.Element { fullWidth center sideIcon={} - onClick={async () => await copyToClipboard(url, 'notebook link')} + onClick={() => void copyToClipboard(url, 'notebook link')} title={url} > {url} diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx index 878364f0d8abc..0a7bf012d8e19 100644 --- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx +++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx @@ -1,38 +1,38 @@ +import { + IconCursor, + IconFunnels, + IconHogQL, + IconLifecycle, + IconPeople, + IconRetention, + IconRewindPlay, + IconStickiness, + IconTrends, + IconUpload, + IconUserPaths, +} from '@posthog/icons' +import { IconCode } from '@posthog/icons' +import { LemonButton, LemonDivider, lemonToast } from '@posthog/lemon-ui' import { Extension } from '@tiptap/core' -import Suggestion from '@tiptap/suggestion' - import { ReactRenderer } from '@tiptap/react' -import { LemonButton, LemonDivider, lemonToast } from '@posthog/lemon-ui' -import { - IconBold, - IconCohort, - IconItalic, - IconRecording, - IconTableChart, - IconUploadFile, - InsightSQLIcon, - InsightsFunnelsIcon, - InsightsLifecycleIcon, - InsightsPathsIcon, - InsightsRetentionIcon, - InsightsStickinessIcon, - InsightsTrendsIcon, -} from 'lib/lemon-ui/icons' -import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' -import { EditorCommands, EditorRange } from './utils' -import { BaseMathType, ChartDisplayType, FunnelVizType, NotebookNodeType, PathType, RetentionPeriod } from '~/types' -import { Popover } from 'lib/lemon-ui/Popover' -import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' +import Suggestion from '@tiptap/suggestion' import Fuse from 'fuse.js' import { useValues } from 'kea' -import { notebookLogic } from './notebookLogic' -import { selectFile } from '../Nodes/utils' -import NotebookIconHeading from './NotebookIconHeading' -import { NodeKind } from '~/queries/schema' +import { IconBold, IconItalic } from 'lib/lemon-ui/icons' +import { Popover } from 'lib/lemon-ui/Popover' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' -import { buildInsightVizQueryContent, buildNodeQueryContent } from '../Nodes/NotebookNodeQuery' +import { NodeKind } from '~/queries/schema' +import { BaseMathType, ChartDisplayType, FunnelVizType, NotebookNodeType, PathType, RetentionPeriod } from '~/types' + import { buildNodeEmbed } from '../Nodes/NotebookNodeEmbed' -import { IconCode } from '@posthog/icons' +import { buildInsightVizQueryContent, buildNodeQueryContent } from '../Nodes/NotebookNodeQuery' +import { selectFile } from '../Nodes/utils' +import NotebookIconHeading from './NotebookIconHeading' +import { notebookLogic } from './notebookLogic' +import { EditorCommands, EditorRange } from './utils' type SlashCommandConditionalProps = | { @@ -99,8 +99,8 @@ const TEXT_CONTROLS: SlashCommandsItem[] = [ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'Trend', - search: 'trend insight', - icon: , + search: 'graph trend insight', + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -125,7 +125,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'Funnel', search: 'funnel insight', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -152,7 +152,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'Retention', search: 'retention insight', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -178,8 +178,8 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ }, { title: 'Paths', - search: 'paths insight', - icon: , + search: 'user paths insight', + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -194,7 +194,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'Stickiness', search: 'stickiness insight', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -215,7 +215,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'Lifecycle', search: 'lifecycle insight', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -235,7 +235,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ { title: 'HogQL', search: 'sql', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -267,7 +267,7 @@ order by count() desc { title: 'Events', search: 'data explore', - icon: , + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -284,9 +284,9 @@ order by count() desc ), }, { - title: 'Persons', - search: 'people users', - icon: , + title: 'People', + search: 'persons users', + icon: , command: (chain, pos) => chain.insertContentAt( pos, @@ -301,15 +301,15 @@ order by count() desc ), }, { - title: 'Session Replays', - search: 'recordings video', - icon: , + title: 'Session recordings', + search: 'video replay', + icon: , command: (chain, pos) => chain.insertContentAt(pos, { type: NotebookNodeType.RecordingPlaylist, attrs: {} }), }, { title: 'Image', - search: 'picture', - icon: , + search: 'picture gif', + icon: , command: async (chain, pos) => { // Trigger upload followed by insert try { @@ -464,7 +464,7 @@ export const SlashCommands = forwardRef(fu status="primary-alt" size="small" active={selectedIndex === -1 && selectedHorizontalIndex === index} - onClick={async () => await execute(item)} + onClick={() => void execute(item)} icon={item.icon} /> ))} @@ -479,7 +479,7 @@ export const SlashCommands = forwardRef(fu status="primary-alt" icon={item.icon} active={index === selectedIndex} - onClick={async () => await execute(item)} + onClick={() => void execute(item)} > {item.title}
    diff --git a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts index b87917836a5db..56c0cbe4586af 100644 --- a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts +++ b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts @@ -1,7 +1,8 @@ -import { NotebookType } from '~/types' import { MOCK_DEFAULT_BASIC_USER } from 'lib/api.mock' import { JSONContent } from 'scenes/notebooks/Notebook/utils' +import { NotebookType } from '~/types' + export const notebookTestTemplate = ( title: string = 'Notebook for snapshots', notebookJson: JSONContent[] diff --git a/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts b/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts index 80ec5b615fdb7..bd589a5803573 100644 --- a/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts +++ b/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts @@ -1,4 +1,5 @@ import { JSONContent } from '@tiptap/core' + import { NodeKind } from '~/queries/schema' import { NotebookNodeType, NotebookType } from '~/types' diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index e87521c26ad87..fd825533f93aa 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -1,6 +1,7 @@ +import { lemonToast } from '@posthog/lemon-ui' import { - BuiltLogic, actions, + BuiltLogic, connect, kea, key, @@ -11,25 +12,25 @@ import { selectors, sharedListeners, } from 'kea' -import type { notebookLogicType } from './notebookLogicType' import { loaders } from 'kea-loaders' -import { notebooksModel, openNotebook, SCRATCHPAD_NOTEBOOK } from '~/models/notebooksModel' -import { NotebookNodeType, NotebookSyncStatus, NotebookTarget, NotebookType } from '~/types' - -// NOTE: Annoyingly, if we import this then kea logic type-gen generates -// two imports and fails so, we reimport it from a utils file -import { EditorRange, JSONContent, NotebookEditor } from './utils' +import { router, urlToAction } from 'kea-router' import api from 'lib/api' -import posthog from 'posthog-js' import { downloadFile, slugify } from 'lib/utils' -import { lemonToast } from '@posthog/lemon-ui' -import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' +import posthog from 'posthog-js' import { buildTimestampCommentContent, NotebookNodeReplayTimestampAttrs, } from 'scenes/notebooks/Nodes/NotebookNodeReplayTimestamp' -import { NOTEBOOKS_VERSION, migrate } from './migrations/migrate' -import { router, urlToAction } from 'kea-router' + +import { notebooksModel, openNotebook, SCRATCHPAD_NOTEBOOK } from '~/models/notebooksModel' +import { NotebookNodeType, NotebookSyncStatus, NotebookTarget, NotebookType } from '~/types' + +import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' +import { migrate, NOTEBOOKS_VERSION } from './migrations/migrate' +import type { notebookLogicType } from './notebookLogicType' +// NOTE: Annoyingly, if we import this then kea logic type-gen generates +// two imports and fails so, we reimport it from a utils file +import { EditorRange, JSONContent, NotebookEditor } from './utils' const SYNC_DELAY = 1000 @@ -80,6 +81,7 @@ export const notebookLogic = kea([ clearPreviewContent: true, loadNotebook: true, saveNotebook: (notebook: Pick) => ({ notebook }), + renameNotebook: (title: string) => ({ title }), setEditingNodeId: (editingNodeId: string | null) => ({ editingNodeId }), exportJSON: true, showConflictWarning: true, @@ -265,6 +267,13 @@ export const notebookLogic = kea([ } } }, + renameNotebook: async ({ title }) => { + if (!values.notebook) { + return values.notebook + } + const response = await api.notebooks.update(values.notebook.short_id, { title }) + return response + }, }, ], diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index 2a989fd8a8299..c084ecad319a4 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -1,18 +1,20 @@ // Helpers for Kea issue with double importing import { LemonButtonProps } from '@posthog/lemon-ui' import { + Attribute, ChainedCommands as EditorCommands, Editor as TTEditor, + ExtendedRegExpMatchArray, FocusPosition as EditorFocusPosition, getText, JSONContent as TTJSONContent, Range as EditorRange, TextSerializer, - ExtendedRegExpMatchArray, - Attribute, } from '@tiptap/core' import { Node as PMNode } from '@tiptap/pm/model' + import { NotebookNodeResource, NotebookNodeType } from '~/types' + import { NotebookNodeLogicProps } from '../Nodes/notebookNodeLogic' // TODO: fix the typing of string to NotebookNodeType @@ -53,8 +55,8 @@ export interface JSONContent extends TTJSONContent {} export { ChainedCommands as EditorCommands, - Range as EditorRange, FocusPosition as EditorFocusPosition, + Range as EditorRange, } from '@tiptap/core' export type CustomNotebookNodeAttributes = Record diff --git a/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx b/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx index 894d00344a1b8..e6db7608942d4 100644 --- a/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx +++ b/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx @@ -1,13 +1,15 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { NotebookLogicProps, notebookLogic } from './Notebook/notebookLogic' -import { Notebook } from './Notebook/Notebook' import './NotebookScene.scss' -import { useMemo } from 'react' -import { uuid } from 'lib/utils' -import { useActions } from 'kea' + import { LemonBanner } from '@posthog/lemon-ui' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { useActions } from 'kea' import { NotFound } from 'lib/components/NotFound' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { uuid } from 'lib/utils' +import { useMemo } from 'react' +import { SceneExport } from 'scenes/sceneTypes' + +import { Notebook } from './Notebook/Notebook' +import { notebookLogic, NotebookLogicProps } from './Notebook/notebookLogic' export const scene: SceneExport = { component: NotebookCanvas, diff --git a/frontend/src/scenes/notebooks/NotebookMenu.tsx b/frontend/src/scenes/notebooks/NotebookMenu.tsx index ea307f726c8b9..b604271643f1a 100644 --- a/frontend/src/scenes/notebooks/NotebookMenu.tsx +++ b/frontend/src/scenes/notebooks/NotebookMenu.tsx @@ -1,12 +1,15 @@ -import { useActions, useValues } from 'kea' -import { NotebookLogicProps, notebookLogic } from './Notebook/notebookLogic' +import './NotebookScene.scss' + import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { router } from 'kea-router' import { IconDelete, IconEllipsis, IconExport, IconNotification, IconShare } from 'lib/lemon-ui/icons' import { LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { notebooksModel } from '~/models/notebooksModel' -import { router } from 'kea-router' import { urls } from 'scenes/urls' -import './NotebookScene.scss' + +import { notebooksModel } from '~/models/notebooksModel' + +import { notebookLogic, NotebookLogicProps } from './Notebook/notebookLogic' import { openNotebookShareDialog } from './Notebook/NotebookShare' export function NotebookMenu({ shortId }: NotebookLogicProps): JSX.Element { diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.scss b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.scss index c23308bf2eadf..5cd1a52a8ca02 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.scss +++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.scss @@ -40,7 +40,7 @@ } &--active { - border-color: var(--primary); + border-color: var(--primary-3000); height: 8rem; .NotebookPanelDropzone__message { @@ -52,7 +52,6 @@ border: none; height: 100%; margin: 1rem; - justify-content: flex-start; align-items: initial; } diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx index ef4503e51a8c4..8392f5dc9f2cf 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx +++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx @@ -1,19 +1,22 @@ -import { useActions, useValues } from 'kea' import './NotebookPanel.scss' -import { Notebook } from '../Notebook/Notebook' + import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { useMemo } from 'react' +import { urls } from 'scenes/urls' + +import { SidePanelPaneHeader } from '~/layout/navigation-3000/sidepanel/components/SidePanelPane' +import { NotebookTarget } from '~/types' + +import { Notebook } from '../Notebook/Notebook' import { NotebookListMini } from '../Notebook/NotebookListMini' -import { NotebookExpandButton, NotebookSyncInfo } from '../Notebook/NotebookMeta' import { notebookLogic } from '../Notebook/notebookLogic' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { notebookPanelLogic } from './notebookPanelLogic' -import { NotebookPanelDropzone } from './NotebookPanelDropzone' -import { urls } from 'scenes/urls' +import { NotebookExpandButton, NotebookSyncInfo } from '../Notebook/NotebookMeta' import { NotebookMenu } from '../NotebookMenu' -import { NotebookTarget } from '~/types' -import { SidePanelPaneHeader } from '~/layout/navigation-3000/sidepanel/components/SidePanelPane' +import { NotebookPanelDropzone } from './NotebookPanelDropzone' +import { notebookPanelLogic } from './notebookPanelLogic' export function NotebookPanel(): JSX.Element | null { const { selectedNotebook, initialAutofocus, droppedResource, dropProperties } = useValues(notebookPanelLogic) diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanelDropzone.tsx b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanelDropzone.tsx index 4f4167486e22e..2e219c197fc98 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanelDropzone.tsx +++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanelDropzone.tsx @@ -1,10 +1,12 @@ +import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' -import { DragEventHandler, useState } from 'react' import { useActions, useValues } from 'kea' +import { DragEventHandler, useState } from 'react' + import { NotebookNodeType } from '~/types' -import { NotebookSelectList } from '../NotebookSelectButton/NotebookSelectButton' + import { notebookLogicType } from '../Notebook/notebookLogicType' -import { LemonButton } from '@posthog/lemon-ui' +import { NotebookSelectList } from '../NotebookSelectButton/NotebookSelectButton' import { notebookPanelLogic } from './notebookPanelLogic' export function NotebookPanelDropzone(): JSX.Element | null { diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.scss b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.scss index 1c8b49d798561..4534f030b9bf5 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.scss +++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.scss @@ -2,21 +2,15 @@ .NotebookPopover { position: fixed; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; + inset: 0; z-index: var(--z-modal); pointer-events: none; .NotebookPopover__backdrop { position: absolute; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; + inset: 0; z-index: 1; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgb(0 0 0 / 10%); pointer-events: none; opacity: 0; transition: opacity 200ms ease-out; @@ -47,7 +41,7 @@ border-radius: var(--radius); background-color: var(--bg-3000); border: 1px solid var(--border-3000); - box-shadow: 0px 16px 16px rgba(0, 0, 0, 0); + box-shadow: 0 16px 16px rgb(0 0 0 / 0%); transition: box-shadow 150ms linear; overflow: hidden; } @@ -61,8 +55,9 @@ .NotebookPopover__content { transform: translateX(0); + .NotebookPopover__content__card { - box-shadow: 0px 16px 16px rgba(0, 0, 0, 0.15); + box-shadow: 0 16px 16px rgb(0 0 0 / 15%); } } } @@ -71,8 +66,9 @@ .NotebookPopover__content { transition: none; // NOTE: This shouldn't be none as it affects other transitions transform: translateX(calc(100% - 5rem)); + .NotebookPopover__content__card { - box-shadow: 0px 16px 16px rgba(0, 0, 0, 0.15); + box-shadow: 0 16px 16px rgb(0 0 0 / 15%); } } } @@ -91,10 +87,9 @@ } .NotebookPanelDropzone { - box-shadow: 0px 16px 16px rgba(0, 0, 0, 0.15); + box-shadow: 0 16px 16px rgb(0 0 0 / 15%); border: 2px dashed var(--border-3000); border-radius: var(--radius); - transition: all 150ms; height: 4rem; margin-bottom: 1rem; @@ -126,7 +121,7 @@ } &--active { - border-color: var(--primary); + border-color: var(--primary-3000); height: 8rem; .NotebookPanelDropzone__message { @@ -136,7 +131,7 @@ &--dropped { padding: 1rem; - border-color: var(--primary); + border-color: var(--primary-3000); background-color: var(--bg-light); height: 100%; justify-content: flex-start; diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.tsx b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.tsx index 9775a3a0a7406..a10016e0eb5cc 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.tsx +++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPopover.tsx @@ -1,21 +1,23 @@ -import { useActions, useValues } from 'kea' -import clsx from 'clsx' import './NotebookPopover.scss' -import { Notebook } from '../Notebook/Notebook' -import { notebookPopoverLogic } from 'scenes/notebooks/NotebookPanel/notebookPopoverLogic' + import { LemonButton } from '@posthog/lemon-ui' -import { IconFullScreen, IconChevronRight, IconOpenInNew, IconShare } from 'lib/lemon-ui/icons' -import { useEffect, useMemo, useRef } from 'react' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' -import { NotebookListMini } from '../Notebook/NotebookListMini' -import { NotebookExpandButton, NotebookSyncInfo } from '../Notebook/NotebookMeta' -import { notebookLogic } from '../Notebook/notebookLogic' -import { urls } from 'scenes/urls' -import { NotebookPanelDropzone } from './NotebookPanelDropzone' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { openNotebookShareDialog } from '../Notebook/NotebookShare' +import { IconChevronRight, IconFullScreen, IconOpenInNew, IconShare } from 'lib/lemon-ui/icons' +import { useEffect, useMemo, useRef } from 'react' +import { notebookPopoverLogic } from 'scenes/notebooks/NotebookPanel/notebookPopoverLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Notebook } from '../Notebook/Notebook' +import { NotebookListMini } from '../Notebook/NotebookListMini' +import { notebookLogic } from '../Notebook/notebookLogic' +import { NotebookExpandButton, NotebookSyncInfo } from '../Notebook/NotebookMeta' +import { openNotebookShareDialog } from '../Notebook/NotebookShare' +import { NotebookPanelDropzone } from './NotebookPanelDropzone' export function NotebookPopoverCard(): JSX.Element | null { const { popoverVisibility, shownAtLeastOnce, fullScreen, selectedNotebook, initialAutofocus, droppedResource } = diff --git a/frontend/src/scenes/notebooks/NotebookPanel/notebookPanelLogic.ts b/frontend/src/scenes/notebooks/NotebookPanel/notebookPanelLogic.ts index 0f18ce5290ece..7f77123b42c53 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/notebookPanelLogic.ts +++ b/frontend/src/scenes/notebooks/NotebookPanel/notebookPanelLogic.ts @@ -1,14 +1,14 @@ -import { actions, kea, reducers, path, listeners, selectors, connect } from 'kea' - +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { HTMLProps } from 'react' -import { EditorFocusPosition } from '../Notebook/utils' -import type { notebookPanelLogicType } from './notebookPanelLogicType' +import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' import { NotebookNodeResource, SidePanelTab } from '~/types' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' + +import { EditorFocusPosition } from '../Notebook/utils' +import type { notebookPanelLogicType } from './notebookPanelLogicType' import { notebookPopoverLogic } from './notebookPopoverLogic' -import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' export const notebookPanelLogic = kea([ path(['scenes', 'notebooks', 'Notebook', 'notebookPanelLogic']), diff --git a/frontend/src/scenes/notebooks/NotebookPanel/notebookPopoverLogic.ts b/frontend/src/scenes/notebooks/NotebookPanel/notebookPopoverLogic.ts index 471da0ed75e8b..d1a0997d2179e 100644 --- a/frontend/src/scenes/notebooks/NotebookPanel/notebookPopoverLogic.ts +++ b/frontend/src/scenes/notebooks/NotebookPanel/notebookPopoverLogic.ts @@ -1,14 +1,14 @@ -import { actions, kea, reducers, path, listeners, selectors } from 'kea' - +import { actions, kea, listeners, path, reducers, selectors } from 'kea' import { urlToAction } from 'kea-router' -import { HTMLProps, RefObject } from 'react' -import posthog from 'posthog-js' import { subscriptions } from 'kea-subscriptions' -import { EditorFocusPosition } from '../Notebook/utils' +import posthog from 'posthog-js' +import { HTMLProps, RefObject } from 'react' -import type { notebookPopoverLogicType } from './notebookPopoverLogicType' import { NotebookNodeResource, NotebookPopoverVisibility } from '~/types' +import { EditorFocusPosition } from '../Notebook/utils' +import type { notebookPopoverLogicType } from './notebookPopoverLogicType' + export const notebookPopoverLogic = kea([ path(['scenes', 'notebooks', 'Notebook', 'notebookPopoverLogic']), actions({ diff --git a/frontend/src/scenes/notebooks/NotebookScene.scss b/frontend/src/scenes/notebooks/NotebookScene.scss index d3c9b8e85811b..596dcd4efbed7 100644 --- a/frontend/src/scenes/notebooks/NotebookScene.scss +++ b/frontend/src/scenes/notebooks/NotebookScene.scss @@ -1,10 +1,7 @@ .NotebookScene { .Navigation3000 & { position: absolute; - left: 0; - top: 0; - bottom: 0; - right: 0; + inset: 0; overflow-y: auto; padding: 0 1rem; } diff --git a/frontend/src/scenes/notebooks/NotebookScene.tsx b/frontend/src/scenes/notebooks/NotebookScene.tsx index a6a4ad229270f..0d0b2baa69f5e 100644 --- a/frontend/src/scenes/notebooks/NotebookScene.tsx +++ b/frontend/src/scenes/notebooks/NotebookScene.tsx @@ -1,21 +1,24 @@ +import './NotebookScene.scss' + +import { IconInfo, IconOpenSidebar } from '@posthog/icons' +import { LemonButton, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { SceneExport } from 'scenes/sceneTypes' -import { notebookLogic } from './Notebook/notebookLogic' -import { Notebook } from './Notebook/Notebook' import { NotFound } from 'lib/components/NotFound' -import { NotebookSceneLogicProps, notebookSceneLogic } from './notebookSceneLogic' -import { LemonButton, LemonTag } from '@posthog/lemon-ui' -import { NotebookExpandButton, NotebookSyncInfo } from './Notebook/NotebookMeta' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' -import { IconArrowRight, IconHelpOutline } from 'lib/lemon-ui/icons' -import { LOCAL_NOTEBOOK_TEMPLATES } from './NotebookTemplates/notebookTemplates' -import './NotebookScene.scss' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { NotebookTarget } from '~/types' + +import { Notebook } from './Notebook/Notebook' import { NotebookLoadingState } from './Notebook/NotebookLoadingState' -import { notebookPanelLogic } from './NotebookPanel/notebookPanelLogic' +import { notebookLogic } from './Notebook/notebookLogic' +import { NotebookExpandButton, NotebookSyncInfo } from './Notebook/NotebookMeta' import { NotebookMenu } from './NotebookMenu' -import { NotebookTarget } from '~/types' +import { notebookPanelLogic } from './NotebookPanel/notebookPanelLogic' +import { notebookSceneLogic, NotebookSceneLogicProps } from './notebookSceneLogic' +import { LOCAL_NOTEBOOK_TEMPLATES } from './NotebookTemplates/notebookTemplates' interface NotebookSceneProps { shortId?: string @@ -48,7 +51,7 @@ export function NotebookScene(): JSX.Element { return (

    - This Notebook is open in the side panel + This Notebook is open in the side panel

    @@ -84,7 +87,7 @@ export function NotebookScene(): JSX.Element { } + icon={} size={buttonSize} onClick={() => { if (selectedNotebook === LOCAL_NOTEBOOK_TEMPLATES[0].short_id && visibility === 'visible') { @@ -109,11 +112,11 @@ export function NotebookScene(): JSX.Element { tooltip={ <> Opens the notebook in a side panel, that can be accessed from anywhere in the PostHog - app. This is great for dragging and dropping elements like Insights, Recordings or even - Feature Flags into your active Notebook. + app. This is great for dragging and dropping elements like insights, recordings or even + feature flags into your active notebook. } - sideIcon={} + sideIcon={} > Open in side panel diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx index a68a60d9b5bdc..d7bf1039a7f9a 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryFn } from '@storybook/react' +import { FEATURE_FLAGS } from 'lib/constants' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' + import { setFeatureFlags, useStorybookMocks } from '~/mocks/browser' import { NotebookNodeType } from '~/types' -import { FEATURE_FLAGS } from 'lib/constants' export default { title: 'Scenes-App/Notebooks/Components/Notebook Select Button', diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx index d240cf1179a53..63e8f2b501a9a 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx @@ -1,24 +1,25 @@ -import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' - +import { LemonDivider, ProfilePicture } from '@posthog/lemon-ui' +import { BuiltLogic, useActions, useValues } from 'kea' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FEATURE_FLAGS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' import { IconPlus, IconWithCount } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { Popover, PopoverProps } from 'lib/lemon-ui/Popover' +import { ReactChild, useEffect } from 'react' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { - NotebookSelectButtonLogicProps, notebookSelectButtonLogic, + NotebookSelectButtonLogicProps, } from 'scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic' -import { BuiltLogic, useActions, useValues } from 'kea' -import { dayjs } from 'lib/dayjs' -import { NotebookListItemType, NotebookTarget } from '~/types' + import { notebooksModel, openNotebook } from '~/models/notebooksModel' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' -import { Popover, PopoverProps } from 'lib/lemon-ui/Popover' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { notebookLogicType } from '../Notebook/notebookLogicType' -import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' -import { ReactChild, useEffect } from 'react' -import { LemonDivider, ProfilePicture } from '@posthog/lemon-ui' +import { NotebookListItemType, NotebookTarget } from '~/types' + import { IconNotebook } from '../IconNotebook' +import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType' +import { notebookLogicType } from '../Notebook/notebookLogicType' export type NotebookSelectProps = NotebookSelectButtonLogicProps & { newNotebookTitle?: string @@ -85,9 +86,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { const { setShowPopover, setSearchQuery, loadNotebooksContainingResource, loadAllNotebooks } = useActions(logic) const { createNotebook } = useActions(notebooksModel) - const openAndAddToNotebook = async (notebookShortId: string, exists: boolean): Promise => { + const openAndAddToNotebook = (notebookShortId: string, exists: boolean): void => { const position = props.resource ? 'end' : 'start' - await openNotebook(notebookShortId, NotebookTarget.Popover, position, (theNotebookLogic) => { + void openNotebook(notebookShortId, NotebookTarget.Popover, position, (theNotebookLogic) => { if (!exists && props.resource) { theNotebookLogic.actions.insertAfterLastNode([props.resource]) } @@ -168,9 +169,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { emptyState={ searchQuery.length ? 'No matching notebooks' : 'Not already in any notebooks' } - onClick={async (notebookShortId) => { + onClick={(notebookShortId) => { setShowPopover(false) - await openAndAddToNotebook(notebookShortId, true) + openAndAddToNotebook(notebookShortId, true) }} /> @@ -180,9 +181,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { { + onClick={(notebookShortId) => { setShowPopover(false) - await openAndAddToNotebook(notebookShortId, false) + openAndAddToNotebook(notebookShortId, false) }} /> diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts b/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts index ed446007c031d..956a96094fad7 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts @@ -1,9 +1,9 @@ import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { NotebookListItemType, NotebookNodeResource, NotebookNodeType } from '~/types' - import api from 'lib/api' +import { NotebookListItemType, NotebookNodeResource, NotebookNodeType } from '~/types' + import type { notebookSelectButtonLogicType } from './notebookSelectButtonLogicType' export interface NotebookSelectButtonLogicProps { diff --git a/frontend/src/scenes/notebooks/NotebooksScene.tsx b/frontend/src/scenes/notebooks/NotebooksScene.tsx index 7caede1f116db..df4360e389353 100644 --- a/frontend/src/scenes/notebooks/NotebooksScene.tsx +++ b/frontend/src/scenes/notebooks/NotebooksScene.tsx @@ -1,10 +1,12 @@ -import { SceneExport } from 'scenes/sceneTypes' import './NotebookScene.scss' -import { NotebooksTable } from './NotebooksTable/NotebooksTable' + import { LemonButton, LemonTag } from '@posthog/lemon-ui' import { PageHeader } from 'lib/components/PageHeader' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { NotebooksTable } from './NotebooksTable/NotebooksTable' + export const scene: SceneExport = { component: NotebooksScene, } diff --git a/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx b/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx index 1a8c2f3159b86..94fcfaa5572c9 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx @@ -1,7 +1,9 @@ -import { NotebookNodeType } from '~/types' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple' +import posthog from 'posthog-js' import { NotebooksListFilters } from 'scenes/notebooks/NotebooksTable/notebooksTableLogic' +import { NotebookNodeType } from '~/types' + export const fromNodeTypeToLabel: Omit< Record, | NotebookNodeType.Backlink @@ -48,6 +50,7 @@ export function ContainsTypeFilters({ }, {})} value={filters.contains} onChange={(newValue: string[]) => { + posthog.capture('notebook containing filter applied') setFilters({ contains: newValue.map((x) => x as NotebookNodeType) }) }} data-attr={'notebooks-list-contains-filters'} diff --git a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx index 169fb46aeced8..bb10007b72fd9 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx @@ -1,18 +1,20 @@ +import { LemonButton, LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { IconDelete, IconEllipsis } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonMenu } from 'lib/lemon-ui/LemonMenu' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { NotebookListItemType } from '~/types' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { LemonButton, LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui' -import { notebooksModel } from '~/models/notebooksModel' +import { Link } from 'lib/lemon-ui/Link' import { useEffect } from 'react' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { IconDelete, IconEllipsis } from 'lib/lemon-ui/icons' -import { membersLogic } from 'scenes/organization/membersLogic' import { ContainsTypeFilters } from 'scenes/notebooks/NotebooksTable/ContainsTypeFilter' import { DEFAULT_FILTERS, notebooksTableLogic } from 'scenes/notebooks/NotebooksTable/notebooksTableLogic' +import { membersLogic } from 'scenes/organization/membersLogic' +import { urls } from 'scenes/urls' + +import { notebooksModel } from '~/models/notebooksModel' +import { NotebookListItemType } from '~/types' + import { notebookPanelLogic } from '../NotebookPanel/notebookPanelLogic' function titleColumn(): LemonTableColumn { @@ -115,7 +117,7 @@ export function NotebooksTable(): JSX.Element { Created by: ({ value: x.user.uuid, label: x.user.first_name, diff --git a/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts b/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts index d65d055617987..b234af21f1270 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts +++ b/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts @@ -1,11 +1,12 @@ -import { actions, kea, listeners, reducers, path, selectors, connect } from 'kea' -import { NotebookListItemType, NotebookNodeType } from '~/types' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import api from 'lib/api' import { objectClean, objectsEqual } from 'lib/utils' -import { loaders } from 'kea-loaders' -import type { notebooksTableLogicType } from './notebooksTableLogicType' import { notebooksModel } from '~/models/notebooksModel' +import { NotebookListItemType, NotebookNodeType } from '~/types' + +import type { notebooksTableLogicType } from './notebooksTableLogicType' export interface NotebooksListFilters { search: string @@ -32,7 +33,7 @@ export const notebooksTableLogic = kea([ }), reducers({ filters: [ - DEFAULT_FILTERS as NotebooksListFilters, + DEFAULT_FILTERS, { setFilters: (state, { filters }) => objectClean({ diff --git a/frontend/src/scenes/notebooks/Suggestions/FloatingSuggestions.tsx b/frontend/src/scenes/notebooks/Suggestions/FloatingSuggestions.tsx index f5c17eca5d90c..42a557eb7ab91 100644 --- a/frontend/src/scenes/notebooks/Suggestions/FloatingSuggestions.tsx +++ b/frontend/src/scenes/notebooks/Suggestions/FloatingSuggestions.tsx @@ -1,11 +1,13 @@ import './FloatingSuggestions.scss' + import { Editor as TTEditor } from '@tiptap/core' import { useActions, useValues } from 'kea' -import { insertionSuggestionsLogic } from './insertionSuggestionsLogic' -import { isCurrentNodeEmpty } from '../Notebook/utils' +import { useResizeObserver } from 'lib/hooks/useResizeObserver' import { useEffect, useState } from 'react' + import { notebookLogic } from '../Notebook/notebookLogic' -import { useResizeObserver } from 'lib/hooks/useResizeObserver' +import { isCurrentNodeEmpty } from '../Notebook/utils' +import { insertionSuggestionsLogic } from './insertionSuggestionsLogic' export function FloatingSuggestions({ editor }: { editor: TTEditor }): JSX.Element | null { const logic = insertionSuggestionsLogic() diff --git a/frontend/src/scenes/notebooks/Suggestions/ReplayTimestamp.tsx b/frontend/src/scenes/notebooks/Suggestions/ReplayTimestamp.tsx index ce7c762d7bd66..451dd067d7d9c 100644 --- a/frontend/src/scenes/notebooks/Suggestions/ReplayTimestamp.tsx +++ b/frontend/src/scenes/notebooks/Suggestions/ReplayTimestamp.tsx @@ -1,12 +1,14 @@ +import { LemonButton } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' + import { NotebookNodeType } from '~/types' -import { firstChildOfType, hasChildOfType } from '../Notebook/Editor' -import { buildTimestampCommentContent, formatTimestamp } from '../Nodes/NotebookNodeReplayTimestamp' + import { sessionRecordingPlayerProps } from '../Nodes/NotebookNodeRecording' -import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { useValues } from 'kea' -import { InsertionSuggestion, InsertionSuggestionViewProps } from './InsertionSuggestion' +import { buildTimestampCommentContent, formatTimestamp } from '../Nodes/NotebookNodeReplayTimestamp' +import { firstChildOfType, hasChildOfType } from '../Notebook/Editor' import { Node, NotebookEditor } from '../Notebook/utils' -import { LemonButton } from '@posthog/lemon-ui' +import { InsertionSuggestion, InsertionSuggestionViewProps } from './InsertionSuggestion' const insertTimestamp = ({ editor, diff --git a/frontend/src/scenes/notebooks/Suggestions/SlashCommands.tsx b/frontend/src/scenes/notebooks/Suggestions/SlashCommands.tsx index 707bb1e46b90f..3c5c0710f48ee 100644 --- a/frontend/src/scenes/notebooks/Suggestions/SlashCommands.tsx +++ b/frontend/src/scenes/notebooks/Suggestions/SlashCommands.tsx @@ -1,9 +1,10 @@ -import { IconPlus } from 'lib/lemon-ui/icons' -import { InsertionSuggestion, InsertionSuggestionViewProps } from './InsertionSuggestion' -import { SlashCommandsPopover } from '../Notebook/SlashCommands' import { LemonButton } from '@posthog/lemon-ui' +import { IconPlus } from 'lib/lemon-ui/icons' import { useState } from 'react' +import { SlashCommandsPopover } from '../Notebook/SlashCommands' +import { InsertionSuggestion, InsertionSuggestionViewProps } from './InsertionSuggestion' + const Component = ({ editor }: InsertionSuggestionViewProps): JSX.Element => { const [visible, setVisible] = useState(false) diff --git a/frontend/src/scenes/notebooks/Suggestions/insertionSuggestionsLogic.ts b/frontend/src/scenes/notebooks/Suggestions/insertionSuggestionsLogic.ts index d7930368c4e18..8da79c7d6876c 100644 --- a/frontend/src/scenes/notebooks/Suggestions/insertionSuggestionsLogic.ts +++ b/frontend/src/scenes/notebooks/Suggestions/insertionSuggestionsLogic.ts @@ -1,9 +1,10 @@ import { actions, events, kea, listeners, path, reducers, selectors } from 'kea' + +import { Node, NotebookEditor } from '../Notebook/utils' +import { InsertionSuggestion } from './InsertionSuggestion' import type { insertionSuggestionsLogicType } from './insertionSuggestionsLogicType' import ReplayTimestampSuggestion from './ReplayTimestamp' import SlashCommands from './SlashCommands' -import { InsertionSuggestion } from './InsertionSuggestion' -import { Node, NotebookEditor } from '../Notebook/utils' export const insertionSuggestionsLogic = kea([ path(['scenes', 'notebooks', 'Suggestions', 'insertionSuggestionsLogic']), diff --git a/frontend/src/scenes/notebooks/notebookSceneLogic.ts b/frontend/src/scenes/notebooks/notebookSceneLogic.ts index 2d8656ddd5447..aa9c7fa0eabb3 100644 --- a/frontend/src/scenes/notebooks/notebookSceneLogic.ts +++ b/frontend/src/scenes/notebooks/notebookSceneLogic.ts @@ -1,10 +1,12 @@ import { afterMount, connect, kea, key, path, props, selectors } from 'kea' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { notebooksModel } from '~/models/notebooksModel' import { Breadcrumb, NotebookTarget } from '~/types' -import type { notebookSceneLogicType } from './notebookSceneLogicType' import { notebookLogic } from './Notebook/notebookLogic' -import { urls } from 'scenes/urls' -import { notebooksModel } from '~/models/notebooksModel' +import type { notebookSceneLogicType } from './notebookSceneLogicType' export type NotebookSceneLogicProps = { shortId: string @@ -17,7 +19,7 @@ export const notebookSceneLogic = kea([ values: [notebookLogic(props), ['notebook', 'notebookLoading'], notebooksModel, ['notebooksLoading']], actions: [notebookLogic(props), ['loadNotebook'], notebooksModel, ['createNotebook']], })), - selectors(() => ({ + selectors(({ props }) => ({ notebookId: [() => [(_, props) => props], (props): string => props.shortId], loading: [ @@ -29,11 +31,18 @@ export const notebookSceneLogic = kea([ (s) => [s.notebook, s.loading], (notebook, loading): Breadcrumb[] => [ { + key: Scene.Notebooks, name: 'Notebooks', path: urls.notebooks(), }, { - name: notebook ? notebook?.title || 'Unnamed' : loading ? 'Loading...' : 'Notebook not found', + key: notebook?.short_id || 'new', + name: notebook ? notebook?.title || 'Unnamed' : loading ? null : 'Notebook not found', + onRename: !notebook?.is_template + ? async (title: string) => { + await notebookLogic(props).asyncActions.renameNotebook(title) + } + : undefined, }, ], ], diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 50a1ff9f25956..f7e01007b81cf 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -1,18 +1,17 @@ -import { SceneExport } from 'scenes/sceneTypes' import { useActions, useValues } from 'kea' import { useEffect, useState } from 'react' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { urls } from 'scenes/urls' -import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' -import { SDKs } from './sdks/SDKs' +import { SceneExport } from 'scenes/sceneTypes' + import { ProductKey } from '~/types' -import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions' -import { SessionReplaySDKInstructions } from './sdks/session-replay/SessionReplaySDKInstructions' + import { OnboardingBillingStep } from './OnboardingBillingStep' +import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' import { OnboardingOtherProductsStep } from './OnboardingOtherProductsStep' import { OnboardingVerificationStep } from './OnboardingVerificationStep' import { FeatureFlagsSDKInstructions } from './sdks/feature-flags/FeatureFlagsSDKInstructions' +import { ProductAnalyticsSDKInstructions } from './sdks/product-analytics/ProductAnalyticsSDKInstructions' +import { SDKs } from './sdks/SDKs' +import { SessionReplaySDKInstructions } from './sdks/session-replay/SessionReplaySDKInstructions' import { SurveysSDKInstructions } from './sdks/surveys/SurveysSDKInstructions' export const scene: SceneExport = { @@ -120,15 +119,8 @@ const SurveysOnboarding = (): JSX.Element => { } export function Onboarding(): JSX.Element | null { - const { featureFlags } = useValues(featureFlagLogic) const { product } = useValues(onboardingLogic) - useEffect(() => { - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== 'test') { - location.href = urls.ingestion() - } - }, []) - if (!product) { return <> } diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index c6901e9d7b829..18b3c77554ae2 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -1,17 +1,19 @@ -import { OnboardingStep } from './OnboardingStep' -import { PlanComparison } from 'scenes/billing/PlanComparison' +import { LemonBanner, LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { billingLogic } from 'scenes/billing/billingLogic' -import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' -import { BillingProductV2Type } from '~/types' +import { StarHog } from 'lib/components/hedgehogs' +import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner' -import { BillingHero } from 'scenes/billing/BillingHero' -import { LemonButton } from '@posthog/lemon-ui' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { getUpgradeProductLink } from 'scenes/billing/billing-utils' +import { BillingHero } from 'scenes/billing/BillingHero' +import { billingLogic } from 'scenes/billing/billingLogic' import { billingProductLogic } from 'scenes/billing/billingProductLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' -import { StarHog } from 'lib/components/hedgehogs' +import { PlanComparison } from 'scenes/billing/PlanComparison' + +import { BillingProductV2Type } from '~/types' + +import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' +import { OnboardingStep } from './OnboardingStep' export const OnboardingBillingStep = ({ product, @@ -25,6 +27,7 @@ export const OnboardingBillingStep = ({ const { currentAndUpgradePlans } = useValues(billingProductLogic({ product })) const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const plan = currentAndUpgradePlans?.upgradePlan + const currentPlan = currentAndUpgradePlans?.currentPlan return ( - Upgrade to paid + Subscribe ) } @@ -52,7 +55,7 @@ export const OnboardingBillingStep = ({

    {product.subscribed ? (
    -
    +
    @@ -64,6 +67,15 @@ export const OnboardingBillingStep = ({
    + {currentPlan?.initial_billing_limit && ( +
    + + To protect your costs and ours, this product has an initial billing limit of $ + {currentPlan.initial_billing_limit}. You can change or remove this limit on the + Billing page. + +
    + )}
    ) : ( <> diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx index 54618e56a0d64..caac1d4f6cc0a 100644 --- a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx @@ -1,8 +1,9 @@ -import { LemonButton, LemonCard } from '@posthog/lemon-ui' -import { OnboardingStep } from './OnboardingStep' -import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' import { useActions, useValues } from 'kea' -import { getProductIcon } from 'scenes/products/Products' +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, @@ -11,6 +12,8 @@ export const OnboardingOtherProductsStep = ({ }): 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) => ( - -
    -
    {getProductIcon(suggestedProduct.icon_key, 'text-2xl')}
    -
    -

    {suggestedProduct.name}

    -

    {suggestedProduct.description}

    -
    -
    -
    - completeOnboarding(suggestedProduct.type)}> - Get started - -
    -
    + getStartedActionOverride={() => completeOnboarding(suggestedProduct.type)} + orientation={horizontalCard ? 'horizontal' : 'vertical'} + className="w-full" + /> ))}
    diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index a4bfdd92cbf42..fde8a4dfff949 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -1,11 +1,12 @@ import { LemonButton } from '@posthog/lemon-ui' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' import { useActions, useValues } from 'kea' -import { IconArrowLeft, IconArrowRight } from 'lib/lemon-ui/icons' import { router } from 'kea-router' +import { BridgePage } from 'lib/components/BridgePage/BridgePage' +import { IconArrowLeft, IconArrowRight } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' +import { onboardingLogic, OnboardingStepKey } from './onboardingLogic' + export const OnboardingStep = ({ stepKey, title, @@ -14,6 +15,7 @@ export const OnboardingStep = ({ showSkip = false, onSkip, continueOverride, + backActionOverride, }: { stepKey: OnboardingStepKey title: string @@ -22,6 +24,7 @@ export const OnboardingStep = ({ showSkip?: boolean onSkip?: () => void continueOverride?: JSX.Element + backActionOverride?: () => void }): JSX.Element => { const { hasNextStep, hasPreviousStep } = useValues(onboardingLogic) const { completeOnboarding, goToNextStep, goToPreviousStep } = useActions(onboardingLogic) @@ -39,14 +42,20 @@ export const OnboardingStep = ({
    } - onClick={() => (hasPreviousStep ? goToPreviousStep() : router.actions.push(urls.products()))} + onClick={() => + backActionOverride + ? backActionOverride() + : hasPreviousStep + ? goToPreviousStep() + : router.actions.push(urls.products()) + } > Back
    } > -
    +

    {title}

    {subtitle}

    {children} diff --git a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx index 11ba1dc4fd065..358058a312e92 100644 --- a/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingVerificationStep.tsx @@ -1,12 +1,13 @@ import { Spinner } from '@posthog/lemon-ui' -import { OnboardingStep } from './OnboardingStep' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { useInterval } from 'lib/hooks/useInterval' import { BlushingHog } from 'lib/components/hedgehogs' +import { useInterval } from 'lib/hooks/useInterval' import { capitalizeFirstLetter } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { teamLogic } from 'scenes/teamLogic' + import { OnboardingStepKey } from './onboardingLogic' +import { OnboardingStep } from './OnboardingStep' export const OnboardingVerificationStep = ({ listeningForName, diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index c484105b60e7a..f79b06d1a9011 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -1,11 +1,11 @@ -import { kea, props, path, connect, actions, reducers, selectors, listeners } from 'kea' -import { BillingProductV2Type, ProductKey } from '~/types' -import { urls } from 'scenes/urls' - +import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' +import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { billingLogic } from 'scenes/billing/billingLogic' import { teamLogic } from 'scenes/teamLogic' -import { combineUrl, router, actionToUrl, urlToAction } from 'kea-router' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { urls } from 'scenes/urls' + +import { BillingProductV2Type, ProductKey } from '~/types' import type { onboardingLogicType } from './onboardingLogicType' @@ -74,7 +74,7 @@ export const onboardingLogic = kea([ allOnboardingSteps: [ [] as AllOnboardingSteps, { - setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps as AllOnboardingSteps, + setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps, }, ], stepKey: [ @@ -84,7 +84,7 @@ export const onboardingLogic = kea([ }, ], onCompleteOnboardingRedirectUrl: [ - urls.default() as string, + urls.default(), { setProductKey: (_, { productKey }) => { return productKey ? getProductUri(productKey as ProductKey) : urls.default() @@ -153,7 +153,9 @@ export const onboardingLogic = kea([ }), listeners(({ actions, values }) => ({ loadBillingSuccess: () => { - actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) + if (window.location.pathname.startsWith('/onboarding')) { + actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) + } }, setProduct: ({ product }) => { if (!product) { @@ -205,7 +207,7 @@ export const onboardingLogic = kea([ } }, resetStepKey: () => { - actions.setStepKey(values.allOnboardingSteps[0].props.stepKey) + values.allOnboardingSteps[0] && actions.setStepKey(values.allOnboardingSteps[0]?.props.stepKey) }, })), actionToUrl(({ values }) => ({ diff --git a/frontend/src/scenes/onboarding/sdks/SDKSnippet.tsx b/frontend/src/scenes/onboarding/sdks/SDKSnippet.tsx index d83a72c3d7eb1..1a3a90c87a3eb 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKSnippet.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKSnippet.tsx @@ -1,6 +1,7 @@ -import { SDK } from '~/types' import { Link } from 'lib/lemon-ui/Link' +import { SDK } from '~/types' + export const SDKSnippet = ({ sdk, sdkInstructions }: { sdk: SDK; sdkInstructions: () => JSX.Element }): JSX.Element => { return (
    diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 9a91ad06268d6..3c51ba77b0a72 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -1,13 +1,17 @@ +import { IconArrowLeft } from '@posthog/icons' import { LemonButton, LemonCard, LemonDivider, LemonSelect } from '@posthog/lemon-ui' -import { sdksLogic } from './sdksLogic' import { useActions, useValues } from 'kea' -import { OnboardingStep } from '../OnboardingStep' -import { SDKSnippet } from './SDKSnippet' -import { OnboardingStepKey, onboardingLogic } from '../onboardingLogic' +import { useWindowSize } from 'lib/hooks/useWindowSize' import { useEffect } from 'react' import React from 'react' -import { SDKInstructionsMap } from '~/types' + import { InviteMembersButton } from '~/layout/navigation/TopBar/SitePopover' +import { SDKInstructionsMap } from '~/types' + +import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' +import { OnboardingStep } from '../OnboardingStep' +import { sdksLogic } from './sdksLogic' +import { SDKSnippet } from './SDKSnippet' export function SDKs({ usersAction, @@ -20,23 +24,37 @@ export function SDKs({ subtitle?: string stepKey?: OnboardingStepKey }): JSX.Element { - const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap } = useActions(sdksLogic) - const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect } = useValues(sdksLogic) + const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap, setShowSideBySide, setPanel } = + useActions(sdksLogic) + const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect, showSideBySide, panel } = + useValues(sdksLogic) const { productKey } = useValues(onboardingLogic) + const { width } = useWindowSize() + const minimumSideBySideSize = 768 useEffect(() => { setAvailableSDKInstructionsMap(sdkInstructionMap) }, []) + useEffect(() => { + width && setShowSideBySide(width > minimumSideBySideSize) + }, [width]) + return ( : undefined} + backActionOverride={!showSideBySide && panel === 'instructions' ? () => setPanel('options') : undefined} >
    -
    +
    {showSourceOptionsSelect && (
    {selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && ( -
    +
    + {!showSideBySide && ( + } + onClick={() => setPanel('options')} + className="mb-8" + type="secondary" + > + View all SDKs + + )}
    )} diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx index 729a335fa3004..fee11b72005d0 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/FeatureFlagsSDKInstructions.tsx @@ -1,17 +1,18 @@ import { SDKInstructionsMap, SDKKey } from '~/types' + import { - FeatureFlagsJSWebInstructions, - FeatureFlagsNextJSInstructions, - FeatureFlagsAPIInstructions, FeatureFlagsAndroidInstructions, + FeatureFlagsAPIInstructions, FeatureFlagsGoInstructions, FeatureFlagsIOSInstructions, + FeatureFlagsJSWebInstructions, + FeatureFlagsNextJSInstructions, FeatureFlagsNodeInstructions, FeatureFlagsPHPInstructions, FeatureFlagsPythonInstructions, + FeatureFlagsReactInstructions, FeatureFlagsRNInstructions, FeatureFlagsRubyInstructions, - FeatureFlagsReactInstructions, } from '.' export const FeatureFlagsSDKInstructions: SDKInstructionsMap = { diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/android.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/android.tsx index 0c9a64d274a8d..be4eda78a2056 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/android.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/android.tsx @@ -1,6 +1,7 @@ -import { FlagImplementationSnippet } from './flagImplementationSnippet' import { SDKKey } from '~/types' + import { SDKInstallAndroidInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsAndroidInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/api.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/api.tsx index 5402e66f53b48..2555cbcf8bb9e 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/api.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/api.tsx @@ -1,4 +1,5 @@ import { SDKKey } from '~/types' + import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsAPIInstructions(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/flagImplementationSnippet.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/flagImplementationSnippet.tsx index 38a4e9b81d546..a4c3abd3db803 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/flagImplementationSnippet.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/flagImplementationSnippet.tsx @@ -1,5 +1,6 @@ import { OPTIONS } from 'scenes/feature-flags/FeatureFlagCodeOptions' import { CodeInstructions } from 'scenes/feature-flags/FeatureFlagInstructions' + import { SDKKey } from '~/types' export const FlagImplementationSnippet = ({ sdkKey }: { sdkKey: SDKKey }): JSX.Element => { diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/go.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/go.tsx index cdb750a2396f8..f95758e28b13e 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/go.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/go.tsx @@ -1,6 +1,7 @@ import { SDKKey } from '~/types' -import { FlagImplementationSnippet } from './flagImplementationSnippet' + import { SDKInstallGoInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsGoInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx index 11e1743082019..69d5234c62f1e 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/index.tsx @@ -1,12 +1,12 @@ export * from './android' +export * from './api' export * from './go' -export * from './nodejs' export * from './ios' +export * from './js-web' +export * from './next-js' +export * from './nodejs' export * from './php' export * from './python' +export * from './react' export * from './react-native' export * from './ruby' -export * from './api' -export * from './js-web' -export * from './react' -export * from './next-js' diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/ios.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/ios.tsx index 250c98fd4d3fd..5a9bee6e12017 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/ios.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/ios.tsx @@ -1,6 +1,7 @@ -import { FlagImplementationSnippet } from './flagImplementationSnippet' import { SDKKey } from '~/types' + import { SDKInstallIOSInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsIOSInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx index 78a2fa373faa6..7479715a80442 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/js-web.tsx @@ -1,7 +1,9 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { FlagImplementationSnippet } from './flagImplementationSnippet' + import { SDKKey } from '~/types' + import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsJSWebInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx index 7b1b37f16b2a1..121bb3aaea799 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/next-js.tsx @@ -1,7 +1,8 @@ import { SDKKey } from '~/types' -import { FlagImplementationSnippet } from './flagImplementationSnippet' -import { SDKInstallNextJSInstructions } from '../sdk-install-instructions/next-js' + import { NodeInstallSnippet, NodeSetupSnippet } from '../sdk-install-instructions' +import { SDKInstallNextJSInstructions } from '../sdk-install-instructions/next-js' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsNextJSInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/nodejs.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/nodejs.tsx index 576e6cd9091d2..9117e4b364683 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/nodejs.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/nodejs.tsx @@ -1,6 +1,7 @@ -import { FlagImplementationSnippet } from './flagImplementationSnippet' import { SDKKey } from '~/types' + import { SDKInstallNodeInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsNodeInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/php.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/php.tsx index 68a97ef96d9c4..6356354a84956 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/php.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/php.tsx @@ -1,6 +1,7 @@ import { SDKKey } from '~/types' -import { FlagImplementationSnippet } from './flagImplementationSnippet' + import { SDKInstallPHPInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsPHPInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/python.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/python.tsx index 55962b40f52ee..f06a7329e60f6 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/python.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/python.tsx @@ -1,6 +1,7 @@ import { SDKKey } from '~/types' -import { FlagImplementationSnippet } from './flagImplementationSnippet' + import { SDKInstallPythonInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsPythonInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/react-native.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/react-native.tsx index f045c817abcb8..f542713bea88e 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/react-native.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/react-native.tsx @@ -1,6 +1,7 @@ +import { SDKKey } from '~/types' + import { SDKInstallRNInstructions } from '../sdk-install-instructions' import { FlagImplementationSnippet } from './flagImplementationSnippet' -import { SDKKey } from '~/types' export function FeatureFlagsRNInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx index 35ff77019b763..45803d51941f0 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/react.tsx @@ -1,6 +1,7 @@ -import { FlagImplementationSnippet } from './flagImplementationSnippet' import { SDKKey } from '~/types' + import { SDKInstallReactInstructions } from '../sdk-install-instructions/react' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsReactInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/feature-flags/ruby.tsx b/frontend/src/scenes/onboarding/sdks/feature-flags/ruby.tsx index 388d934ede926..fd84d557b7ba3 100644 --- a/frontend/src/scenes/onboarding/sdks/feature-flags/ruby.tsx +++ b/frontend/src/scenes/onboarding/sdks/feature-flags/ruby.tsx @@ -1,6 +1,7 @@ -import { FlagImplementationSnippet } from './flagImplementationSnippet' import { SDKKey } from '~/types' + import { SDKInstallRubyInstructions } from '../sdk-install-instructions' +import { FlagImplementationSnippet } from './flagImplementationSnippet' export function FeatureFlagsRubyInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx index f572132017ab4..181aa04218e40 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx @@ -1,8 +1,9 @@ import { SDKInstructionsMap, SDKKey } from '~/types' + import { JSWebInstructions, - ProductAnalyticsAPIInstructions, ProductAnalyticsAndroidInstructions, + ProductAnalyticsAPIInstructions, ProductAnalyticsElixirInstructions, ProductAnalyticsFlutterInstructions, ProductAnalyticsGoInstructions, diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/android.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/android.tsx index 71435dd4fdee1..3bdbc959aed8d 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/android.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/android.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallAndroidInstructions } from '../sdk-install-instructions' function AndroidCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx index 64974e21d1905..263d14511865d 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function APISnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/flutter.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/flutter.tsx index 01c793bfc8d74..a5cd98e80bf96 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/flutter.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/flutter.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallFlutterInstructions } from '../sdk-install-instructions' function FlutterCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/go.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/go.tsx index 7d7d14f0cd818..b674d6567253a 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/go.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/go.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallGoInstructions } from '../sdk-install-instructions' function GoCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx index 0680cf876820f..dc6b1dd3e0bf8 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx @@ -1,12 +1,12 @@ export * from './android' +export * from './api' +export * from './elixir' +export * from './flutter' export * from './go' -export * from './nodejs' export * from './ios' +export * from './js-web' +export * from './nodejs' export * from './php' export * from './python' export * from './react-native' export * from './ruby' -export * from './api' -export * from './elixir' -export * from './flutter' -export * from './js-web' diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/ios.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/ios.tsx index 79ae931729710..b2ff488df0719 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/ios.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/ios.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallIOSInstructions } from '../sdk-install-instructions' function IOS_OBJ_C_CaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx index fc2eb0f53c67d..5491625dd047e 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx @@ -1,6 +1,7 @@ +import { LemonDivider } from '@posthog/lemon-ui' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' -import { LemonDivider } from '@posthog/lemon-ui' function JSEventSnippet(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/nodejs.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/nodejs.tsx index 6a6050ca44f49..f679751e5571e 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/nodejs.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/nodejs.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallNodeInstructions } from '../sdk-install-instructions' function NodeCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/php.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/php.tsx index 2704a4c285e2b..ba8f6387cef2a 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/php.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/php.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallPHPInstructions } from '../sdk-install-instructions' function PHPCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/python.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/python.tsx index 486326bf34669..fd49bb32b86b3 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/python.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/python.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallPythonInstructions } from '../sdk-install-instructions' function PythonCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx index 0492b8c210960..459644b3ec02a 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallRNInstructions } from '../sdk-install-instructions' export function ProductAnalyticsRNInstructions(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/ruby.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/ruby.tsx index 905897614ebcd..a02e140db827e 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/ruby.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/ruby.tsx @@ -1,4 +1,5 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { SDKInstallRubyInstructions } from '../sdk-install-instructions' function RubyCaptureSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/android.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/android.tsx index 01a4b7d11d934..b29914de6d192 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/android.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/android.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function AndroidInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/elixir.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/elixir.tsx index 2378c5ef93d0b..c73c631373462 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/elixir.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/elixir.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function ElixirInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx index e37b2b1038388..b2284e00d6596 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function FlutterInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/go.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/go.tsx index 87bf25b337c40..d29559e6067af 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/go.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/go.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function GoInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/index.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/index.tsx index cc0382dd22581..0765b5bbf12f6 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/index.tsx @@ -1,11 +1,11 @@ export * from './android' +export * from './elixir' +export * from './flutter' export * from './go' -export * from './nodejs' export * from './ios' +export * from './js-web' +export * from './nodejs' export * from './php' export * from './python' export * from './react-native' export * from './ruby' -export * from './elixir' -export * from './flutter' -export * from './js-web' diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx index 314f4c0305343..ce68fc3a5225a 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function IOSInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx index 88b5e8acc8adc..f560a0f20d3a5 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx @@ -1,7 +1,7 @@ +import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { JSSnippet } from 'lib/components/JSSnippet' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' export function JSInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx index e3312f1e07dd8..f8f2c17e0be8d 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx @@ -1,7 +1,8 @@ -import { Link } from 'lib/lemon-ui/Link' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { Link } from 'lib/lemon-ui/Link' import { teamLogic } from 'scenes/teamLogic' + import { JSInstallSnippet } from './js-web' function NextEnvVarsSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nodejs.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nodejs.tsx index bab12bd12c45e..794cbbec86daa 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nodejs.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nodejs.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' export function NodeInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx index 136dee636404a..6b7aa26973ec1 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function PHPConfigSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/python.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/python.tsx index 54ece50952ec3..1f58740d96192 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/python.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/python.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function PythonInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx index 298cb434f6751..6b489e0088ad2 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx @@ -1,7 +1,7 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { Link } from '@posthog/lemon-ui' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' -import { Link } from '@posthog/lemon-ui' export function SDKInstallRNInstructions(): JSX.Element { const { currentTeam } = useValues(teamLogic) diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react.tsx index a066848419416..8941ac2f45c24 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react.tsx @@ -1,6 +1,7 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' + import { JSInstallSnippet } from './js-web' function ReactEnvVarsSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx index bd5521f351983..d51f97cd8c9e7 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx @@ -1,5 +1,5 @@ -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { useValues } from 'kea' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { teamLogic } from 'scenes/teamLogic' function RubyInstallSnippet(): JSX.Element { diff --git a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx index 83203a1ef7bc4..26c75e8d4d5f4 100644 --- a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx @@ -1,10 +1,11 @@ -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { LemonSelectOptions } from 'lib/lemon-ui/LemonSelect/LemonSelect' -import type { sdksLogicType } from './sdksLogicType' import { SDK, SDKInstructionsMap } from '~/types' + import { onboardingLogic } from '../onboardingLogic' import { allSDKs } from './allSDKs' -import { LemonSelectOptions } from 'lib/lemon-ui/LemonSelect/LemonSelect' +import type { sdksLogicType } from './sdksLogicType' /* To add SDK instructions for your product: @@ -40,6 +41,8 @@ export const sdksLogic = kea([ setSourceOptions: (sourceOptions: LemonSelectOptions) => ({ sourceOptions }), resetSDKs: true, setAvailableSDKInstructionsMap: (sdkInstructionMap: SDKInstructionsMap) => ({ sdkInstructionMap }), + setShowSideBySide: (showSideBySide: boolean) => ({ showSideBySide }), + setPanel: (panel: 'instructions' | 'options') => ({ panel }), }), reducers({ sourceFilter: [ @@ -72,6 +75,18 @@ export const sdksLogic = kea([ setAvailableSDKInstructionsMap: (_, { sdkInstructionMap }) => sdkInstructionMap, }, ], + showSideBySide: [ + null as boolean | null, + { + setShowSideBySide: (_, { showSideBySide }) => showSideBySide, + }, + ], + panel: [ + 'options' as 'instructions' | 'options', + { + setPanel: (_, { panel }) => panel, + }, + ], }), selectors({ showSourceOptionsSelect: [ @@ -100,7 +115,7 @@ export const sdksLogic = kea([ actions.filterSDKs() }, setSDKs: () => { - if (!values.selectedSDK) { + if (!values.selectedSDK && values.showSideBySide == true) { actions.setSelectedSDK(values.sdks?.[0] || null) } }, @@ -118,6 +133,16 @@ export const sdksLogic = kea([ actions.setSourceFilter(null) actions.setSourceOptions(getSourceOptions(values.availableSDKInstructionsMap)) }, + setSelectedSDK: () => { + if (values.selectedSDK) { + actions.setPanel('instructions') + } + }, + setShowSideBySide: () => { + if (values.showSideBySide && !values.selectedSDK) { + actions.setSelectedSDK(values.sdks?.[0] || null) + } + }, })), events(({ actions }) => ({ afterMount: () => { diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx index 81d8068d956ca..bf8566b5dcc77 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx @@ -1,4 +1,5 @@ import { SDKInstructionsMap, SDKKey } from '~/types' + import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' export const SessionReplaySDKInstructions: SDKInstructionsMap = { diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx index fc799bbd8a65a..d465faf1de9cb 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/js-web.tsx @@ -1,6 +1,7 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { SessionReplayFinalSteps } from '../shared-snippets' + import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' +import { SessionReplayFinalSteps } from '../shared-snippets' export function JSWebInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/next-js.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/next-js.tsx index dadd37388cb0a..8173f3be97115 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/next-js.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/next-js.tsx @@ -1,5 +1,5 @@ -import { SessionReplayFinalSteps } from '../shared-snippets' import { SDKInstallNextJSInstructions } from '../sdk-install-instructions/next-js' +import { SessionReplayFinalSteps } from '../shared-snippets' export function NextJSInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/react.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/react.tsx index 361884112a15b..f9863c945dd75 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/react.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/react.tsx @@ -1,5 +1,5 @@ -import { SessionReplayFinalSteps } from '../shared-snippets' import { SDKInstallReactInstructions } from '../sdk-install-instructions/react' +import { SessionReplayFinalSteps } from '../shared-snippets' export function ReactInstructions(): JSX.Element { return ( diff --git a/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx index 352a4b3d96d82..ad9db67494c88 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx @@ -1,4 +1,5 @@ import { SDKInstructionsMap, SDKKey } from '~/types' + import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' export const SurveysSDKInstructions: SDKInstructionsMap = { diff --git a/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx b/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx index 522c229904cc7..d478de9b3dd0b 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/js-web.tsx @@ -1,4 +1,5 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + import { SDKInstallJSWebInstructions } from '../sdk-install-instructions' import { SurveysFinalSteps } from './SurveysFinalSteps' diff --git a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx index 4a151a7d21b94..e62ccf55c058e 100644 --- a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx +++ b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx @@ -1,17 +1,18 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { organizationLogic } from 'scenes/organizationLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { confirmOrganizationLogic } from './confirmOrganizationLogic' -import { Field } from 'lib/forms/Field' -import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' import { Form } from 'kea-forms' +import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import SignupRoleSelect from 'lib/components/SignupRoleSelect' import SignupReferralSource from 'lib/components/SignupReferralSource' -import { Link } from '@posthog/lemon-ui' +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 { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { organizationLogic } from 'scenes/organizationLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { confirmOrganizationLogic } from './confirmOrganizationLogic' export const scene: SceneExport = { component: ConfirmOrganization, diff --git a/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.test.ts b/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.test.ts index 13c651dca5aa6..599f90de5ff89 100644 --- a/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.test.ts +++ b/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.test.ts @@ -1,7 +1,9 @@ -import { confirmOrganizationLogic } from './confirmOrganizationLogic' +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' + import { initKeaTests } from '~/test/init' -import { router } from 'kea-router' + +import { confirmOrganizationLogic } from './confirmOrganizationLogic' describe('confirmOrganizationLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.ts b/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.ts index c0c9126454968..84e328f82a51a 100644 --- a/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.ts +++ b/frontend/src/scenes/organization/ConfirmOrganization/confirmOrganizationLogic.ts @@ -1,11 +1,10 @@ import { actions, kea, path, reducers } from 'kea' - -import api from 'lib/api' +import { forms } from 'kea-forms' import { urlToAction } from 'kea-router' +import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import type { confirmOrganizationLogicType } from './confirmOrganizationLogicType' -import { forms } from 'kea-forms' -import { lemonToast } from 'lib/lemon-ui/lemonToast' export interface ConfirmOrganizationFormValues { organization_name?: string diff --git a/frontend/src/scenes/organization/Create/index.tsx b/frontend/src/scenes/organization/Create/index.tsx index 2c9d5d1aaa710..32c6c03580584 100644 --- a/frontend/src/scenes/organization/Create/index.tsx +++ b/frontend/src/scenes/organization/Create/index.tsx @@ -1,6 +1,7 @@ -import { CreateOrganizationModal } from '../CreateOrganizationModal' -import { SceneExport } from 'scenes/sceneTypes' import { organizationLogic } from 'scenes/organizationLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { CreateOrganizationModal } from '../CreateOrganizationModal' export const scene: SceneExport = { component: OrganizationCreate, diff --git a/frontend/src/scenes/organization/membersLogic.tsx b/frontend/src/scenes/organization/membersLogic.tsx index ebf874aaa5063..49a5ac406bc87 100644 --- a/frontend/src/scenes/organization/membersLogic.tsx +++ b/frontend/src/scenes/organization/membersLogic.tsx @@ -1,14 +1,16 @@ +import Fuse from 'fuse.js' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' import api from 'lib/api' -import type { membersLogicType } from './membersLogicType' import { OrganizationMembershipLevel } from 'lib/constants' -import { OrganizationMemberType } from '~/types' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { membershipLevelToName } from 'lib/utils/permissioning' import { organizationLogic } from 'scenes/organizationLogic' import { userLogic } from 'scenes/userLogic' -import { membershipLevelToName } from 'lib/utils/permissioning' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import Fuse from 'fuse.js' + +import { OrganizationMemberType } from '~/types' + +import type { membersLogicType } from './membersLogicType' export interface MembersFuse extends Fuse {} diff --git a/frontend/src/scenes/organizationLogic.test.ts b/frontend/src/scenes/organizationLogic.test.ts index 464a89b39f763..496f6f7307740 100644 --- a/frontend/src/scenes/organizationLogic.test.ts +++ b/frontend/src/scenes/organizationLogic.test.ts @@ -1,6 +1,8 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' import { MOCK_DEFAULT_ORGANIZATION } from 'lib/api.mock' + +import { initKeaTests } from '~/test/init' + import { AppContext } from '../types' import { organizationLogic } from './organizationLogic' diff --git a/frontend/src/scenes/organizationLogic.tsx b/frontend/src/scenes/organizationLogic.tsx index c582391f506ef..544ddd623d90d 100644 --- a/frontend/src/scenes/organizationLogic.tsx +++ b/frontend/src/scenes/organizationLogic.tsx @@ -1,13 +1,15 @@ import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' -import type { organizationLogicType } from './organizationLogicType' -import { AvailableFeature, OrganizationType } from '~/types' -import { userLogic } from './userLogic' -import { getAppContext } from 'lib/utils/getAppContext' +import { loaders } from 'kea-loaders' +import api, { ApiConfig } from 'lib/api' import { OrganizationMembershipLevel } from 'lib/constants' -import { isUserLoggedIn } from 'lib/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { loaders } from 'kea-loaders' +import { isUserLoggedIn } from 'lib/utils' +import { getAppContext } from 'lib/utils/getAppContext' + +import { AvailableFeature, OrganizationType } from '~/types' + +import type { organizationLogicType } from './organizationLogicType' +import { userLogic } from './userLogic' export type OrganizationUpdatePayload = Partial< Pick @@ -92,6 +94,11 @@ export const organizationLogic = kea([ ], }), listeners(({ actions }) => ({ + loadCurrentOrganizationSuccess: ({ currentOrganization }) => { + if (currentOrganization) { + ApiConfig.setCurrentOrganizationId(currentOrganization.id) + } + }, createOrganizationSuccess: () => { window.location.href = '/organization/members' }, diff --git a/frontend/src/scenes/paths/PathNodeCard.tsx b/frontend/src/scenes/paths/PathNodeCard.tsx index 7c5a6a4f6e786..2f999f94bef03 100644 --- a/frontend/src/scenes/paths/PathNodeCard.tsx +++ b/frontend/src/scenes/paths/PathNodeCard.tsx @@ -1,14 +1,14 @@ -import { useActions, useValues } from 'kea' +import { Tooltip } from '@posthog/lemon-ui' import { Dropdown } from 'antd' +import { useActions, useValues } from 'kea' import { InsightLogicProps } from '~/types' -import { pageUrl, isSelectedPathStartOrEnd, PathNodeData } from './pathUtils' -import { PathNodeCardMenu } from './PathNodeCardMenu' -import { PathNodeCardButton } from './PathNodeCardButton' import { PATH_NODE_CARD_LEFT_OFFSET, PATH_NODE_CARD_TOP_OFFSET, PATH_NODE_CARD_WIDTH } from './constants' +import { PathNodeCardButton } from './PathNodeCardButton' +import { PathNodeCardMenu } from './PathNodeCardMenu' import { pathsDataLogic } from './pathsDataLogic' -import { Tooltip } from '@posthog/lemon-ui' +import { isSelectedPathStartOrEnd, pageUrl, PathNodeData } from './pathUtils' export type PathNodeCardProps = { insightProps: InsightLogicProps diff --git a/frontend/src/scenes/paths/PathNodeCardButton.tsx b/frontend/src/scenes/paths/PathNodeCardButton.tsx index 9923b6f2eac19..b6f29e57739c7 100644 --- a/frontend/src/scenes/paths/PathNodeCardButton.tsx +++ b/frontend/src/scenes/paths/PathNodeCardButton.tsx @@ -1,14 +1,14 @@ +import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' import { useValues } from 'kea' - +import { IconEllipsis } from 'lib/lemon-ui/icons' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { userLogic } from 'scenes/userLogic' import { AvailableFeature, PathsFilterType } from '~/types' -import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' -import { IconEllipsis } from 'lib/lemon-ui/icons' -import { copyToClipboard } from 'lib/utils' -import { pageUrl, PathNodeData } from './pathUtils' import { pathsDataLogicType } from './pathsDataLogicType' +import { pageUrl, PathNodeData } from './pathUtils' type PathNodeCardButton = { name: string @@ -40,8 +40,8 @@ export function PathNodeCardButton({ const viewFunnel = (): void => { viewPathToFunnel(node) } - const copyName = async (): Promise => { - await copyToClipboard(pageUrl(node)) + const copyName = (): void => { + void copyToClipboard(pageUrl(node)).then(captureException) } const openModal = (): void => openPersonsModal({ path_end_key: name }) diff --git a/frontend/src/scenes/paths/PathNodeCardMenu.tsx b/frontend/src/scenes/paths/PathNodeCardMenu.tsx index d860ec441cc8c..ec61abdd3d935 100644 --- a/frontend/src/scenes/paths/PathNodeCardMenu.tsx +++ b/frontend/src/scenes/paths/PathNodeCardMenu.tsx @@ -1,11 +1,10 @@ -import { MouseEventHandler } from 'react' - import { LemonButton } from '@posthog/lemon-ui' -import { IconTrendingFlat, IconTrendingFlatDown, IconSchedule } from 'lib/lemon-ui/icons' +import { IconSchedule, IconTrendingFlat, IconTrendingFlatDown } from 'lib/lemon-ui/icons' import { humanFriendlyDuration } from 'lib/utils' +import { MouseEventHandler } from 'react' -import { pathsDataLogicType } from './pathsDataLogicType' import { PATH_NODE_CARD_WIDTH } from './constants' +import { pathsDataLogicType } from './pathsDataLogicType' type PathNodeCardMenuProps = { name: string diff --git a/frontend/src/scenes/paths/Paths.tsx b/frontend/src/scenes/paths/Paths.tsx index 7ddebb7033fb0..e865923142e17 100644 --- a/frontend/src/scenes/paths/Paths.tsx +++ b/frontend/src/scenes/paths/Paths.tsx @@ -1,16 +1,15 @@ -import { useRef, useEffect, useState } from 'react' +import './Paths.scss' + import { useValues } from 'kea' import { useResizeObserver } from 'lib/hooks/useResizeObserver' - +import { useEffect, useRef, useState } from 'react' +import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' import { insightLogic } from 'scenes/insights/insightLogic' -import { pathsDataLogic } from './pathsDataLogic' -import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' import { PathNodeCard } from './PathNodeCard' -import { renderPaths } from './renderPaths' +import { pathsDataLogic } from './pathsDataLogic' import type { PathNodeData } from './pathUtils' - -import './Paths.scss' +import { renderPaths } from './renderPaths' const DEFAULT_PATHS_ID = 'default_paths' export const HIDE_PATH_CARD_HEIGHT = 30 diff --git a/frontend/src/scenes/paths/pathUtils.ts b/frontend/src/scenes/paths/pathUtils.ts index 4ca7ee1bd93b4..3458de87e6a03 100644 --- a/frontend/src/scenes/paths/pathUtils.ts +++ b/frontend/src/scenes/paths/pathUtils.ts @@ -1,4 +1,5 @@ import { RGBColor } from 'd3' + import { FunnelPathType, PathsFilterType } from '~/types' export interface PathTargetLink { diff --git a/frontend/src/scenes/paths/pathsDataLogic.test.ts b/frontend/src/scenes/paths/pathsDataLogic.test.ts index 85745f976ab62..15b7dac69bcef 100644 --- a/frontend/src/scenes/paths/pathsDataLogic.test.ts +++ b/frontend/src/scenes/paths/pathsDataLogic.test.ts @@ -1,10 +1,9 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' - +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' +import { initKeaTests } from '~/test/init' import { InsightLogicProps, InsightType, PathType } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' let logic: ReturnType diff --git a/frontend/src/scenes/paths/pathsDataLogic.ts b/frontend/src/scenes/paths/pathsDataLogic.ts index 7a9796901f2b7..22af56a4c585c 100644 --- a/frontend/src/scenes/paths/pathsDataLogic.ts +++ b/frontend/src/scenes/paths/pathsDataLogic.ts @@ -1,26 +1,27 @@ -import { kea, path, props, key, connect, selectors, actions, listeners } from 'kea' +import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea' +import { router } from 'kea-router' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { buildPeopleUrl, pathsTitle } from 'scenes/trends/persons-modal/persons-modal-utils' +import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' +import { urls } from 'scenes/urls' + +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { InsightQueryNode } from '~/queries/schema' +import { isPathsQuery } from '~/queries/utils' import { + ActionFilter, InsightLogicProps, - PathType, - PathsFilterType, InsightType, - ActionFilter, - PropertyOperator, + PathsFilterType, + PathType, PropertyFilterType, + PropertyOperator, } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import type { pathsDataLogicType } from './pathsDataLogicType' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { isPathsQuery } from '~/queries/utils' import { PathNodeData } from './pathUtils' -import { buildPeopleUrl, pathsTitle } from 'scenes/trends/persons-modal/persons-modal-utils' -import { openPersonsModal } from 'scenes/trends/persons-modal/PersonsModal' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { InsightQueryNode } from '~/queries/schema' export const DEFAULT_STEP_LIMIT = 5 diff --git a/frontend/src/scenes/paths/renderPaths.ts b/frontend/src/scenes/paths/renderPaths.ts index 22244d11d1727..891ebd67665fa 100644 --- a/frontend/src/scenes/paths/renderPaths.ts +++ b/frontend/src/scenes/paths/renderPaths.ts @@ -1,14 +1,14 @@ -import { Dispatch, RefObject, SetStateAction } from 'react' import * as d3 from 'd3' import * as Sankey from 'd3-sankey' +import { D3Selector } from 'lib/hooks/useD3' +import { stripHTTP } from 'lib/utils' +import { Dispatch, RefObject, SetStateAction } from 'react' import { PathsFilterType } from '~/types' -import { stripHTTP } from 'lib/utils' -import { D3Selector } from 'lib/hooks/useD3' -import { HIDE_PATH_CARD_HEIGHT, FALLBACK_CANVAS_WIDTH } from './Paths' -import { roundedRect, isSelectedPathStartOrEnd, PathNodeData, PathTargetLink } from './pathUtils' +import { FALLBACK_CANVAS_WIDTH, HIDE_PATH_CARD_HEIGHT } from './Paths' import { PathNode } from './pathsDataLogic' +import { isSelectedPathStartOrEnd, PathNodeData, PathTargetLink, roundedRect } from './pathUtils' const createCanvas = (canvasRef: RefObject, width: number, height: number): D3Selector => { return d3 diff --git a/frontend/src/scenes/persons-management/PersonsManagementScene.tsx b/frontend/src/scenes/persons-management/PersonsManagementScene.tsx index 156f54d1d6342..87e1f318f7216 100644 --- a/frontend/src/scenes/persons-management/PersonsManagementScene.tsx +++ b/frontend/src/scenes/persons-management/PersonsManagementScene.tsx @@ -1,10 +1,11 @@ import { useActions, useValues } from 'kea' +import { PageHeader } from 'lib/components/PageHeader' import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { SceneExport } from 'scenes/sceneTypes' import { groupsModel } from '~/models/groupsModel' -import { PageHeader } from 'lib/components/PageHeader' + import { personsManagementSceneLogic } from './personsManagementSceneLogic' -import { SceneExport } from 'scenes/sceneTypes' export function PersonsManagementScene(): JSX.Element { const { tabs, activeTab, tabKey } = useValues(personsManagementSceneLogic) diff --git a/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx b/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx index 0e1202affc14c..7734a37566197 100644 --- a/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx +++ b/frontend/src/scenes/persons-management/personsManagementSceneLogic.tsx @@ -1,18 +1,19 @@ +import { LemonButton } from '@posthog/lemon-ui' import { actions, connect, kea, path, reducers, selectors } from 'kea' import { actionToUrl, router, urlToAction } from 'kea-router' -import { urls } from 'scenes/urls' +import { GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' import { LemonTab } from 'lib/lemon-ui/LemonTabs' -import { Breadcrumb } from '~/types' import { capitalizeFirstLetter } from 'lib/utils' -import { GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' +import { Cohorts } from 'scenes/cohorts/Cohorts' +import { Groups } from 'scenes/groups/Groups' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { groupsModel } from '~/models/groupsModel' -import { Persons } from './tabs/Persons' -import { Cohorts } from 'scenes/cohorts/Cohorts' -import { LemonButton } from '@posthog/lemon-ui' +import { Breadcrumb } from '~/types' import type { personsManagementSceneLogicType } from './personsManagementSceneLogicType' -import { Groups } from 'scenes/groups/Groups' +import { Persons } from './tabs/Persons' export type PersonsManagementTab = { key: string @@ -51,7 +52,7 @@ export const personsManagementSceneLogic = kea( { key: 'persons', url: urls.persons(), - label: 'Persons', + label: 'People', content: , }, { @@ -116,15 +117,18 @@ export const personsManagementSceneLogic = kea( (tabs, activeTab): Breadcrumb[] => { return [ { + key: Scene.PersonsManagement, name: `People`, path: tabs[0].url, }, activeTab ? { + key: activeTab.key, name: activeTab.label, path: activeTab.url, } : { + key: 'loading', name: 'Loading...', }, ] diff --git a/frontend/src/scenes/persons-management/tabs/Persons.tsx b/frontend/src/scenes/persons-management/tabs/Persons.tsx index 15fb70d8f518f..5adb633e66ffb 100644 --- a/frontend/src/scenes/persons-management/tabs/Persons.tsx +++ b/frontend/src/scenes/persons-management/tabs/Persons.tsx @@ -1,5 +1,6 @@ import { useActions, useValues } from 'kea' import { personsSceneLogic } from 'scenes/persons-management/tabs/personsSceneLogic' + import { Query } from '~/queries/Query/Query' export function Persons(): JSX.Element { diff --git a/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts b/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts index b1c5321c99253..a96c0291d2379 100644 --- a/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts +++ b/frontend/src/scenes/persons-management/tabs/personsSceneLogic.ts @@ -1,11 +1,11 @@ import { actions, connect, kea, path, reducers, selectors } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' import { DataTableNode, Node, NodeKind } from '~/queries/schema' import type { personsSceneLogicType } from './personsSceneLogicType' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' const getDefaultQuery = (usePersonsQuery = false): DataTableNode => ({ kind: NodeKind.DataTableNode, diff --git a/frontend/src/scenes/persons/GroupActorDisplay.tsx b/frontend/src/scenes/persons/GroupActorDisplay.tsx index a39c4b9bab7a9..a41d42b07f987 100644 --- a/frontend/src/scenes/persons/GroupActorDisplay.tsx +++ b/frontend/src/scenes/persons/GroupActorDisplay.tsx @@ -1,8 +1,10 @@ -import { GroupActorType } from '~/types' import './PersonDisplay.scss' + import { Link } from 'lib/lemon-ui/Link' import { urls } from 'scenes/urls' +import { GroupActorType } from '~/types' + export interface GroupActorDisplayProps { actor: GroupActorType } diff --git a/frontend/src/scenes/persons/MergeSplitPerson.tsx b/frontend/src/scenes/persons/MergeSplitPerson.tsx index 8f1796c0c56d6..ca9725ee9587f 100644 --- a/frontend/src/scenes/persons/MergeSplitPerson.tsx +++ b/frontend/src/scenes/persons/MergeSplitPerson.tsx @@ -1,10 +1,13 @@ -import { Select, Modal } from 'antd' -import { PersonType } from '~/types' -import { useActions, useValues, BindLogic } from 'kea' import './MergeSplitPerson.scss' -import { mergeSplitPersonLogic } from './mergeSplitPersonLogic' -import { pluralize } from 'lib/utils' + +import { Modal, Select } from 'antd' +import { BindLogic, useActions, useValues } from 'kea' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { pluralize } from 'lib/utils' + +import { PersonType } from '~/types' + +import { mergeSplitPersonLogic } from './mergeSplitPersonLogic' export function MergeSplitPerson({ person }: { person: PersonType }): JSX.Element { const logicProps = { person } diff --git a/frontend/src/scenes/persons/NewProperty.tsx b/frontend/src/scenes/persons/NewProperty.tsx index b0ddfda001016..3df2b183ff87c 100644 --- a/frontend/src/scenes/persons/NewProperty.tsx +++ b/frontend/src/scenes/persons/NewProperty.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInput, LemonLabel, LemonModal, LemonSegmentedButton } from '@posthog/lemon-ui' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useState } from 'react' interface NewPropertyInterface { creating: boolean diff --git a/frontend/src/scenes/persons/PersonCohorts.tsx b/frontend/src/scenes/persons/PersonCohorts.tsx index 7ae2d11912d38..66ebfa04144f1 100644 --- a/frontend/src/scenes/persons/PersonCohorts.tsx +++ b/frontend/src/scenes/persons/PersonCohorts.tsx @@ -1,10 +1,12 @@ -import { useEffect } from 'react' import { useActions, useValues } from 'kea' -import { personsLogic } from './personsLogic' -import { CohortType } from '~/types' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { urls } from 'scenes/urls' import { Link } from 'lib/lemon-ui/Link' +import { useEffect } from 'react' +import { urls } from 'scenes/urls' + +import { CohortType } from '~/types' + +import { personsLogic } from './personsLogic' export function PersonCohorts(): JSX.Element { const { cohorts, cohortsLoading, person } = useValues(personsLogic) diff --git a/frontend/src/scenes/persons/PersonDashboard.tsx b/frontend/src/scenes/persons/PersonDashboard.tsx index 877bd35efb3c3..9f58578487ba4 100644 --- a/frontend/src/scenes/persons/PersonDashboard.tsx +++ b/frontend/src/scenes/persons/PersonDashboard.tsx @@ -1,14 +1,15 @@ -import { HogQLPropertyFilter, PersonType, PropertyFilterType } from '~/types' -import { Scene } from 'scenes/sceneTypes' -import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' +import { useActions, useValues } from 'kea' import { SceneDashboardChoiceModal } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceModal' import { sceneDashboardChoiceModalLogic } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' -import { useActions, useValues } from 'kea' -import { Dashboard } from 'scenes/dashboard/Dashboard' -import { personDashboardLogic } from 'scenes/persons/personDashboardLogic' +import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { useEffect } from 'react' +import { Dashboard } from 'scenes/dashboard/Dashboard' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { personDashboardLogic } from 'scenes/persons/personDashboardLogic' +import { Scene } from 'scenes/sceneTypes' + +import { HogQLPropertyFilter, PersonType, PropertyFilterType } from '~/types' export function PersonDashboard({ person }: { person: PersonType }): JSX.Element { const { showSceneDashboardChoiceModal } = useActions(sceneDashboardChoiceModalLogic({ scene: Scene.Person })) diff --git a/frontend/src/scenes/persons/PersonDeleteModal.tsx b/frontend/src/scenes/persons/PersonDeleteModal.tsx index 4a026ec4c5b3b..d695c33850aac 100644 --- a/frontend/src/scenes/persons/PersonDeleteModal.tsx +++ b/frontend/src/scenes/persons/PersonDeleteModal.tsx @@ -1,7 +1,9 @@ -import { useActions, useValues } from 'kea' import { LemonButton, LemonModal, Link } from '@posthog/lemon-ui' -import { PersonType } from '~/types' +import { useActions, useValues } from 'kea' import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' + +import { PersonType } from '~/types' + import { asDisplay } from './person-utils' export function PersonDeleteModal(): JSX.Element | null { diff --git a/frontend/src/scenes/persons/PersonDisplay.scss b/frontend/src/scenes/persons/PersonDisplay.scss index 9d65707aabd8d..1a4c0a82b7a14 100644 --- a/frontend/src/scenes/persons/PersonDisplay.scss +++ b/frontend/src/scenes/persons/PersonDisplay.scss @@ -1,9 +1,11 @@ .PersonDisplay { display: inline; + .ProfilePicture { transition: opacity 200ms ease; margin-right: 0.5rem; } + a:hover { .ProfilePicture { opacity: 0.75; diff --git a/frontend/src/scenes/persons/PersonDisplay.tsx b/frontend/src/scenes/persons/PersonDisplay.tsx index 06b3e4b2158ad..3d2cb1d9b758c 100644 --- a/frontend/src/scenes/persons/PersonDisplay.tsx +++ b/frontend/src/scenes/persons/PersonDisplay.tsx @@ -1,15 +1,18 @@ import './PersonDisplay.scss' -import { Link } from 'lib/lemon-ui/Link' -import { ProfilePicture, ProfilePictureProps } from 'lib/lemon-ui/ProfilePicture' + import clsx from 'clsx' +import { router } from 'kea-router' +import { Link } from 'lib/lemon-ui/Link' import { Popover } from 'lib/lemon-ui/Popover' -import { PersonPreview } from './PersonPreview' +import { ProfilePicture, ProfilePictureProps } from 'lib/lemon-ui/ProfilePicture' import { useMemo, useState } from 'react' -import { router } from 'kea-router' -import { asDisplay, asLink } from './person-utils' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' + import { NotebookNodeType } from '~/types' +import { asDisplay, asLink } from './person-utils' +import { PersonPreview } from './PersonPreview' + type PersonPropType = | { properties?: Record; distinct_ids?: string[]; distinct_id?: never } | { properties?: Record; distinct_ids?: never; distinct_id?: string } diff --git a/frontend/src/scenes/persons/PersonFeedCanvas.tsx b/frontend/src/scenes/persons/PersonFeedCanvas.tsx index fd3ebd2448934..958da87425fb9 100644 --- a/frontend/src/scenes/persons/PersonFeedCanvas.tsx +++ b/frontend/src/scenes/persons/PersonFeedCanvas.tsx @@ -1,10 +1,10 @@ import { useValues } from 'kea' - -import { PersonType } from '~/types' -import { Notebook } from 'scenes/notebooks/Notebook/Notebook' import { uuid } from 'lib/utils' +import { Notebook } from 'scenes/notebooks/Notebook/Notebook' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { PersonType } from '~/types' + type PersonFeedCanvasProps = { person: PersonType } diff --git a/frontend/src/scenes/persons/PersonPreview.tsx b/frontend/src/scenes/persons/PersonPreview.tsx index b303e6a05ba92..2c874726304c7 100644 --- a/frontend/src/scenes/persons/PersonPreview.tsx +++ b/frontend/src/scenes/persons/PersonPreview.tsx @@ -1,14 +1,16 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { personLogic } from './personLogic' -import { Spinner } from 'lib/lemon-ui/Spinner' +import { PropertiesTable } from 'lib/components/PropertiesTable' +import { IconOpenInNew } from 'lib/lemon-ui/icons' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { LemonButton, Link } from '@posthog/lemon-ui' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { urls } from 'scenes/urls' -import { PropertiesTable } from 'lib/components/PropertiesTable' + import { NotebookNodeType, PropertyDefinitionType } from '~/types' -import { IconOpenInNew } from 'lib/lemon-ui/icons' + import { asDisplay } from './person-utils' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { personLogic } from './personLogic' export type PersonPreviewProps = { distinctId: string | undefined diff --git a/frontend/src/scenes/persons/PersonScene.scss b/frontend/src/scenes/persons/PersonScene.scss index b96d0e15713e2..e555d6434aecb 100644 --- a/frontend/src/scenes/persons/PersonScene.scss +++ b/frontend/src/scenes/persons/PersonScene.scss @@ -5,8 +5,9 @@ line-height: 1.125rem; margin: 0 0 0 0.25rem; padding: 0 0.25rem 0 0.375rem; - color: var(--primary); + color: var(--primary-3000); cursor: pointer; + svg { margin-left: 0.25rem; } diff --git a/frontend/src/scenes/persons/PersonScene.tsx b/frontend/src/scenes/persons/PersonScene.tsx index 6459e73dab82e..b570b1d9d7bac 100644 --- a/frontend/src/scenes/persons/PersonScene.tsx +++ b/frontend/src/scenes/persons/PersonScene.tsx @@ -1,41 +1,44 @@ -import { Dropdown, Menu } from 'antd' +import './PersonScene.scss' + // eslint-disable-next-line no-restricted-imports import { DownOutlined } from '@ant-design/icons' +import { LemonButton, LemonDivider, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' +import { Dropdown, Menu } from 'antd' import { useActions, useValues } from 'kea' -import { personsLogic } from './personsLogic' -import { PersonDisplay } from './PersonDisplay' -import './PersonScene.scss' +import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { MergeSplitPerson } from './MergeSplitPerson' -import { PersonCohorts } from './PersonCohorts' +import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' import { PropertiesTable } from 'lib/components/PropertiesTable' import { TZLabel } from 'lib/components/TZLabel' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { NotebookNodeType, PersonsTabType, PersonType, PropertyDefinitionType } from '~/types' -import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' -import { RelatedGroups } from 'scenes/groups/RelatedGroups' import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { LemonButton, LemonDivider, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' -import { teamLogic } from 'scenes/teamLogic' +import { IconInfo } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { NotFound } from 'lib/components/NotFound' -import { RelatedFeatureFlags } from './RelatedFeatureFlags' -import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { RelatedGroups } from 'scenes/groups/RelatedGroups' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' -import { IconInfo } from 'lib/lemon-ui/icons' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { NotebookNodeType, PersonsTabType, PersonType, PropertyDefinitionType } from '~/types' + +import { MergeSplitPerson } from './MergeSplitPerson' +import { PersonCohorts } from './PersonCohorts' import { PersonDashboard } from './PersonDashboard' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' -import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' +import { PersonDisplay } from './PersonDisplay' import PersonFeedCanvas from './PersonFeedCanvas' +import { personsLogic } from './personsLogic' +import { RelatedFeatureFlags } from './RelatedFeatureFlags' export const scene: SceneExport = { component: PersonScene, diff --git a/frontend/src/scenes/persons/PersonsSearch.tsx b/frontend/src/scenes/persons/PersonsSearch.tsx index 8b4c5166cad21..a222715637bae 100644 --- a/frontend/src/scenes/persons/PersonsSearch.tsx +++ b/frontend/src/scenes/persons/PersonsSearch.tsx @@ -1,11 +1,12 @@ -import { useEffect, useState } from 'react' -import { useValues, useActions } from 'kea' -import { personsLogic } from './personsLogic' +import { LemonInput } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { IconInfo } from 'lib/lemon-ui/icons' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { LemonInput } from '@posthog/lemon-ui' +import { useEffect, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' +import { personsLogic } from './personsLogic' + export const PersonsSearch = (): JSX.Element => { const { loadPersons, setListFilters } = useActions(personsLogic) const { listFilters } = useValues(personsLogic) diff --git a/frontend/src/scenes/persons/PersonsTable.tsx b/frontend/src/scenes/persons/PersonsTable.tsx index c3d8a2f456fe6..3131fb28b6c87 100644 --- a/frontend/src/scenes/persons/PersonsTable.tsx +++ b/frontend/src/scenes/persons/PersonsTable.tsx @@ -1,16 +1,18 @@ -import { TZLabel } from 'lib/components/TZLabel' -import { PropertiesTable } from 'lib/components/PropertiesTable' -import { PersonType, PropertyDefinitionType } from '~/types' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { PersonDisplay } from './PersonDisplay' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { LemonButton } from '@posthog/lemon-ui' -import { IconDelete } from 'lib/lemon-ui/icons' import { useActions } from 'kea' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { PropertiesTable } from 'lib/components/PropertiesTable' +import { TZLabel } from 'lib/components/TZLabel' +import { IconDelete } from 'lib/lemon-ui/icons' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' import { personDeleteModalLogic } from 'scenes/persons/personDeleteModalLogic' import { personsLogic } from 'scenes/persons/personsLogic' +import { PersonType, PropertyDefinitionType } from '~/types' + +import { PersonDisplay } from './PersonDisplay' + interface PersonsTableType { people: PersonType[] loading?: boolean diff --git a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx index 3e1505f4b1aeb..c641a38c478a2 100644 --- a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx +++ b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx @@ -1,13 +1,15 @@ import { LemonInput, LemonSelect, LemonTable, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { normalizeColumnTitle } from 'lib/components/Table/utils' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { capitalizeFirstLetter } from 'lib/utils' import stringWithWBR from 'lib/utils/stringWithWBR' import { urls } from 'scenes/urls' + import { FeatureFlagReleaseType } from '~/types' -import { relatedFeatureFlagsLogic, RelatedFeatureFlag } from './relatedFeatureFlagsLogic' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' + +import { RelatedFeatureFlag, relatedFeatureFlagsLogic } from './relatedFeatureFlagsLogic' interface Props { distinctId: string diff --git a/frontend/src/scenes/persons/activityDescriptions.tsx b/frontend/src/scenes/persons/activityDescriptions.tsx index f11568827c9dd..ffca0f970d702 100644 --- a/frontend/src/scenes/persons/activityDescriptions.tsx +++ b/frontend/src/scenes/persons/activityDescriptions.tsx @@ -1,7 +1,7 @@ import { ActivityLogItem, HumanizedChange } from 'lib/components/ActivityLog/humanizeActivity' -import { PersonDisplay } from 'scenes/persons/PersonDisplay' import { SentenceList } from 'lib/components/ActivityLog/SentenceList' import { Link } from 'lib/lemon-ui/Link' +import { PersonDisplay } from 'scenes/persons/PersonDisplay' import { urls } from 'scenes/urls' export function personActivityDescriber(logItem: ActivityLogItem): HumanizedChange { diff --git a/frontend/src/scenes/persons/mergeSplitPersonLogic.ts b/frontend/src/scenes/persons/mergeSplitPersonLogic.ts index 3e5c4dae2417f..524d7de4de293 100644 --- a/frontend/src/scenes/persons/mergeSplitPersonLogic.ts +++ b/frontend/src/scenes/persons/mergeSplitPersonLogic.ts @@ -1,10 +1,12 @@ +import { actions, connect, events, kea, key, listeners, path, props, reducers } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, listeners, events } from 'kea' import { router } from 'kea-router' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' + import { PersonType } from '~/types' + import type { mergeSplitPersonLogicType } from './mergeSplitPersonLogicType' import { personsLogic } from './personsLogic' diff --git a/frontend/src/scenes/persons/person-utils.test.ts b/frontend/src/scenes/persons/person-utils.test.ts index 2baca39945f4b..6fe01850ac000 100644 --- a/frontend/src/scenes/persons/person-utils.test.ts +++ b/frontend/src/scenes/persons/person-utils.test.ts @@ -1,7 +1,9 @@ -import { PersonType } from '~/types' import { uuid } from 'lib/utils' import { urls } from 'scenes/urls' -import { asLink, asDisplay } from './person-utils' + +import { PersonType } from '~/types' + +import { asDisplay, asLink } from './person-utils' describe('the person header', () => { describe('linking to a person', () => { diff --git a/frontend/src/scenes/persons/person-utils.ts b/frontend/src/scenes/persons/person-utils.ts index 35928473b08be..a91a1a08e029b 100644 --- a/frontend/src/scenes/persons/person-utils.ts +++ b/frontend/src/scenes/persons/person-utils.ts @@ -1,9 +1,10 @@ import './PersonDisplay.scss' -import { urls } from 'scenes/urls' -import { ProfilePictureProps } from 'lib/lemon-ui/ProfilePicture' -import { teamLogic } from 'scenes/teamLogic' + import { PERSON_DEFAULT_DISPLAY_NAME_PROPERTIES } from 'lib/constants' +import { ProfilePictureProps } from 'lib/lemon-ui/ProfilePicture' import { midEllipsis } from 'lib/utils' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' type PersonPropType = | { properties?: Record; distinct_ids?: string[]; distinct_id?: never } diff --git a/frontend/src/scenes/persons/personDashboardLogic.ts b/frontend/src/scenes/persons/personDashboardLogic.ts index 2d9b3fd123218..5c97b78ede12e 100644 --- a/frontend/src/scenes/persons/personDashboardLogic.ts +++ b/frontend/src/scenes/persons/personDashboardLogic.ts @@ -1,11 +1,12 @@ -import { connect, kea, selectors, path } from 'kea' - -import type { personDashboardLogicType } from './personDashboardLogicType' -import { DashboardPlacement, PersonType } from '~/types' +import { connect, kea, path, selectors } from 'kea' import { DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' import { Scene } from 'scenes/sceneTypes' import { userLogic } from 'scenes/userLogic' +import { DashboardPlacement, PersonType } from '~/types' + +import type { personDashboardLogicType } from './personDashboardLogicType' + export interface PersonDashboardLogicProps { person: PersonType } diff --git a/frontend/src/scenes/persons/personDeleteModalLogic.tsx b/frontend/src/scenes/persons/personDeleteModalLogic.tsx index 7c9dd0343392c..07da7f430e345 100644 --- a/frontend/src/scenes/persons/personDeleteModalLogic.tsx +++ b/frontend/src/scenes/persons/personDeleteModalLogic.tsx @@ -1,11 +1,13 @@ -import { actions, kea, props, reducers, path } from 'kea' +import { actions, kea, path, props, reducers } from 'kea' +import { loaders } from 'kea-loaders' import api from 'lib/api' -import { PersonType } from '~/types' -import { toParams } from 'lib/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import type { personDeleteModalLogicType } from './personDeleteModalLogicType' -import { loaders } from 'kea-loaders' +import { toParams } from 'lib/utils' + +import { PersonType } from '~/types' + import { asDisplay } from './person-utils' +import type { personDeleteModalLogicType } from './personDeleteModalLogicType' export interface PersonDeleteModalLogicProps { person: PersonType diff --git a/frontend/src/scenes/persons/personLogic.tsx b/frontend/src/scenes/persons/personLogic.tsx index 9718cdd720de6..93e655f8757f9 100644 --- a/frontend/src/scenes/persons/personLogic.tsx +++ b/frontend/src/scenes/persons/personLogic.tsx @@ -1,7 +1,8 @@ import { actions, afterMount, kea, key, path, props } from 'kea' +import { loaders } from 'kea-loaders' import api from 'lib/api' + import { PersonType } from '~/types' -import { loaders } from 'kea-loaders' import type { personLogicType } from './personLogicType' diff --git a/frontend/src/scenes/persons/personsLogic.test.ts b/frontend/src/scenes/persons/personsLogic.test.ts index f8bc7fbc3a53f..d34a46fe6c748 100644 --- a/frontend/src/scenes/persons/personsLogic.test.ts +++ b/frontend/src/scenes/persons/personsLogic.test.ts @@ -1,12 +1,13 @@ +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' +import api from 'lib/api' +import { MOCK_TEAM_ID } from 'lib/api.mock' + +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import { personsLogic } from './personsLogic' -import { router } from 'kea-router' import { PropertyFilterType, PropertyOperator } from '~/types' -import { useMocks } from '~/mocks/jest' -import api from 'lib/api' -import { MOCK_TEAM_ID } from 'lib/api.mock' +import { personsLogic } from './personsLogic' describe('personsLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/persons/personsLogic.tsx b/frontend/src/scenes/persons/personsLogic.tsx index 136d69f317baf..e87d7f3d749f3 100644 --- a/frontend/src/scenes/persons/personsLogic.tsx +++ b/frontend/src/scenes/persons/personsLogic.tsx @@ -1,29 +1,32 @@ +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import { decodeParams, router, actionToUrl, urlToAction } from 'kea-router' +import { actionToUrl, decodeParams, router, urlToAction } from 'kea-router' import api, { CountedPaginatedResponse } from 'lib/api' -import type { personsLogicType } from './personsLogicType' +import { TriggerExportProps } from 'lib/components/ExportButton/exporter' +import { convertPropertyGroupToProperties, isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' +import { FEATURE_FLAGS } from 'lib/constants' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { Scene } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { hogqlQuery } from '~/queries/query' import { - PersonPropertyFilter, + AnyPropertyFilter, Breadcrumb, CohortType, ExporterFormat, PersonListParams, + PersonPropertyFilter, PersonsTabType, PersonType, - AnyPropertyFilter, } from '~/types' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { urls } from 'scenes/urls' -import { teamLogic } from 'scenes/teamLogic' -import { convertPropertyGroupToProperties, toParams } from 'lib/utils' -import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { TriggerExportProps } from 'lib/components/ExportButton/exporter' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' + import { asDisplay } from './person-utils' -import { hogqlQuery } from '~/queries/query' +import type { personsLogicType } from './personsLogicType' export interface PersonsLogicProps { cohort?: number | 'new' @@ -246,12 +249,14 @@ export const personsLogic = kea([ const showPerson = person && location.pathname.match(/\/person\/.+/) const breadcrumbs: Breadcrumb[] = [ { - name: 'Persons', + key: Scene.PersonsManagement, + name: 'People', path: urls.persons(), }, ] if (showPerson) { breadcrumbs.push({ + key: person.id || 'unknown', name: asDisplay(person), }) } diff --git a/frontend/src/scenes/persons/relatedFeatureFlagsLogic.ts b/frontend/src/scenes/persons/relatedFeatureFlagsLogic.ts index 4f8fc9817ea4a..7f60dfda38c67 100644 --- a/frontend/src/scenes/persons/relatedFeatureFlagsLogic.ts +++ b/frontend/src/scenes/persons/relatedFeatureFlagsLogic.ts @@ -1,13 +1,14 @@ import Fuse from 'fuse.js' -import { actions, connect, events, kea, key, path, props, selectors, reducers } from 'kea' +import { actions, connect, events, kea, key, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import { toParams } from 'lib/utils' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { teamLogic } from 'scenes/teamLogic' + import { FeatureFlagReleaseType, FeatureFlagType } from '~/types' -import { FeatureFlagMatchReason } from './RelatedFeatureFlags' +import { FeatureFlagMatchReason } from './RelatedFeatureFlags' import type { relatedFeatureFlagsLogicType } from './relatedFeatureFlagsLogicType' export interface RelatedFeatureFlag extends FeatureFlagType { value: boolean | string diff --git a/frontend/src/scenes/pipeline/NewButton.tsx b/frontend/src/scenes/pipeline/NewButton.tsx index 1b453ae8beee0..96ea31b811189 100644 --- a/frontend/src/scenes/pipeline/NewButton.tsx +++ b/frontend/src/scenes/pipeline/NewButton.tsx @@ -1,8 +1,10 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton/LemonButton' import { urls } from 'scenes/urls' -import { singularName } from './pipelineLogic' + import { PipelineTabs } from '~/types' +import { singularName } from './pipelineLogic' + type NewButtonProps = { tab: PipelineTabs } diff --git a/frontend/src/scenes/pipeline/Pipeline.stories.tsx b/frontend/src/scenes/pipeline/Pipeline.stories.tsx index 639cf3d1d8cea..c59cacefe547e 100644 --- a/frontend/src/scenes/pipeline/Pipeline.stories.tsx +++ b/frontend/src/scenes/pipeline/Pipeline.stories.tsx @@ -1,11 +1,13 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' -import { App } from 'scenes/App' import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' -import { PipelineTabs } from '~/types' -import { pipelineLogic } from './pipelineLogic' + import { mswDecorator, useStorybookMocks } from '~/mocks/browser' +import { PipelineAppTabs, PipelineTabs } from '~/types' + +import { pipelineLogic } from './pipelineLogic' export default { title: 'Scenes-App/Pipeline', @@ -58,3 +60,29 @@ export function PipelineTransformationsPage(): JSX.Element { }, []) return } + +export function PipelineAppConfiguration(): JSX.Element { + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Configuration)) + }, []) + return +} + +export function PipelineAppMetrics(): JSX.Element { + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Metrics)) + }, []) + return +} + +export function PipelineAppLogs(): JSX.Element { + useStorybookMocks({ + get: { + 'api/projects/:team_id/plugin_configs/1/logs': require('./__mocks__/pluginLogs.json'), + }, + }) + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Logs)) + }, []) + return +} diff --git a/frontend/src/scenes/pipeline/Pipeline.tsx b/frontend/src/scenes/pipeline/Pipeline.tsx index a688d84b81cd6..1e7a73d7d357e 100644 --- a/frontend/src/scenes/pipeline/Pipeline.tsx +++ b/frontend/src/scenes/pipeline/Pipeline.tsx @@ -1,14 +1,16 @@ -import { SceneExport } from 'scenes/sceneTypes' +import { useValues } from 'kea' +import { router } from 'kea-router' import { PageHeader } from 'lib/components/PageHeader' -import { humanFriendlyTabName, pipelineLogic } from './pipelineLogic' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { useValues } from 'kea' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { router } from 'kea-router' + import { PipelineTabs } from '~/types' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { Transformations } from './Transformations' + import { NewButton } from './NewButton' +import { humanFriendlyTabName, pipelineLogic } from './pipelineLogic' +import { Transformations } from './Transformations' export function Pipeline(): JSX.Element { const { currentTab } = useValues(pipelineLogic) diff --git a/frontend/src/scenes/pipeline/PipelineApp.tsx b/frontend/src/scenes/pipeline/PipelineApp.tsx new file mode 100644 index 0000000000000..3ca9430e7d8a2 --- /dev/null +++ b/frontend/src/scenes/pipeline/PipelineApp.tsx @@ -0,0 +1,50 @@ +import { Spinner } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { router } from 'kea-router' +import { PageHeader } from 'lib/components/PageHeader' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs/LemonTabs' +import { capitalizeFirstLetter } from 'lib/utils' +import { PluginLogs } from 'scenes/plugins/plugin/PluginLogs' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { PipelineAppTabs } from '~/types' + +import { pipelineAppLogic } from './pipelineAppLogic' + +export const scene: SceneExport = { + component: PipelineApp, + logic: pipelineAppLogic, + paramsToProps: ({ params: { id } }: { params: { id?: string } }) => ({ id: id ? parseInt(id) : 'new' }), +} + +export function PipelineApp({ id }: { id?: string } = {}): JSX.Element { + const { currentTab } = useValues(pipelineAppLogic) + + const confId = id ? parseInt(id) : undefined + + if (!confId) { + return + } + + const tab_to_content: Record = { + [PipelineAppTabs.Configuration]:
    Configuration editing
    , + [PipelineAppTabs.Metrics]:
    Metrics page
    , + [PipelineAppTabs.Logs]: , + } + + return ( +
    + + router.actions.push(urls.pipelineApp(confId, tab as PipelineAppTabs))} + tabs={Object.values(PipelineAppTabs).map((tab) => ({ + label: capitalizeFirstLetter(tab), + key: tab, + content: tab_to_content[tab], + }))} + /> +
    + ) +} diff --git a/frontend/src/scenes/pipeline/Transformations.tsx b/frontend/src/scenes/pipeline/Transformations.tsx index 39768644c6462..93a47862e0fde 100644 --- a/frontend/src/scenes/pipeline/Transformations.tsx +++ b/frontend/src/scenes/pipeline/Transformations.tsx @@ -1,3 +1,7 @@ +import { DndContext, DragEndEvent } from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { LemonBadge, LemonButton, @@ -10,21 +14,20 @@ import { Tooltip, } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { pipelineTransformationsLogic } from './transformationsLogic' -import { PluginImage } from 'scenes/plugins/plugin/PluginImage' -import { PipelineTabs, PluginConfigTypeNew, PluginType, ProductKey } from '~/types' -import { urls } from 'scenes/urls' -import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' -import { DndContext, DragEndEvent } from '@dnd-kit/core' -import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' -import { CSS } from '@dnd-kit/utilities' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { dayjs } from 'lib/dayjs' import { More } from 'lib/lemon-ui/LemonButton/More' -import { updatedAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { deleteWithUndo, humanFriendlyDetailedTime } from 'lib/utils' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown/LemonMarkdown' -import { dayjs } from 'lib/dayjs' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { updatedAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { humanFriendlyDetailedTime } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { PluginImage } from 'scenes/plugins/plugin/PluginImage' +import { urls } from 'scenes/urls' + +import { PipelineAppTabs, PipelineTabs, PluginConfigTypeNew, PluginType, ProductKey } from '~/types' + import { NewButton } from './NewButton' +import { pipelineTransformationsLogic } from './transformationsLogic' export function Transformations(): JSX.Element { const { @@ -100,7 +103,12 @@ export function Transformations(): JSX.Element { return ( <> - + {pluginConfig.name} @@ -153,7 +161,9 @@ export function Transformations(): JSX.Element { } > - + Error @@ -207,7 +217,10 @@ export function Transformations(): JSX.Element { )} @@ -215,7 +228,7 @@ export function Transformations(): JSX.Element { @@ -223,7 +236,7 @@ export function Transformations(): JSX.Element { @@ -244,7 +257,7 @@ export function Transformations(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `plugin_config`, object: { id: pluginConfig.id, diff --git a/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json b/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json new file mode 100644 index 0000000000000..09fa96824bf74 --- /dev/null +++ b/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json @@ -0,0 +1,29 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": "018bb51f-0f9f-0000-34ae-d3aa1d9a5770", + "team_id": 1, + "plugin_id": 1, + "plugin_config_id": 11, + "timestamp": "2023-11-09T17:26:33.626000Z", + "source": "PLUGIN", + "type": "ERROR", + "message": "Error: Received an unexpected error from the endpoint API. Response 400: {\"meta\":{\"errors\":[\"value for attribute '$current_url' cannot be longer than 1000 bytes\"]}}\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)", + "instance_id": "12345678-1234-1234-1234-123456789012" + }, + { + "id": "018bb51e-262a-0000-eb34-39afd4691d56", + "team_id": 1, + "plugin_id": 1, + "plugin_config_id": 11, + "timestamp": "2023-11-09T17:25:33.790000Z", + "source": "PLUGIN", + "type": "INFO", + "message": "Successfully sent event to endpoint", + "instance_id": "12345678-1234-1234-1234-123456789012" + } + ] +} diff --git a/frontend/src/scenes/pipeline/pipelineAppLogic.tsx b/frontend/src/scenes/pipeline/pipelineAppLogic.tsx new file mode 100644 index 0000000000000..b7f3b0110ae58 --- /dev/null +++ b/frontend/src/scenes/pipeline/pipelineAppLogic.tsx @@ -0,0 +1,57 @@ +import { actions, kea, key, path, props, reducers, selectors } from 'kea' +import { actionToUrl, urlToAction } from 'kea-router' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb, PipelineAppTabs } from '~/types' + +import type { pipelineAppLogicType } from './pipelineAppLogicType' + +export interface PipelineAppLogicProps { + id: number +} + +export const pipelineAppLogic = kea([ + props({} as PipelineAppLogicProps), + key(({ id }) => id), + path((id) => ['scenes', 'pipeline', 'pipelineAppLogic', id]), + actions({ + setCurrentTab: (tab: PipelineAppTabs = PipelineAppTabs.Configuration) => ({ tab }), + }), + reducers({ + currentTab: [ + PipelineAppTabs.Configuration as PipelineAppTabs, + { + setCurrentTab: (_, { tab }) => tab, + }, + ], + }), + selectors({ + breadcrumbs: [ + () => [], + (): Breadcrumb[] => [ + { + key: Scene.Pipeline, + name: 'Pipeline', + path: urls.pipeline(), + }, + { + key: 'todo', + name: 'App name', + }, + ], + ], + }), + actionToUrl(({ values, props }) => { + return { + setCurrentTab: () => [urls.pipelineApp(props.id, values.currentTab)], + } + }), + urlToAction(({ actions, values }) => ({ + '/pipeline/:id/:tab': ({ tab }) => { + if (tab !== values.currentTab) { + actions.setCurrentTab(tab as PipelineAppTabs) + } + }, + })), +]) diff --git a/frontend/src/scenes/pipeline/pipelineLogic.tsx b/frontend/src/scenes/pipeline/pipelineLogic.tsx index 017e1966745b7..ff96bd6077d86 100644 --- a/frontend/src/scenes/pipeline/pipelineLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineLogic.tsx @@ -1,9 +1,12 @@ import { actions, kea, path, reducers, selectors } from 'kea' -import type { pipelineLogicType } from './pipelineLogicType' import { actionToUrl, urlToAction } from 'kea-router' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + import { Breadcrumb, PipelineTabs } from '~/types' +import type { pipelineLogicType } from './pipelineLogicType' + export const singularName = (tab: PipelineTabs): string => { switch (tab) { case PipelineTabs.Filters: @@ -43,12 +46,13 @@ export const pipelineLogic = kea([ breadcrumbs: [ (s) => [s.currentTab], (tab): Breadcrumb[] => { - const breadcrumbs: Breadcrumb[] = [{ name: 'Pipeline' }] - breadcrumbs.push({ - name: humanFriendlyTabName(tab), - }) - - return breadcrumbs + return [ + { key: Scene.Pipeline, name: 'Data pipeline' }, + { + key: tab, + name: humanFriendlyTabName(tab), + }, + ] }, ], })), diff --git a/frontend/src/scenes/pipeline/transformationsLogic.tsx b/frontend/src/scenes/pipeline/transformationsLogic.tsx index 40c83cb3c6750..5bb16e830253b 100644 --- a/frontend/src/scenes/pipeline/transformationsLogic.tsx +++ b/frontend/src/scenes/pipeline/transformationsLogic.tsx @@ -1,14 +1,15 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' -import { PluginConfigTypeNew, PluginType, ProductKey } from '~/types' import posthog from 'posthog-js' - -import type { pipelineTransformationsLogicType } from './transformationsLogicType' -import { teamLogic } from 'scenes/teamLogic' import { canConfigurePlugins } from 'scenes/plugins/access' +import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' +import { PluginConfigTypeNew, PluginType, ProductKey } from '~/types' + +import type { pipelineTransformationsLogicType } from './transformationsLogicType' + function capturePluginEvent(event: string, plugin: PluginType, pluginConfig: PluginConfigTypeNew): void { posthog.capture(event, { plugin_id: plugin.id, @@ -36,7 +37,7 @@ export const pipelineTransformationsLogic = kea, { loadPlugins: async () => { - const results: PluginType[] = await loadPaginatedResults( + const results: PluginType[] = await api.loadPaginatedResults( `api/organizations/@current/pipeline_transformations` ) const plugins: Record = {} @@ -60,7 +61,7 @@ export const pipelineTransformationsLogic = kea { const pluginConfigs: Record = {} - const results = await loadPaginatedResults( + const results = await api.loadPaginatedResults( `api/projects/${values.currentTeamId}/pipeline_transformations_configs` ) @@ -185,21 +186,3 @@ export const pipelineTransformationsLogic = kea { - let results: any[] = [] - for (let i = 0; i <= maxIterations; ++i) { - if (!url) { - break - } - - const { results: partialResults, next } = await api.get(url) - results = results.concat(partialResults) - url = next - } - return results -} diff --git a/frontend/src/scenes/plugins/AppsScene.tsx b/frontend/src/scenes/plugins/AppsScene.tsx index 374681460c088..54d476409bb0d 100644 --- a/frontend/src/scenes/plugins/AppsScene.tsx +++ b/frontend/src/scenes/plugins/AppsScene.tsx @@ -1,21 +1,22 @@ -import { useEffect } from 'react' +import './Plugins.scss' + +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { pluginsLogic } from './pluginsLogic' -import { PageHeader } from 'lib/components/PageHeader' -import { canGloballyManagePlugins, canViewPlugins } from './access' -import { userLogic } from 'scenes/userLogic' -import { SceneExport } from 'scenes/sceneTypes' import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { PageHeader } from 'lib/components/PageHeader' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { BatchExportsTab } from './tabs/batch-exports/BatchExportsTab' -import { AppsTab } from './tabs/apps/AppsTab' -import { PluginTab } from './types' -import { LemonButton } from '@posthog/lemon-ui' +import { useEffect } from 'react' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' -import './Plugins.scss' +import { canGloballyManagePlugins, canViewPlugins } from './access' +import { pluginsLogic } from './pluginsLogic' import { AppsManagementTab } from './tabs/apps/AppsManagementTab' +import { AppsTab } from './tabs/apps/AppsTab' +import { BatchExportsTab } from './tabs/batch-exports/BatchExportsTab' +import { PluginTab } from './types' export const scene: SceneExport = { component: AppsScene, diff --git a/frontend/src/scenes/plugins/Plugins.stories.tsx b/frontend/src/scenes/plugins/Plugins.stories.tsx index 2344623bb0fcc..714eff490674e 100644 --- a/frontend/src/scenes/plugins/Plugins.stories.tsx +++ b/frontend/src/scenes/plugins/Plugins.stories.tsx @@ -1,8 +1,9 @@ import { Meta, Story } from '@storybook/react' -import { App } from 'scenes/App' -import { useEffect } from 'react' import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' + import { useAvailableFeatures } from '~/mocks/features' import { AvailableFeature } from '~/types' diff --git a/frontend/src/scenes/plugins/PluginsSearch.tsx b/frontend/src/scenes/plugins/PluginsSearch.tsx index 0cb80c2bf9679..66559480381d5 100644 --- a/frontend/src/scenes/plugins/PluginsSearch.tsx +++ b/frontend/src/scenes/plugins/PluginsSearch.tsx @@ -1,6 +1,6 @@ +import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { LemonInput } from '@posthog/lemon-ui' export function PluginsSearch(): JSX.Element { const { searchTerm } = useValues(pluginsLogic) diff --git a/frontend/src/scenes/plugins/access.ts b/frontend/src/scenes/plugins/access.ts index 3d0f80fdb9c5d..a66dae1d5a17f 100644 --- a/frontend/src/scenes/plugins/access.ts +++ b/frontend/src/scenes/plugins/access.ts @@ -1,4 +1,5 @@ import { PluginsAccessLevel } from 'lib/constants' + import { OrganizationType } from '../../types' export function canGloballyManagePlugins(organization: OrganizationType | null | undefined): boolean { diff --git a/frontend/src/scenes/plugins/edit/PluginDrawer.tsx b/frontend/src/scenes/plugins/edit/PluginDrawer.tsx index b5a5b7f31f5a0..0fbf516201734 100644 --- a/frontend/src/scenes/plugins/edit/PluginDrawer.tsx +++ b/frontend/src/scenes/plugins/edit/PluginDrawer.tsx @@ -1,25 +1,26 @@ -import React, { useEffect, useState } from 'react' +import { IconCode } from '@posthog/icons' +import { LemonButton, LemonSwitch, LemonTag, Link } from '@posthog/lemon-ui' +import { PluginConfigChoice, PluginConfigSchema } from '@posthog/plugin-scaffold' +import { Form } from 'antd' import { useActions, useValues } from 'kea' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { Form, Switch } from 'antd' -import { userLogic } from 'scenes/userLogic' -import { PluginImage } from 'scenes/plugins/plugin/PluginImage' import { Drawer } from 'lib/components/Drawer' -import { defaultConfigForPlugin, doFieldRequirementsMatch, getConfigSchemaArray } from 'scenes/plugins/utils' -import { PluginSource } from '../source/PluginSource' -import { PluginConfigChoice, PluginConfigSchema } from '@posthog/plugin-scaffold' -import { PluginField } from 'scenes/plugins/edit/PluginField' +import { MOCK_NODE_PROCESS } from 'lib/constants' +import { IconLock } from 'lib/lemon-ui/icons' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { endWithPunctation } from 'lib/utils' +import React, { useEffect, useState } from 'react' +import { PluginField } from 'scenes/plugins/edit/PluginField' +import { PluginImage } from 'scenes/plugins/plugin/PluginImage' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { defaultConfigForPlugin, doFieldRequirementsMatch, getConfigSchemaArray } from 'scenes/plugins/utils' +import { userLogic } from 'scenes/userLogic' + import { canGloballyManagePlugins } from '../access' +import { PluginSource } from '../source/PluginSource' +import { PluginTags } from '../tabs/apps/components' import { capabilitiesInfo } from './CapabilitiesInfo' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { PluginJobOptions } from './interface-jobs/PluginJobOptions' -import { MOCK_NODE_PROCESS } from 'lib/constants' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' -import { PluginTags } from '../tabs/apps/components' -import { IconLock } from 'lib/lemon-ui/icons' -import { LemonButton, LemonTag, Link } from '@posthog/lemon-ui' -import { IconCode } from '@posthog/icons' window.process = MOCK_NODE_PROCESS @@ -31,10 +32,10 @@ function EnabledDisabledSwitch({ onChange?: (value: boolean) => void }): JSX.Element { return ( - <> - - {value ? 'Enabled' : 'Disabled'} - +
    + + {value ? 'Enabled' : 'Disabled'} +
    ) } diff --git a/frontend/src/scenes/plugins/edit/PluginField.tsx b/frontend/src/scenes/plugins/edit/PluginField.tsx index f4ccae78890b0..48773fc765898 100644 --- a/frontend/src/scenes/plugins/edit/PluginField.tsx +++ b/frontend/src/scenes/plugins/edit/PluginField.tsx @@ -1,11 +1,11 @@ -import { UploadField } from 'scenes/plugins/edit/UploadField' -import { Button, Input, Select } from 'antd' -import { useState } from 'react' import { PluginConfigSchema } from '@posthog/plugin-scaffold/src/types' -import { SECRET_FIELD_VALUE } from 'scenes/plugins/utils' -import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { Button, Input, Select } from 'antd' import { CodeEditor } from 'lib/components/CodeEditors' import { IconEdit } from 'lib/lemon-ui/icons' +import { useState } from 'react' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { UploadField } from 'scenes/plugins/edit/UploadField' +import { SECRET_FIELD_VALUE } from 'scenes/plugins/utils' function JsonConfigField(props: { onChange: (value: any) => void diff --git a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx index 497b9b667eaa9..cb09686bd9b19 100644 --- a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx +++ b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx @@ -1,22 +1,23 @@ -import { useMemo } from 'react' -import { Radio, InputNumber } from 'antd' +import { IconCheck } from '@posthog/icons' +import { LemonSegmentedButton, Tooltip } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { ChildFunctionProps, Form } from 'kea-forms' +import { CodeEditor } from 'lib/components/CodeEditors' +import { DatePicker } from 'lib/components/DatePicker' +import { dayjs } from 'lib/dayjs' import { Field } from 'lib/forms/Field' -import { useValues, useActions } from 'kea' -import { userLogic } from 'scenes/userLogic' -import { JobPayloadFieldOptions } from '~/types' -import { interfaceJobsLogic, InterfaceJobsProps } from './interfaceJobsLogic' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { IconClose, IconPlayCircle, IconSettings } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonCalendarRangeInline } from 'lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline' -import { dayjs } from 'lib/dayjs' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonModal } from 'lib/lemon-ui/LemonModal' import { formatDate, formatDateRange } from 'lib/utils' -import { DatePicker } from 'lib/components/DatePicker' -import { CodeEditor } from 'lib/components/CodeEditors' -import { IconClose, IconPlayCircle, IconSettings } from 'lib/lemon-ui/icons' -import { IconCheck } from '@posthog/icons' -import { Tooltip } from '@posthog/lemon-ui' +import { useMemo } from 'react' +import { userLogic } from 'scenes/userLogic' + +import { JobPayloadFieldOptions } from '~/types' + +import { interfaceJobsLogic, InterfaceJobsProps } from './interfaceJobsLogic' // keep in sync with plugin-server's export-historical-events.ts export const HISTORICAL_EXPORT_JOB_NAME = 'Export historical events' @@ -106,7 +107,7 @@ function FieldInput({ case 'string': return case 'number': - return + return case 'json': return ( onChange(e.target.value)} - > - - True - - - False - - + options={[ + { + value: true, + label: 'True', + icon: , + }, + { + value: false, + label: 'False', + icon: , + }, + ]} + /> ) case 'date': return ( diff --git a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobOptions.tsx b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobOptions.tsx index 1f13476a61e8f..85b9fce6a0b50 100644 --- a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobOptions.tsx +++ b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobOptions.tsx @@ -1,15 +1,17 @@ import { LemonTag } from '@posthog/lemon-ui' -import { Link } from 'lib/lemon-ui/Link' import { useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { Link } from 'lib/lemon-ui/Link' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { useMemo } from 'react' + import { JobSpec } from '~/types' + import { HISTORICAL_EXPORT_JOB_NAME, HISTORICAL_EXPORT_JOB_NAME_V2, PluginJobConfiguration, } from './PluginJobConfiguration' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' interface PluginJobOptionsProps { pluginId: number diff --git a/frontend/src/scenes/plugins/edit/interface-jobs/interfaceJobsLogic.ts b/frontend/src/scenes/plugins/edit/interface-jobs/interfaceJobsLogic.ts index b4ad3e623147a..c18eb2f98e50f 100644 --- a/frontend/src/scenes/plugins/edit/interface-jobs/interfaceJobsLogic.ts +++ b/frontend/src/scenes/plugins/edit/interface-jobs/interfaceJobsLogic.ts @@ -1,12 +1,14 @@ import type { FormInstance } from 'antd/lib/form/hooks/useForm.d' -import { actions, kea, key, events, listeners, path, props, reducers } from 'kea' +import { actions, events, kea, key, listeners, path, props, reducers } from 'kea' import { forms } from 'kea-forms' import api from 'lib/api' -import type { interfaceJobsLogicType } from './interfaceJobsLogicType' -import { JobSpec } from '~/types' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { validateJson } from 'lib/utils' +import { JobSpec } from '~/types' + +import type { interfaceJobsLogicType } from './interfaceJobsLogicType' + export interface InterfaceJobsProps { jobName: string jobSpec: JobSpec diff --git a/frontend/src/scenes/plugins/plugin/LogsDrawer.tsx b/frontend/src/scenes/plugins/plugin/LogsDrawer.tsx index 30ccebb2e0827..432294928549a 100644 --- a/frontend/src/scenes/plugins/plugin/LogsDrawer.tsx +++ b/frontend/src/scenes/plugins/plugin/LogsDrawer.tsx @@ -1,7 +1,8 @@ +import { Drawer } from 'antd' import { useActions, useValues } from 'kea' + import { pluginsLogic } from '../pluginsLogic' import { PluginLogs } from './PluginLogs' -import { Drawer } from 'antd' export function LogsDrawer(): JSX.Element { const { showingLogsPlugin, lastShownLogsPlugin } = useValues(pluginsLogic) diff --git a/frontend/src/scenes/plugins/plugin/PluginImage.tsx b/frontend/src/scenes/plugins/plugin/PluginImage.tsx index 9fec8b6275e9e..b7401e2e6561d 100644 --- a/frontend/src/scenes/plugins/plugin/PluginImage.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginImage.tsx @@ -1,8 +1,9 @@ +import { IconTerminal } from 'lib/lemon-ui/icons' import { parseGithubRepoURL } from 'lib/utils' -import { useEffect, useState } from 'react' import imgPluginDefault from 'public/plugin-default.svg' +import { useEffect, useState } from 'react' + import { PluginType } from '~/types' -import { IconTerminal } from 'lib/lemon-ui/icons' export function PluginImage({ plugin, diff --git a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx index 11275bcab2da5..12a4018d81fce 100644 --- a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx @@ -1,9 +1,11 @@ +import { LemonButton, LemonCheckbox, LemonInput, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { LOGS_PORTION_LIMIT } from 'lib/constants' +import { dayjs } from 'lib/dayjs' import { pluralize } from 'lib/utils' + import { PluginLogEntryType } from '../../../types' -import { LOGS_PORTION_LIMIT, pluginLogsLogic, PluginLogsProps } from './pluginLogsLogic' -import { dayjs } from 'lib/dayjs' -import { LemonButton, LemonCheckbox, LemonInput, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' +import { pluginLogsLogic, PluginLogsProps } from './pluginLogsLogic' function PluginLogEntryTypeDisplay(type: PluginLogEntryType): JSX.Element { let color: string | undefined @@ -51,6 +53,7 @@ const columns: LemonTableColumns> = [ title: 'Message', key: 'message', dataIndex: 'message', + render: (message: string) => {message}, }, ] diff --git a/frontend/src/scenes/plugins/plugin/SuccessRateBadge.tsx b/frontend/src/scenes/plugins/plugin/SuccessRateBadge.tsx index 50f0b06df583f..18a2877003358 100644 --- a/frontend/src/scenes/plugins/plugin/SuccessRateBadge.tsx +++ b/frontend/src/scenes/plugins/plugin/SuccessRateBadge.tsx @@ -1,7 +1,7 @@ +import { LemonBadge, LemonBadgeProps } from '@posthog/lemon-ui' +import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { urls } from 'scenes/urls' -import { Link } from 'lib/lemon-ui/Link' -import { LemonBadge, LemonBadgeProps } from '@posthog/lemon-ui' export function SuccessRateBadge({ deliveryRate, diff --git a/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts b/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts index 437393efcb4cf..c8db5be4b0f0a 100644 --- a/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts +++ b/frontend/src/scenes/plugins/plugin/pluginLogsLogic.ts @@ -1,17 +1,18 @@ +import { CheckboxValueType } from 'antd/lib/checkbox/Group' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' +import { LOGS_PORTION_LIMIT } from 'lib/constants' + import api from '~/lib/api' import { PluginLogEntry, PluginLogEntryType } from '~/types' + import { teamLogic } from '../../teamLogic' import type { pluginLogsLogicType } from './pluginLogsLogicType' -import { CheckboxValueType } from 'antd/lib/checkbox/Group' export interface PluginLogsProps { pluginConfigId: number } -export const LOGS_PORTION_LIMIT = 50 - export const pluginLogsLogic = kea([ props({} as PluginLogsProps), key(({ pluginConfigId }: PluginLogsProps) => pluginConfigId), diff --git a/frontend/src/scenes/plugins/plugin/styles/metrics-drawer.scss b/frontend/src/scenes/plugins/plugin/styles/metrics-drawer.scss index 8ae94920b061e..b158af5a74892 100644 --- a/frontend/src/scenes/plugins/plugin/styles/metrics-drawer.scss +++ b/frontend/src/scenes/plugins/plugin/styles/metrics-drawer.scss @@ -1,10 +1,12 @@ .metrics-drawer { z-index: var(--z-drawer); + .metrics-chart-wrapper { canvas { max-width: 90%; max-height: 80%; } + canvas:hover { cursor: default !important; } diff --git a/frontend/src/scenes/plugins/pluginActivityDescriptions.tsx b/frontend/src/scenes/plugins/pluginActivityDescriptions.tsx index 839f1f7861399..d80f656067f67 100644 --- a/frontend/src/scenes/plugins/pluginActivityDescriptions.tsx +++ b/frontend/src/scenes/plugins/pluginActivityDescriptions.tsx @@ -1,6 +1,7 @@ -import { dayjs } from 'lib/dayjs' import { ActivityLogItem, ActivityScope, HumanizedChange } from 'lib/components/ActivityLog/humanizeActivity' import { SentenceList } from 'lib/components/ActivityLog/SentenceList' +import { dayjs } from 'lib/dayjs' + import { SECRET_FIELD_VALUE } from './utils' export function pluginActivityDescriber(logItem: ActivityLogItem): HumanizedChange { diff --git a/frontend/src/scenes/plugins/pluginsLogic.ts b/frontend/src/scenes/plugins/pluginsLogic.ts index dd03bed0a3f6b..b50d7ce36fd93 100644 --- a/frontend/src/scenes/plugins/pluginsLogic.ts +++ b/frontend/src/scenes/plugins/pluginsLogic.ts @@ -1,9 +1,21 @@ +import type { FormInstance } from 'antd/lib/form/hooks/useForm.d' import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { actionToUrl, router, urlToAction } from 'kea-router' -import type { pluginsLogicType } from './pluginsLogicType' import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import posthog from 'posthog-js' +import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' +import { createDefaultPluginSource } from 'scenes/plugins/source/createDefaultPluginSource' +import { getConfigSchemaArray, getConfigSchemaObject, getPluginConfigFormData } from 'scenes/plugins/utils' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' + import { PersonalAPIKeyType, PluginConfigType, PluginType } from '~/types' + +import { teamLogic } from '../teamLogic' +import { canGloballyManagePlugins, canInstallPlugins } from './access' +import type { pluginsLogicType } from './pluginsLogicType' import { PluginInstallationType, PluginRepositoryEntry, @@ -11,16 +23,6 @@ import { PluginTypeWithConfig, PluginUpdateStatusType, } from './types' -import { userLogic } from 'scenes/userLogic' -import { getConfigSchemaArray, getConfigSchemaObject, getPluginConfigFormData } from 'scenes/plugins/utils' -import posthog from 'posthog-js' -import type { FormInstance } from 'antd/lib/form/hooks/useForm.d' -import { canGloballyManagePlugins, canInstallPlugins } from './access' -import { teamLogic } from '../teamLogic' -import { createDefaultPluginSource } from 'scenes/plugins/source/createDefaultPluginSource' -import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' -import { urls } from 'scenes/urls' -import { lemonToast } from 'lib/lemon-ui/lemonToast' export type PluginForm = FormInstance @@ -29,8 +31,6 @@ export interface PluginSelectionType { url?: string } -const PAGINATION_DEFAULT_MAX_PAGES = 10 - function capturePluginEvent(event: string, plugin: PluginType, type?: PluginInstallationType): void { posthog.capture(event, { plugin_name: plugin.name, @@ -40,26 +40,9 @@ function capturePluginEvent(event: string, plugin: PluginType, type?: PluginInst }) } -async function loadPaginatedResults( - url: string | null, - maxIterations: number = PAGINATION_DEFAULT_MAX_PAGES -): Promise { - let results: any[] = [] - for (let i = 0; i <= maxIterations; ++i) { - if (!url) { - break - } - - const { results: partialResults, next } = await api.get(url) - results = results.concat(partialResults) - url = next - } - return results -} - export const pluginsLogic = kea([ path(['scenes', 'plugins', 'pluginsLogic']), - connect(frontendAppsLogic), + connect(() => frontendAppsLogic), actions({ editPlugin: (id: number | null, pluginConfigChanges: Record = {}) => ({ id, pluginConfigChanges }), savePluginConfig: (pluginConfigChanges: Record) => ({ pluginConfigChanges }), @@ -102,7 +85,7 @@ export const pluginsLogic = kea([ {} as Record, { loadPlugins: async () => { - const results: PluginType[] = await loadPaginatedResults('api/organizations/@current/plugins') + const results: PluginType[] = await api.loadPaginatedResults('api/organizations/@current/plugins') const plugins: Record = {} for (const plugin of results) { plugins[plugin.id] = plugin @@ -160,7 +143,7 @@ export const pluginsLogic = kea([ { loadPluginConfigs: async () => { const pluginConfigs: Record = {} - const results: PluginConfigType[] = await loadPaginatedResults('api/plugin_config') + const results: PluginConfigType[] = await api.loadPaginatedResults('api/plugin_config') for (const pluginConfig of results) { pluginConfigs[pluginConfig.plugin] = { ...pluginConfig } @@ -623,13 +606,13 @@ export const pluginsLogic = kea([ (s) => [s.repository, s.plugins], (repository, plugins) => { const allPossiblePlugins: PluginSelectionType[] = [] - for (const plugin of Object.values(plugins) as PluginType[]) { + for (const plugin of Object.values(plugins)) { allPossiblePlugins.push({ name: plugin.name, url: plugin.url }) } const installedUrls = new Set(Object.values(plugins).map((plugin) => plugin.url)) - for (const plugin of Object.values(repository) as PluginRepositoryEntry[]) { + for (const plugin of Object.values(repository)) { if (!installedUrls.has(plugin.url)) { allPossiblePlugins.push({ name: plugin.name, url: plugin.url }) } diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index 0d016a06a7ba1..ad47b039462bf 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -1,20 +1,21 @@ import './PluginSource.scss' -import { useEffect } from 'react' -import { useActions, useValues } from 'kea' -import { Button, Skeleton } from 'antd' + import { useMonaco } from '@monaco-editor/react' +import { Link } from '@posthog/lemon-ui' +import { Button, Skeleton } from 'antd' +import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' +import { CodeEditor } from 'lib/components/CodeEditors' import { Drawer } from 'lib/components/Drawer' - -import { userLogic } from 'scenes/userLogic' -import { canGloballyManagePlugins } from '../access' -import { pluginSourceLogic } from 'scenes/plugins/source/pluginSourceLogic' import { Field } from 'lib/forms/Field' -import { PluginSourceTabs } from 'scenes/plugins/source/PluginSourceTabs' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { useEffect } from 'react' import { createDefaultPluginSource } from 'scenes/plugins/source/createDefaultPluginSource' -import { Form } from 'kea-forms' -import { CodeEditor } from 'lib/components/CodeEditors' -import { Link } from '@posthog/lemon-ui' +import { pluginSourceLogic } from 'scenes/plugins/source/pluginSourceLogic' +import { PluginSourceTabs } from 'scenes/plugins/source/PluginSourceTabs' +import { userLogic } from 'scenes/userLogic' + +import { canGloballyManagePlugins } from '../access' interface PluginSourceProps { pluginId: number @@ -57,7 +58,7 @@ export function PluginSource({ if (!monaco) { return } - import('./types/packages.json').then((files) => { + void import('./types/packages.json').then((files) => { for (const [fileName, fileContents] of Object.entries(files).filter( ([fileName]) => fileName !== 'default' )) { diff --git a/frontend/src/scenes/plugins/source/PluginSourceTabs.tsx b/frontend/src/scenes/plugins/source/PluginSourceTabs.tsx index a548f242e1a74..e254b27958d7a 100644 --- a/frontend/src/scenes/plugins/source/PluginSourceTabs.tsx +++ b/frontend/src/scenes/plugins/source/PluginSourceTabs.tsx @@ -1,6 +1,7 @@ import { BuiltLogic, useActions, useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import React from 'react' + import type { pluginSourceLogicType } from './pluginSourceLogicType' export function PluginSourceTabs({ logic }: { logic: BuiltLogic }): JSX.Element { diff --git a/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx b/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx index 27cf774627b4f..38f36fc30b169 100644 --- a/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx +++ b/frontend/src/scenes/plugins/source/pluginSourceLogic.tsx @@ -1,16 +1,16 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' - -import type { pluginSourceLogicType } from './pluginSourceLogicType' import { forms } from 'kea-forms' -import api from 'lib/api' 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' import { validateJson } from 'lib/utils' -import { FormErrors } from 'lib/forms/Errors' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { frontendAppsLogic } from 'scenes/apps/frontendAppsLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { formatSource } from 'scenes/plugins/source/formatSource' -import { beforeUnload } from 'kea-router' + +import type { pluginSourceLogicType } from './pluginSourceLogicType' export interface PluginSourceProps { pluginId: number diff --git a/frontend/src/scenes/plugins/tabs/apps/AdvancedInstallModal.tsx b/frontend/src/scenes/plugins/tabs/apps/AdvancedInstallModal.tsx index 59f18196126e6..9649f799d7a74 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AdvancedInstallModal.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AdvancedInstallModal.tsx @@ -1,10 +1,10 @@ +import { LemonButton, LemonInput, LemonLabel, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { useValues, useActions } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonButton, LemonInput, LemonLabel, Link } from '@posthog/lemon-ui' import { PluginInstallationType } from 'scenes/plugins/types' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' export function AdvancedInstallModal(): JSX.Element { const { preflight } = useValues(preflightLogic) diff --git a/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx index 4d0bc00644da6..f552339777f7c 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx @@ -1,15 +1,17 @@ import { LemonButton, Link } from '@posthog/lemon-ui' +import { Popconfirm } from 'antd' import { useActions, useValues } from 'kea' import { IconCheckmark, IconCloudDownload, IconDelete, IconReplay, IconWeb } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { canGloballyManagePlugins } from 'scenes/plugins/access' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { PluginTypeWithConfig, PluginRepositoryEntry, PluginInstallationType } from 'scenes/plugins/types' +import { PluginInstallationType, PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' +import { userLogic } from 'scenes/userLogic' + import { PluginType } from '~/types' + import { PluginTags } from './components' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Popconfirm } from 'antd' -import { canGloballyManagePlugins } from 'scenes/plugins/access' -import { userLogic } from 'scenes/userLogic' export function AppManagementView({ plugin, diff --git a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx index f4de0dfacaa8c..8ff62f6727844 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx @@ -1,15 +1,17 @@ -import { Link, LemonButton, LemonBadge } from '@posthog/lemon-ui' +import { LemonBadge, LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonMenuItem, LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { IconLink, IconSettings, IconEllipsis, IconLegend, IconErrorOutline } from 'lib/lemon-ui/icons' +import { IconEllipsis, IconErrorOutline, IconLegend, IconLink, IconSettings } from 'lib/lemon-ui/icons' +import { LemonMenu, LemonMenuItem } from 'lib/lemon-ui/LemonMenu' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' import { SuccessRateBadge } from 'scenes/plugins/plugin/SuccessRateBadge' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { PluginTypeWithConfig, PluginRepositoryEntry } from 'scenes/plugins/types' +import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' import { urls } from 'scenes/urls' + import { PluginType } from '~/types' + import { PluginTags } from './components' -import { Tooltip } from 'lib/lemon-ui/Tooltip' export function AppView({ plugin, diff --git a/frontend/src/scenes/plugins/tabs/apps/AppsManagementTab.tsx b/frontend/src/scenes/plugins/tabs/apps/AppsManagementTab.tsx index ea6dc22ccae73..06125e06f06fa 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppsManagementTab.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppsManagementTab.tsx @@ -2,15 +2,17 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { IconCloudDownload, IconRefresh } from 'lib/lemon-ui/icons' import { useMemo } from 'react' -import { PluginsSearch } from 'scenes/plugins/PluginsSearch' import { canGloballyManagePlugins } from 'scenes/plugins/access' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginsSearch } from 'scenes/plugins/PluginsSearch' +import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' import { userLogic } from 'scenes/userLogic' + +import { PluginType } from '~/types' + import { AdvancedInstallModal } from './AdvancedInstallModal' -import { AppsTable } from './AppsTable' import { AppManagementView } from './AppManagementView' -import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' -import { PluginType } from '~/types' +import { AppsTable } from './AppsTable' export function AppsManagementTab(): JSX.Element { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx b/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx index a02ad02f27fef..384d8d697d63b 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx @@ -1,14 +1,16 @@ import { useValues } from 'kea' -import { PluginsSearch } from 'scenes/plugins/PluginsSearch' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { PluginDrawer } from 'scenes/plugins/edit/PluginDrawer' -import { BatchExportsAlternativeWarning } from './components' -import { InstalledAppsReorderModal } from './InstalledAppsReorderModal' -import { AppsTable } from './AppsTable' -import { AppView } from './AppView' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginsSearch } from 'scenes/plugins/PluginsSearch' import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' + import { PluginType } from '~/types' +import { AppsTable } from './AppsTable' +import { AppView } from './AppView' +import { BatchExportsAlternativeWarning } from './components' +import { InstalledAppsReorderModal } from './InstalledAppsReorderModal' + export function AppsTab(): JSX.Element { const { sortableEnabledPlugins, unsortableEnabledPlugins, filteredDisabledPlugins, loading } = useValues(pluginsLogic) diff --git a/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx b/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx index 5fb4af0d52ac7..477ed810eab59 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx @@ -1,9 +1,10 @@ -import { LemonTable, LemonButton } from '@posthog/lemon-ui' +import { LemonButton, LemonTable } from '@posthog/lemon-ui' import { useValues } from 'kea' import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { useState } from 'react' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' + import { PluginType } from '~/types' export function AppsTable({ diff --git a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx index 382a157bbbf6f..612aaa0071e8a 100644 --- a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx @@ -1,13 +1,13 @@ -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { useValues, useActions } from 'kea' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { LemonBadge, LemonButton } from '@posthog/lemon-ui' -import { PluginTypeWithConfig } from 'scenes/plugins/types' -import { PluginImage } from 'scenes/plugins/plugin/PluginImage' -import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' import { DndContext, DragEndEvent } from '@dnd-kit/core' import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' +import { LemonBadge, LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { PluginImage } from 'scenes/plugins/plugin/PluginImage' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginTypeWithConfig } from 'scenes/plugins/types' const MinimalAppView = ({ plugin, order }: { plugin: PluginTypeWithConfig; order: number }): JSX.Element => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: plugin.id }) diff --git a/frontend/src/scenes/plugins/tabs/apps/components.tsx b/frontend/src/scenes/plugins/tabs/apps/components.tsx index 4a0ecb9426d35..621b3d2bfaa95 100644 --- a/frontend/src/scenes/plugins/tabs/apps/components.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/components.tsx @@ -1,15 +1,16 @@ -import { PluginType } from '~/types' import { LemonTag, Link } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { PluginRepositoryEntry, PluginTab } from 'scenes/plugins/types' import { useValues } from 'kea' -import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { organizationLogic } from 'scenes/organizationLogic' import { PluginsAccessLevel } from 'lib/constants' -import { copyToClipboard } from 'lib/utils' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { organizationLogic } from 'scenes/organizationLogic' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginRepositoryEntry, PluginTab } from 'scenes/plugins/types' import { urls } from 'scenes/urls' +import { PluginType } from '~/types' + export function RepositoryTag({ plugin }: { plugin: PluginType | PluginRepositoryEntry }): JSX.Element | null { const { pluginUrlToMaintainer } = useValues(pluginsLogic) @@ -23,7 +24,7 @@ export function RepositoryTag({ plugin }: { plugin: PluginType | PluginRepositor if (plugin.plugin_type === 'local' && plugin.url) { return ( - await copyToClipboard(plugin.url?.substring(5) || '')}> + void copyToClipboard(plugin.url?.substring(5) || '')}> Installed Locally ) diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 11f9d4c0c1132..eed399afba9a7 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -1,19 +1,19 @@ +import * as Icons from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' -import { SceneExport } from 'scenes/sceneTypes' -import { BillingProductV2Type, ProductKey } from '~/types' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { useEffect } from 'react' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { urls } from 'scenes/urls' -import { billingLogic } from 'scenes/billing/billingLogic' -import { Spinner } from 'lib/lemon-ui/Spinner' -import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard' import { router } from 'kea-router' +import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { billingLogic } from 'scenes/billing/billingLogic' import { getProductUri } from 'scenes/onboarding/onboardingLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + +import { BillingProductV2Type, ProductKey } from '~/types' + import { productsLogic } from './productsLogic' -import * as Icons from '@posthog/icons' export const scene: SceneExport = { component: Products, @@ -30,6 +30,7 @@ function OnboardingCompletedButton({ productKey: ProductKey }): JSX.Element { const { onSelectProduct } = useActions(productsLogic) + return ( <> @@ -49,14 +50,26 @@ function OnboardingCompletedButton({ ) } -function OnboardingNotCompletedButton({ url, productKey }: { url: string; productKey: ProductKey }): JSX.Element { +function OnboardingNotCompletedButton({ + url, + productKey, + getStartedActionOverride, +}: { + url: string + productKey: ProductKey + getStartedActionOverride?: () => void +}): JSX.Element { const { onSelectProduct } = useActions(productsLogic) return ( { - onSelectProduct(productKey) - router.actions.push(url) + if (getStartedActionOverride) { + getStartedActionOverride() + } else { + onSelectProduct(productKey) + router.actions.push(url) + } }} > Get started @@ -68,19 +81,36 @@ export function getProductIcon(iconKey?: string | null, className?: string): JSX return Icons[iconKey || 'IconLogomark']({ className }) } -function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Element { +export function ProductCard({ + product, + getStartedActionOverride, + orientation = 'vertical', + className, +}: { + product: BillingProductV2Type + getStartedActionOverride?: () => void + orientation?: 'horizontal' | 'vertical' + className?: string +}): JSX.Element { const { currentTeam } = useValues(teamLogic) const onboardingCompleted = currentTeam?.has_completed_onboarding_for?.[product.type] + const vertical = orientation === 'vertical' + return ( - -
    -
    {getProductIcon(product.icon_key, 'text-2xl')}
    + +
    +
    +
    {getProductIcon(product.icon_key, 'text-2xl')}
    +
    -
    -

    {product.name}

    +
    +

    {product.name}

    +

    {product.description}

    -

    {product.description}

    -
    +
    {onboardingCompleted ? ( ) : ( - +
    + +
    )}
    @@ -99,20 +132,13 @@ function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Elemen } export function Products(): JSX.Element { - const { featureFlags } = useValues(featureFlagLogic) const { billing } = useValues(billingLogic) const { currentTeam } = useValues(teamLogic) const isFirstProduct = Object.keys(currentTeam?.has_completed_onboarding_for || {}).length === 0 const products = billing?.products || [] - useEffect(() => { - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== 'test') { - location.href = urls.ingestion() - } - }, []) - return ( -
    +

    Pick your {isFirstProduct ? 'first' : 'next'} product.

    diff --git a/frontend/src/scenes/products/productsLogic.tsx b/frontend/src/scenes/products/productsLogic.tsx index 48a17171bdc8a..313e26ea70a1c 100644 --- a/frontend/src/scenes/products/productsLogic.tsx +++ b/frontend/src/scenes/products/productsLogic.tsx @@ -1,16 +1,21 @@ -import { kea, path, actions, listeners } from 'kea' +import { actions, connect, kea, listeners, path } from 'kea' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { onboardingLogic } from 'scenes/onboarding/onboardingLogic' import { teamLogic } from 'scenes/teamLogic' + import { ProductKey } from '~/types' import type { productsLogicType } from './productsLogicType' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export const productsLogic = kea([ path(() => ['scenes', 'products', 'productsLogic']), + connect({ + actions: [teamLogic, ['updateCurrentTeam'], onboardingLogic, ['setProduct']], + }), actions(() => ({ onSelectProduct: (product: ProductKey) => ({ product }), })), - listeners(() => ({ + listeners(({ actions }) => ({ onSelectProduct: ({ product }) => { eventUsageLogic.actions.reportOnboardingProductSelected(product) @@ -18,7 +23,7 @@ export const productsLogic = kea([ case ProductKey.PRODUCT_ANALYTICS: return case ProductKey.SESSION_REPLAY: - teamLogic.actions.updateCurrentTeam({ + actions.updateCurrentTeam({ session_recording_opt_in: true, capture_console_log_opt_in: true, capture_performance_opt_in: true, diff --git a/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx b/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx index 0aea88331c5e3..8df69f2bc4697 100644 --- a/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx +++ b/frontend/src/scenes/project-homepage/NewlySeenPersons.tsx @@ -1,16 +1,17 @@ import './ProjectHomepage.scss' -import { useActions, useValues } from 'kea' -import { dayjs } from 'lib/dayjs' +import { useActions, useValues } from 'kea' import { CompactList } from 'lib/components/CompactList/CompactList' +import { dayjs } from 'lib/dayjs' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { asDisplay } from 'scenes/persons/person-utils' import { urls } from 'scenes/urls' + import { PersonType } from '~/types' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { projectHomepageLogic } from './projectHomepageLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { ProjectHomePageCompactListItem } from './ProjectHomePageCompactListItem' -import { asDisplay } from 'scenes/persons/person-utils' +import { projectHomepageLogic } from './projectHomepageLogic' function PersonRow({ person }: { person: PersonType }): JSX.Element { const { reportPersonOpenedFromNewlySeenPersonsList } = useActions(eventUsageLogic) @@ -33,14 +34,14 @@ export function NewlySeenPersons(): JSX.Element { return ( } diff --git a/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx b/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx index c61ac0e2bcc8c..cefd3e82870bc 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx @@ -17,13 +17,13 @@ export function ProjectHomePageCompactListItem({ suffix, }: RecentItemRowProps): JSX.Element { return ( - -

    - {prefix ? {prefix} : null} + +
    + {prefix ? {prefix} : null} -
    -
    {title}
    -
    {subtitle}
    +
    +
    {title}
    +
    {subtitle}
    {suffix ? {suffix} : null} diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.scss b/frontend/src/scenes/project-homepage/ProjectHomepage.scss index b8062e75dda4b..f905c0241e4a4 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.scss +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.scss @@ -1,11 +1,11 @@ .project-homepage { - .homepage-dashboard-header { + .HomepageDashboardHeader { margin-top: 1rem; display: flex; justify-content: space-between; align-items: center; - .dashboard-title-container { + .HomepageDashboardHeader__title { display: flex; flex-direction: row; align-items: center; @@ -17,6 +17,16 @@ margin: 0; } } + + .posthog-3000 & { + a { + color: var(--default); + + &:hover { + color: var(--primary-3000); + } + } + } } } @@ -25,8 +35,9 @@ .top-list { margin-bottom: 1.5rem; + .posthog-3000 & { - margin-bottom: 0px; + margin-bottom: 0; } width: 33%; diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx index fb2a439dbee3b..f62507be91ad2 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx @@ -1,9 +1,10 @@ -import { useEffect } from 'react' import { Meta } from '@storybook/react' -import { mswDecorator } from '~/mocks/browser' -import { App } from 'scenes/App' import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' import { EMPTY_PAGINATED_RESPONSE } from '~/mocks/handlers' const meta: Meta = { diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx index c8fea74e75c9c..ef77e7b65ef86 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx @@ -1,30 +1,33 @@ -import { useRef } from 'react' import './ProjectHomepage.scss' -import { PageHeader } from 'lib/components/PageHeader' -import { Dashboard } from 'scenes/dashboard/Dashboard' + +import { IconHome } from '@posthog/icons' +import { Link } from '@posthog/lemon-ui' +import useSize from '@react-hook/size' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { Scene, SceneExport } from 'scenes/sceneTypes' -import { DashboardPlacement } from '~/types' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { PageHeader } from 'lib/components/PageHeader' +import { SceneDashboardChoiceModal } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceModal' +import { sceneDashboardChoiceModalLogic } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' +import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' +import { FEATURE_FLAGS } from 'lib/constants' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { IconCottage } from 'lib/lemon-ui/icons' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useRef } from 'react' +import { Dashboard } from 'scenes/dashboard/Dashboard' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { projectHomepageLogic } from 'scenes/project-homepage/projectHomepageLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { RecentRecordings } from './RecentRecordings' -import { RecentInsights } from './RecentInsights' -import { NewlySeenPersons } from './NewlySeenPersons' -import useSize from '@react-hook/size' import { NewInsightButton } from 'scenes/saved-insights/SavedInsights' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { Link } from '@posthog/lemon-ui' +import { Scene, SceneExport } from 'scenes/sceneTypes' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { sceneDashboardChoiceModalLogic } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' -import { SceneDashboardChoiceModal } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceModal' -import { SceneDashboardChoiceRequired } from 'lib/components/SceneDashboardChoice/SceneDashboardChoiceRequired' + +import { DashboardPlacement } from '~/types' + +import { NewlySeenPersons } from './NewlySeenPersons' +import { RecentInsights } from './RecentInsights' +import { RecentRecordings } from './RecentRecordings' export function ProjectHomepage(): JSX.Element { const { dashboardLogicProps } = useValues(projectHomepageLogic) @@ -81,12 +84,12 @@ export function ProjectHomepage(): JSX.Element {
    {currentTeam?.primary_dashboard ? ( <> -
    -
    - {!dashboard && } +
    +
    + {!dashboard && } {dashboard?.name && ( <> - + ([ path(['scenes', 'project-homepage', 'projectHomepageLogic']), diff --git a/frontend/src/scenes/project/Create/index.tsx b/frontend/src/scenes/project/Create/index.tsx index 542aa84e527a3..c6d42c631bb30 100644 --- a/frontend/src/scenes/project/Create/index.tsx +++ b/frontend/src/scenes/project/Create/index.tsx @@ -1,9 +1,10 @@ -import { CreateProjectModal } from '../CreateProjectModal' -import { SceneExport } from 'scenes/sceneTypes' -import { teamLogic } from 'scenes/teamLogic' import { useValues } from 'kea' -import { organizationLogic } from 'scenes/organizationLogic' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { organizationLogic } from 'scenes/organizationLogic' +import { SceneExport } from 'scenes/sceneTypes' +import { teamLogic } from 'scenes/teamLogic' + +import { CreateProjectModal } from '../CreateProjectModal' export const scene: SceneExport = { component: ProjectCreate, diff --git a/frontend/src/scenes/project/CreateProjectModal.tsx b/frontend/src/scenes/project/CreateProjectModal.tsx index 878a12f7c0a05..d16abb142f031 100644 --- a/frontend/src/scenes/project/CreateProjectModal.tsx +++ b/frontend/src/scenes/project/CreateProjectModal.tsx @@ -4,6 +4,7 @@ import { PureField } from 'lib/forms/Field' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { useState } from 'react' import { teamLogic } from 'scenes/teamLogic' + import { organizationLogic } from '../organizationLogic' export function CreateProjectModal({ diff --git a/frontend/src/scenes/retention/RetentionContainer.scss b/frontend/src/scenes/retention/RetentionContainer.scss deleted file mode 100644 index 3f450fa32e890..0000000000000 --- a/frontend/src/scenes/retention/RetentionContainer.scss +++ /dev/null @@ -1,20 +0,0 @@ -.RetentionContainer { - width: 100%; -} - -// Here we override based on retention-container the graph-container styling, so -// as to not change the global styling. We need the extra nesting to ensure we -// are more specific than the other insights css -.insights-page { - .insights-graph-container { - .RetentionContainer { - .LineGraph { - position: relative; - margin-top: 0.5rem; - margin-bottom: 1rem; - height: 300px; - width: 100%; - } - } - } -} diff --git a/frontend/src/scenes/retention/RetentionContainer.tsx b/frontend/src/scenes/retention/RetentionContainer.tsx index e12deb74dbf10..15207bdb39a63 100644 --- a/frontend/src/scenes/retention/RetentionContainer.tsx +++ b/frontend/src/scenes/retention/RetentionContainer.tsx @@ -1,25 +1,35 @@ -import { RetentionLineGraph } from './RetentionLineGraph' -import { RetentionTable } from './RetentionTable' -import './RetentionContainer.scss' import { LemonDivider } from '@posthog/lemon-ui' + +import { VizSpecificOptions } from '~/queries/schema' +import { QueryContext } from '~/queries/types' +import { InsightType } from '~/types' + +import { RetentionLineGraph } from './RetentionLineGraph' import { RetentionModal } from './RetentionModal' +import { RetentionTable } from './RetentionTable' export function RetentionContainer({ inCardView, inSharedMode, + vizSpecificOptions, }: { inCardView?: boolean inSharedMode?: boolean + context?: QueryContext + vizSpecificOptions?: VizSpecificOptions[InsightType.RETENTION] }): JSX.Element { + const hideLineGraph = vizSpecificOptions?.hideLineGraph || inCardView return ( -
    - {inCardView ? ( +
    + {hideLineGraph ? ( ) : ( <> - +
    + +
    -
    +
    diff --git a/frontend/src/scenes/retention/RetentionLineGraph.tsx b/frontend/src/scenes/retention/RetentionLineGraph.tsx index c241d7cefc2da..f9ddce22272e6 100644 --- a/frontend/src/scenes/retention/RetentionLineGraph.tsx +++ b/frontend/src/scenes/retention/RetentionLineGraph.tsx @@ -1,14 +1,14 @@ import { useActions, useValues } from 'kea' - +import { roundToDecimal } from 'lib/utils' import { insightLogic } from 'scenes/insights/insightLogic' -import { retentionLineGraphLogic } from './retentionLineGraphLogic' -import { retentionModalLogic } from './retentionModalLogic' -import { GraphType, GraphDataset } from '~/types' -import { roundToDecimal } from 'lib/utils' -import { LineGraph } from '../insights/views/LineGraph/LineGraph' -import { InsightEmptyState } from '../insights/EmptyStates' import { TrendsFilter } from '~/queries/schema' +import { GraphDataset, GraphType } from '~/types' + +import { InsightEmptyState } from '../insights/EmptyStates' +import { LineGraph } from '../insights/views/LineGraph/LineGraph' +import { retentionLineGraphLogic } from './retentionLineGraphLogic' +import { retentionModalLogic } from './retentionModalLogic' interface RetentionLineGraphProps { inSharedMode?: boolean diff --git a/frontend/src/scenes/retention/RetentionModal.tsx b/frontend/src/scenes/retention/RetentionModal.tsx index a47a895d40fff..e3f364621c592 100644 --- a/frontend/src/scenes/retention/RetentionModal.tsx +++ b/frontend/src/scenes/retention/RetentionModal.tsx @@ -1,21 +1,24 @@ -import { capitalizeFirstLetter, isGroupType, percentage } from 'lib/utils' -import { RetentionTableAppearanceType } from 'scenes/retention/types' -import { dayjs } from 'lib/dayjs' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import './RetentionTable.scss' -import { urls } from 'scenes/urls' -import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' + import { LemonButton, LemonModal } from '@posthog/lemon-ui' -import { triggerExport } from 'lib/components/ExportButton/exporter' -import { ExporterFormat } from '~/types' import clsx from 'clsx' -import { MissingPersonsAlert } from 'scenes/trends/persons-modal/PersonsModal' import { useActions, useValues } from 'kea' +import { triggerExport } from 'lib/components/ExportButton/exporter' +import { dayjs } from 'lib/dayjs' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { capitalizeFirstLetter, isGroupType, percentage } from 'lib/utils' import { insightLogic } from 'scenes/insights/insightLogic' +import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { asDisplay } from 'scenes/persons/person-utils' +import { RetentionTableAppearanceType } from 'scenes/retention/types' +import { MissingPersonsAlert } from 'scenes/trends/persons-modal/PersonsModal' +import { urls } from 'scenes/urls' + +import { ExporterFormat } from '~/types' + import { retentionLogic } from './retentionLogic' -import { retentionPeopleLogic } from './retentionPeopleLogic' import { retentionModalLogic } from './retentionModalLogic' -import { asDisplay } from 'scenes/persons/person-utils' +import { retentionPeopleLogic } from './retentionPeopleLogic' export function RetentionModal(): JSX.Element | null { const { insightProps } = useValues(insightLogic) @@ -43,7 +46,7 @@ export function RetentionModal(): JSX.Element | null { - triggerExport({ + void triggerExport({ export_format: ExporterFormat.CSV, export_context: { path: row?.people_url, diff --git a/frontend/src/scenes/retention/RetentionTable.scss b/frontend/src/scenes/retention/RetentionTable.scss index afc444248fb4e..cb796874edeed 100644 --- a/frontend/src/scenes/retention/RetentionTable.scss +++ b/frontend/src/scenes/retention/RetentionTable.scss @@ -1,5 +1,5 @@ .RetentionTable { - --retention-table-color: var(--primary); + --retention-table-color: var(--primary-3000); font-weight: 500; width: 100%; @@ -8,10 +8,11 @@ white-space: nowrap; font-weight: 500; border-left: 1px solid var(--border); - padding: 0rem 0.5rem; + padding: 0 0.5rem; border-top: 10px solid transparent; border-bottom: 10px solid transparent; text-align: left; + &:first-of-type { border-left: none; } @@ -30,7 +31,7 @@ } .RetentionTable__TextTab { - padding: 0rem 1rem 0rem 0.5rem; + padding: 0 1rem 0 0.5rem; white-space: nowrap; } @@ -63,9 +64,30 @@ .RetentionTable__Tab { cursor: initial; + &:hover { transform: none; } } } + + &.RetentionTable--small-layout { + font-size: 0.75rem; + line-height: 1rem; + + th { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + + .RetentionTable__TextTab { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + + .RetentionTable__Tab { + margin: 0; + padding: 0.5rem 0.25rem; + } + } } diff --git a/frontend/src/scenes/retention/RetentionTable.tsx b/frontend/src/scenes/retention/RetentionTable.tsx index 97d124baae554..9aaec1a08c916 100644 --- a/frontend/src/scenes/retention/RetentionTable.tsx +++ b/frontend/src/scenes/retention/RetentionTable.tsx @@ -1,21 +1,26 @@ -import { useValues, useActions } from 'kea' -import clsx from 'clsx' +import './RetentionTable.scss' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { BRAND_BLUE_HSL, gradateColor } from 'lib/colors' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { insightLogic } from 'scenes/insights/insightLogic' -import { retentionTableLogic } from './retentionTableLogic' -import { retentionModalLogic } from './retentionModalLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import './RetentionTable.scss' -import { BRAND_BLUE_HSL, gradateColor } from 'lib/colors' +import { retentionModalLogic } from './retentionModalLogic' +import { retentionTableLogic } from './retentionTableLogic' export function RetentionTable({ inCardView = false }: { inCardView?: boolean }): JSX.Element | null { const { insightProps } = useValues(insightLogic) - const { tableHeaders, tableRows, isLatestPeriod } = useValues(retentionTableLogic(insightProps)) + const { tableHeaders, tableRows, isLatestPeriod, hideSizeColumn, retentionVizOptions } = useValues( + retentionTableLogic(insightProps) + ) const { openModal } = useActions(retentionModalLogic(insightProps)) return ( -
    @@ -231,8 +242,8 @@ export const PlanComparison = ({ > {feature.name}
    {getProductTiers(plan, product.type)} + {getProductTiers(plan, product.type)} +
    +
    {tableHeaders.map((heading) => ( @@ -34,7 +39,7 @@ export function RetentionTable({ inCardView = false }: { inCardView?: boolean }) > {row.map((column, columnIndex) => (
    - {columnIndex <= 1 ? ( + {columnIndex <= (hideSizeColumn ? 0 : 1) ? ( {column} diff --git a/frontend/src/scenes/retention/constants.ts b/frontend/src/scenes/retention/constants.ts index 8137c07d8447b..a33b7fb00d6a8 100644 --- a/frontend/src/scenes/retention/constants.ts +++ b/frontend/src/scenes/retention/constants.ts @@ -1,5 +1,6 @@ -import { OpUnitType } from 'lib/dayjs' import { RETENTION_FIRST_TIME, RETENTION_RECURRING } from 'lib/constants' +import { OpUnitType } from 'lib/dayjs' + import { RetentionPeriod } from '~/types' export const dateOptions: RetentionPeriod[] = [ diff --git a/frontend/src/scenes/retention/retentionLineGraphLogic.test.ts b/frontend/src/scenes/retention/retentionLineGraphLogic.test.ts index c3bfb3a623306..fb565e67a97a0 100644 --- a/frontend/src/scenes/retention/retentionLineGraphLogic.test.ts +++ b/frontend/src/scenes/retention/retentionLineGraphLogic.test.ts @@ -1,9 +1,10 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { retentionLineGraphLogic } from 'scenes/retention/retentionLineGraphLogic' import { insightLogic } from 'scenes/insights/insightLogic' -import { InsightShortId, InsightType, RetentionFilterType } from '~/types' +import { retentionLineGraphLogic } from 'scenes/retention/retentionLineGraphLogic' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { InsightShortId, InsightType, RetentionFilterType } from '~/types' const Insight123 = '123' as InsightShortId const result = [ diff --git a/frontend/src/scenes/retention/retentionLineGraphLogic.ts b/frontend/src/scenes/retention/retentionLineGraphLogic.ts index 127ff04440385..ba460e3a88ea7 100644 --- a/frontend/src/scenes/retention/retentionLineGraphLogic.ts +++ b/frontend/src/scenes/retention/retentionLineGraphLogic.ts @@ -1,14 +1,15 @@ +import { connect, kea, key, path, props, selectors } from 'kea' import { dayjs, QUnitType } from 'lib/dayjs' -import { kea, props, key, path, connect, selectors } from 'kea' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { RetentionTrendPayload } from 'scenes/retention/types' -import { InsightLogicProps, RetentionPeriod } from '~/types' -import { dateOptionToTimeIntervalMap } from './constants' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { retentionLogic } from './retentionLogic' +import { isLifecycleQuery, isStickinessQuery } from '~/queries/utils' +import { InsightLogicProps, RetentionPeriod } from '~/types' +import { dateOptionToTimeIntervalMap } from './constants' import type { retentionLineGraphLogicType } from './retentionLineGraphLogicType' +import { retentionLogic } from './retentionLogic' const DEFAULT_RETENTION_LOGIC_KEY = 'default_retention_key' @@ -117,7 +118,11 @@ export const retentionLineGraphLogic = kea([ aggregationGroupTypeIndex: [ (s) => [s.querySource], (querySource) => { - return querySource?.aggregation_group_type_index ?? 'people' + return ( + (isLifecycleQuery(querySource) || isStickinessQuery(querySource) + ? null + : querySource?.aggregation_group_type_index) ?? 'people' + ) }, ], }), diff --git a/frontend/src/scenes/retention/retentionLogic.ts b/frontend/src/scenes/retention/retentionLogic.ts index bf97e36dfdbb6..006d41843efcb 100644 --- a/frontend/src/scenes/retention/retentionLogic.ts +++ b/frontend/src/scenes/retention/retentionLogic.ts @@ -1,7 +1,8 @@ -import { kea, props, key, path, connect, selectors } from 'kea' +import { connect, kea, key, path, props, selectors } from 'kea' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { RetentionTablePayload } from 'scenes/retention/types' + import { isRetentionQuery } from '~/queries/utils' import { InsightLogicProps } from '~/types' diff --git a/frontend/src/scenes/retention/retentionModalLogic.ts b/frontend/src/scenes/retention/retentionModalLogic.ts index ebd464a94f4b0..47b1eec17c03d 100644 --- a/frontend/src/scenes/retention/retentionModalLogic.ts +++ b/frontend/src/scenes/retention/retentionModalLogic.ts @@ -1,12 +1,13 @@ -import { kea, props, key, path, connect, actions, reducers, selectors, listeners } from 'kea' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { Noun, groupsModel } from '~/models/groupsModel' -import { InsightLogicProps } from '~/types' -import { retentionPeopleLogic } from './retentionPeopleLogic' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { groupsModel, Noun } from '~/models/groupsModel' +import { isLifecycleQuery, isStickinessQuery } from '~/queries/utils' +import { InsightLogicProps } from '~/types' import type { retentionModalLogicType } from './retentionModalLogicType' +import { retentionPeopleLogic } from './retentionPeopleLogic' const DEFAULT_RETENTION_LOGIC_KEY = 'default_retention_key' @@ -35,7 +36,10 @@ export const retentionModalLogic = kea([ aggregationTargetLabel: [ (s) => [s.querySource, s.aggregationLabel], (querySource, aggregationLabel): Noun => { - const { aggregation_group_type_index } = querySource || {} + const aggregation_group_type_index = + isLifecycleQuery(querySource) || isStickinessQuery(querySource) + ? undefined + : querySource?.aggregation_group_type_index return aggregationLabel(aggregation_group_type_index) }, ], diff --git a/frontend/src/scenes/retention/retentionPeopleLogic.ts b/frontend/src/scenes/retention/retentionPeopleLogic.ts index 3268a44c228fd..2cc3b73e9a7f1 100644 --- a/frontend/src/scenes/retention/retentionPeopleLogic.ts +++ b/frontend/src/scenes/retention/retentionPeopleLogic.ts @@ -1,13 +1,13 @@ +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners } from 'kea' import api from 'lib/api' import { toParams } from 'lib/utils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { RetentionTablePeoplePayload } from 'scenes/retention/types' -import { InsightLogicProps } from '~/types' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { InsightLogicProps } from '~/types' import type { retentionPeopleLogicType } from './retentionPeopleLogicType' @@ -31,7 +31,7 @@ export const retentionPeopleLogic = kea([ __default: {} as RetentionTablePeoplePayload, loadPeople: async (rowIndex: number) => { const urlParams = toParams({ ...values.apiFilters, selected_interval: rowIndex }) - return (await api.get(`api/person/retention/?${urlParams}`)) as RetentionTablePeoplePayload + return await api.get(`api/person/retention/?${urlParams}`) }, }, })), diff --git a/frontend/src/scenes/retention/retentionTableLogic.test.ts b/frontend/src/scenes/retention/retentionTableLogic.test.ts index 707c36870fe84..78f4baaa107cc 100644 --- a/frontend/src/scenes/retention/retentionTableLogic.test.ts +++ b/frontend/src/scenes/retention/retentionTableLogic.test.ts @@ -1,9 +1,10 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { retentionTableLogic } from 'scenes/retention/retentionTableLogic' import { insightLogic } from 'scenes/insights/insightLogic' -import { InsightShortId, InsightType, RetentionFilterType } from '~/types' +import { retentionTableLogic } from 'scenes/retention/retentionTableLogic' + import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { InsightShortId, InsightType, RetentionFilterType } from '~/types' const Insight123 = '123' as InsightShortId const result = [ diff --git a/frontend/src/scenes/retention/retentionTableLogic.ts b/frontend/src/scenes/retention/retentionTableLogic.ts index 52e442b0d125e..130145bc4e0e3 100644 --- a/frontend/src/scenes/retention/retentionTableLogic.ts +++ b/frontend/src/scenes/retention/retentionTableLogic.ts @@ -1,12 +1,12 @@ +import { connect, kea, key, path, props, selectors } from 'kea' import { dayjs } from 'lib/dayjs' -import { kea, props, key, path, connect, selectors } from 'kea' import { range } from 'lib/utils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' -import { InsightLogicProps } from '~/types' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { retentionLogic } from './retentionLogic' +import { InsightLogicProps, InsightType } from '~/types' +import { retentionLogic } from './retentionLogic' import type { retentionTableLogicType } from './retentionTableLogicType' const DEFAULT_RETENTION_LOGIC_KEY = 'default_retention_key' @@ -36,7 +36,7 @@ export const retentionTableLogic = kea([ connect((props: InsightLogicProps) => ({ values: [ insightVizDataLogic(props), - ['dateRange', 'retentionFilter', 'breakdown'], + ['dateRange', 'retentionFilter', 'breakdown', 'vizSpecificOptions'], retentionLogic(props), ['results'], ], @@ -47,6 +47,12 @@ export const retentionTableLogic = kea([ (dateRange, retentionFilter) => periodIsLatest(dateRange?.date_to || null, retentionFilter?.period || null), ], + retentionVizOptions: [ + (s) => [s.vizSpecificOptions], + (vizSpecificOptions) => vizSpecificOptions?.[InsightType.RETENTION], + ], + hideSizeColumn: [(s) => [s.retentionVizOptions], (retentionVizOptions) => retentionVizOptions?.hideSizeColumn], + maxIntervalsCount: [ (s) => [s.results], (results) => { @@ -55,15 +61,15 @@ export const retentionTableLogic = kea([ ], tableHeaders: [ - (s) => [s.results], - (results) => { - return ['Cohort', 'Size', ...results.map((x) => x.label)] + (s) => [s.results, s.hideSizeColumn], + (results, hideSizeColumn) => { + return ['Cohort', ...(hideSizeColumn ? [] : ['Size']), ...results.map((x) => x.label)] }, ], tableRows: [ - (s) => [s.results, s.maxIntervalsCount, s.retentionFilter, s.breakdown], - (results, maxIntervalsCount, retentionFilter, breakdown) => { + (s) => [s.results, s.maxIntervalsCount, s.retentionFilter, s.breakdown, s.hideSizeColumn], + (results, maxIntervalsCount, retentionFilter, breakdown, hideSizeColumn) => { const { period } = retentionFilter || {} const { breakdowns } = breakdown || {} @@ -75,7 +81,7 @@ export const retentionTableLogic = kea([ ? dayjs(results[rowIndex].date).format('MMM D, h A') : dayjs(results[rowIndex].date).format('MMM D'), // Second column is the first value (which is essentially the total) - results[rowIndex].values[0].count, + ...(hideSizeColumn ? [] : [results[rowIndex].values[0].count]), // All other columns are rendered as percentage ...results[rowIndex].values.map((row) => { const percentage = diff --git a/frontend/src/scenes/saved-insights/SavedInsights.scss b/frontend/src/scenes/saved-insights/SavedInsights.scss index 73ff3ab53d0fb..a50d141741fc7 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.scss +++ b/frontend/src/scenes/saved-insights/SavedInsights.scss @@ -4,16 +4,22 @@ > .LemonTable td { padding-bottom: 0.75rem !important; padding-top: 0.75rem !important; - &.icon-column .LemonSkeleton { - height: 2rem; - width: 2rem; + + &.icon-column { + font-size: 1.5rem; + color: var(--muted); + + .LemonSkeleton { + height: 2rem; + width: 2rem; + } } } .new-insight-dropdown-btn { cursor: pointer; height: 40px; - background-color: var(--primary); + background-color: var(--primary-3000); padding: 8px 12px 8px 16px; border: 1px solid var(--border); border-radius: 4px; @@ -67,11 +73,13 @@ .insight-type-icon-wrapper { display: flex; align-items: center; + .icon-container { height: 22px; width: 22px; margin-right: 8px; position: relative; + .icon-container-inner { font-size: 22px; margin-left: -2px; @@ -83,9 +91,11 @@ position: relative; display: grid; grid-gap: 1rem; + @include screen($xl) { grid-template-columns: repeat(2, 1fr); } + @include screen($xxl) { grid-template-columns: repeat(3, 1fr); } diff --git a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx index 3b4ee1eb9a163..4e9f15449536f 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx @@ -1,16 +1,15 @@ import { Meta, Story } from '@storybook/react' - +import { router } from 'kea-router' +import { useEffect } from 'react' import { App } from 'scenes/App' -import insightsJson from './__mocks__/insights.json' -import { useEffect } from 'react' -import { router } from 'kea-router' import { mswDecorator, useStorybookMocks } from '~/mocks/browser' +import { EMPTY_PAGINATED_RESPONSE, toPaginatedResponse } from '~/mocks/handlers' +import funnelTopToBottom from '../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottom.json' import trendsBarBreakdown from '../../mocks/fixtures/api/projects/team_id/insights/trendsBarBreakdown.json' import trendsPieBreakdown from '../../mocks/fixtures/api/projects/team_id/insights/trendsPieBreakdown.json' -import funnelTopToBottom from '../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottom.json' -import { EMPTY_PAGINATED_RESPONSE, toPaginatedResponse } from '~/mocks/handlers' +import insightsJson from './__mocks__/insights.json' const insights = [trendsBarBreakdown, trendsPieBreakdown, funnelTopToBottom] diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index 229ebc9b0c613..8b5efc9cfe5f5 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -1,14 +1,26 @@ +import './SavedInsights.scss' + +import { + IconBrackets, + IconFunnels, + IconHogQL, + IconLifecycle, + IconRetention, + IconStar, + IconStarFilled, + IconStickiness, + IconTrends, + IconUserPaths, +} from '@posthog/icons' +import { LemonSelectOptions } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { Link } from 'lib/lemon-ui/Link' +import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' +import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' +import { InsightCard } from 'lib/components/Cards/InsightCard' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' -import { deleteWithUndo } from 'lib/utils' -import { InsightModel, InsightType, LayoutView, SavedInsightsTabs } from '~/types' -import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' -import './SavedInsights.scss' -import { organizationLogic } from 'scenes/organizationLogic' import { PageHeader } from 'lib/components/PageHeader' -import { SavedInsightsEmptyState } from 'scenes/insights/EmptyStates' -import { teamLogic } from '../teamLogic' +import { TZLabel } from 'lib/components/TZLabel' +import { FEATURE_FLAGS } from 'lib/constants' import { IconAction, IconBarChart, @@ -19,45 +31,38 @@ import { IconPerson, IconPlusMini, IconSelectEvents, - IconStarFilled, - IconStarOutline, IconTableChart, - InsightsFunnelsIcon, - InsightsLifecycleIcon, - InsightsPathsIcon, - InsightSQLIcon, - InsightsRetentionIcon, - InsightsStickinessIcon, - InsightsTrendsIcon, } from 'lib/lemon-ui/icons' -import { SceneExport } from 'scenes/sceneTypes' -import { TZLabel } from 'lib/components/TZLabel' -import { urls } from 'scenes/urls' -import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonButton, LemonButtonWithSideAction, LemonButtonWithSideActionProps } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { LemonSegmentedButton } from 'lib/lemon-ui/LemonSegmentedButton' +import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { LemonButton, LemonButtonWithSideAction, LemonButtonWithSideActionProps } from 'lib/lemon-ui/LemonButton' -import { InsightCard } from 'lib/components/Cards/InsightCard' - -import { groupsModel } from '~/models/groupsModel' -import { cohortsModel } from '~/models/cohortsModel' -import { mathsLogic } from 'scenes/trends/mathsLogic' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Link } from 'lib/lemon-ui/Link' import { PaginationControl, usePagination } from 'lib/lemon-ui/PaginationControl' -import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { LemonSelectOptions } from '@posthog/lemon-ui' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' +import { SavedInsightsEmptyState } from 'scenes/insights/EmptyStates' +import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { organizationLogic } from 'scenes/organizationLogic' +import { overlayForNewInsightMenu } from 'scenes/saved-insights/newInsightsMenu' import { SavedInsightsFilters } from 'scenes/saved-insights/SavedInsightsFilters' +import { SceneExport } from 'scenes/sceneTypes' +import { mathsLogic } from 'scenes/trends/mathsLogic' +import { urls } from 'scenes/urls' + +import { cohortsModel } from '~/models/cohortsModel' +import { groupsModel } from '~/models/groupsModel' import { NodeKind } from '~/queries/schema' -import { LemonSegmentedButton } from 'lib/lemon-ui/LemonSegmentedButton' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' import { isInsightVizNode } from '~/queries/utils' -import { overlayForNewInsightMenu } from 'scenes/saved-insights/newInsightsMenu' -import { summarizeInsight } from 'scenes/insights/summarizeInsight' -import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { InsightModel, InsightType, LayoutView, SavedInsightsTabs } from '~/types' + +import { teamLogic } from '../teamLogic' +import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' interface NewInsightButtonProps { dataAttr: string @@ -74,49 +79,49 @@ export const INSIGHT_TYPES_METADATA: Record = [InsightType.TRENDS]: { name: 'Trends', description: 'Visualize and break down how actions or events vary over time.', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, [InsightType.FUNNELS]: { name: 'Funnel', description: 'Discover how many users complete or drop out of a sequence of actions.', - icon: InsightsFunnelsIcon, + icon: IconFunnels, inMenu: true, }, [InsightType.RETENTION]: { name: 'Retention', description: 'See how many users return on subsequent days after an intial action.', - icon: InsightsRetentionIcon, + icon: IconRetention, inMenu: true, }, [InsightType.PATHS]: { name: 'Paths', description: 'Trace the journeys users take within your product and where they drop off.', - icon: InsightsPathsIcon, + icon: IconUserPaths, inMenu: true, }, [InsightType.STICKINESS]: { name: 'Stickiness', description: 'See what keeps users coming back by viewing the interval between repeated actions.', - icon: InsightsStickinessIcon, + icon: IconStickiness, inMenu: true, }, [InsightType.LIFECYCLE]: { name: 'Lifecycle', description: 'Understand growth by breaking down new, resurrected, returning and dormant users.', - icon: InsightsLifecycleIcon, + icon: IconLifecycle, inMenu: true, }, [InsightType.SQL]: { name: 'SQL', description: 'Use HogQL to query your data.', - icon: InsightSQLIcon, + icon: IconHogQL, inMenu: true, }, [InsightType.JSON]: { name: 'Custom', description: 'Save components powered by our JSON query language.', - icon: InsightSQLIcon, + icon: IconBrackets, inMenu: true, }, } @@ -125,37 +130,37 @@ export const QUERY_TYPES_METADATA: Record = { [NodeKind.TrendsQuery]: { name: 'Trends', description: 'Visualize and break down how actions or events vary over time', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, [NodeKind.FunnelsQuery]: { name: 'Funnel', description: 'Discover how many users complete or drop out of a sequence of actions', - icon: InsightsFunnelsIcon, + icon: IconFunnels, inMenu: true, }, [NodeKind.RetentionQuery]: { name: 'Retention', description: 'See how many users return on subsequent days after an intial action', - icon: InsightsRetentionIcon, + icon: IconRetention, inMenu: true, }, [NodeKind.PathsQuery]: { name: 'Paths', description: 'Trace the journeys users take within your product and where they drop off', - icon: InsightsPathsIcon, + icon: IconUserPaths, inMenu: true, }, [NodeKind.StickinessQuery]: { name: 'Stickiness', description: 'See what keeps users coming back by viewing the interval between repeated actions', - icon: InsightsStickinessIcon, + icon: IconStickiness, inMenu: true, }, [NodeKind.LifecycleQuery]: { name: 'Lifecycle', description: 'Understand growth by breaking down new, resurrected, returning and dormant users', - icon: InsightsLifecycleIcon, + icon: IconLifecycle, inMenu: true, }, [NodeKind.EventsNode]: { @@ -239,43 +244,43 @@ export const QUERY_TYPES_METADATA: Record = { [NodeKind.SessionsTimelineQuery]: { name: 'Sessions', description: 'Sessions timeline query', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, [NodeKind.HogQLQuery]: { name: 'HogQL', description: 'Direct HogQL query', - icon: InsightSQLIcon, + icon: IconHogQL, inMenu: true, }, [NodeKind.HogQLMetadata]: { name: 'HogQL Metadata', description: 'Metadata for a HogQL query', - icon: InsightSQLIcon, + icon: IconHogQL, inMenu: true, }, [NodeKind.DatabaseSchemaQuery]: { name: 'Database Schema', description: 'Introspect the PostHog database schema', - icon: InsightSQLIcon, + icon: IconHogQL, inMenu: true, }, [NodeKind.WebOverviewQuery]: { name: 'Overview Stats', description: 'View overview stats for a website', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, [NodeKind.WebStatsTableQuery]: { name: 'Web Table', description: 'A table of results from web analytics, with a breakdown', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, [NodeKind.WebTopClicksQuery]: { name: 'Top Clicks', description: 'View top clicks for a website', - icon: InsightsTrendsIcon, + icon: IconTrends, inMenu: true, }, } @@ -301,7 +306,7 @@ export function InsightIcon({ insight }: { insight: InsightModel }): JSX.Element } const insightMetadata = INSIGHT_TYPES_METADATA[insightType] if (insightMetadata && insightMetadata.icon) { - return + return } return null } @@ -353,8 +358,8 @@ function SavedInsightsGrid(): JSX.Element { insight={{ ...insight }} rename={() => renameInsight(insight)} duplicate={() => duplicateInsight(insight)} - deleteWithUndo={() => - deleteWithUndo({ + deleteWithUndo={async () => + await deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: loadInsights, @@ -426,10 +431,10 @@ export function SavedInsights(): JSX.Element { insight.favorited ? ( ) : ( - + ) } - tooltip={`${insight.favorited ? 'Add to' : 'Remove from'} favorite insights`} + tooltip={`${insight.favorited ? 'Remove from' : 'Add to'} favorite insights`} /> {hasDashboardCollaboration && insight.description && ( @@ -501,7 +506,7 @@ export function SavedInsights(): JSX.Element { - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: loadInsights, @@ -522,7 +527,10 @@ export function SavedInsights(): JSX.Element { return (
    - } /> + } + /> setSavedInsightsFilters({ tab })} diff --git a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx index 33369ddf00218..e1e9e66aa111c 100644 --- a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx @@ -1,13 +1,14 @@ -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { IconCalendar } from '@posthog/icons' +import { useActions, useValues } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { SavedInsightsTabs } from '~/types' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { LemonSelect } from 'lib/lemon-ui/LemonSelect' +import { membersLogic } from 'scenes/organization/membersLogic' import { INSIGHT_TYPE_OPTIONS } from 'scenes/saved-insights/SavedInsights' -import { useActions, useValues } from 'kea' -import { dashboardsModel } from '~/models/dashboardsModel' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { membersLogic } from 'scenes/organization/membersLogic' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { IconCalendar } from '@posthog/icons' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { SavedInsightsTabs } from '~/types' export function SavedInsightsFilters(): JSX.Element { const { nameSortedDashboards } = useValues(dashboardsModel) diff --git a/frontend/src/scenes/saved-insights/activityDescriptions.tsx b/frontend/src/scenes/saved-insights/activityDescriptions.tsx index ab67fb7972184..1ab0fb0e218c2 100644 --- a/frontend/src/scenes/saved-insights/activityDescriptions.tsx +++ b/frontend/src/scenes/saved-insights/activityDescriptions.tsx @@ -1,3 +1,6 @@ +import '../../lib/components/Cards/InsightCard/InsightCard.scss' + +import { captureException } from '@sentry/react' import { ActivityChange, ActivityLogItem, @@ -6,18 +9,17 @@ import { detectBoolean, HumanizedChange, } from 'lib/components/ActivityLog/humanizeActivity' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' -import { FilterType, InsightModel, InsightShortId } from '~/types' +import { SentenceList } from 'lib/components/ActivityLog/SentenceList' import { BreakdownSummary, FiltersSummary, QuerySummary } from 'lib/components/Cards/InsightCard/InsightDetails' -import '../../lib/components/Cards/InsightCard/InsightCard.scss' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' +import { Link } from 'lib/lemon-ui/Link' import { areObjectValuesEmpty, pluralize } from 'lib/utils' -import { SentenceList } from 'lib/components/ActivityLog/SentenceList' +import { urls } from 'scenes/urls' + import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { InsightQueryNode, QuerySchema } from '~/queries/schema' import { isInsightQueryNode } from '~/queries/utils' -import { captureException } from '@sentry/react' +import { FilterType, InsightModel, InsightShortId } from '~/types' const nameOrLinkToInsight = (short_id?: InsightShortId | null, name?: string | null): string | JSX.Element => { const displayName = name || '(empty string)' diff --git a/frontend/src/scenes/saved-insights/newInsightsMenu.tsx b/frontend/src/scenes/saved-insights/newInsightsMenu.tsx index 3e67827c42907..e7dee45af8d99 100644 --- a/frontend/src/scenes/saved-insights/newInsightsMenu.tsx +++ b/frontend/src/scenes/saved-insights/newInsightsMenu.tsx @@ -1,9 +1,10 @@ -import { InsightType } from '~/types' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { INSIGHT_TYPES_METADATA, InsightTypeMetadata } from 'scenes/saved-insights/SavedInsights' import { ReactNode } from 'react' import { insightTypeURL } from 'scenes/insights/utils' +import { INSIGHT_TYPES_METADATA, InsightTypeMetadata } from 'scenes/saved-insights/SavedInsights' + +import { InsightType } from '~/types' function insightTypesForMenu(): [string, InsightTypeMetadata][] { // never show JSON InsightType in the menu @@ -33,7 +34,7 @@ export function overlayForNewInsightMenu(dataAttr: string): ReactNode[] { >
    {listedInsightTypeMetadata.name} - {listedInsightTypeMetadata.description} + {listedInsightTypeMetadata.description}
    ) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts index 34475f73408c7..37957afac97c9 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts @@ -1,16 +1,18 @@ -import { expectLogic, partial } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { InsightsResult, savedInsightsLogic } from './savedInsightsLogic' -import { InsightModel, InsightType } from '~/types' import { combineUrl, router } from 'kea-router' -import { urls } from 'scenes/urls' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' -import { useMocks } from '~/mocks/jest' +import { expectLogic, partial } from 'kea-test-utils' import api from 'lib/api' import { MOCK_TEAM_ID } from 'lib/api.mock' -import { DuplicateDashboardForm, duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' import { DeleteDashboardForm, deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' +import { DuplicateDashboardForm, duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { urls } from 'scenes/urls' + +import { useMocks } from '~/mocks/jest' import { dashboardsModel } from '~/models/dashboardsModel' +import { initKeaTests } from '~/test/init' +import { InsightModel, InsightType } from '~/types' + +import { InsightsResult, savedInsightsLogic } from './savedInsightsLogic' jest.spyOn(api, 'create') @@ -193,7 +195,7 @@ describe('savedInsightsLogic', () => { const sourceInsight = createInsight(123, 'hello') sourceInsight.name = '' sourceInsight.derived_name = 'should be copied' - await logic.actions.duplicateInsight(sourceInsight) + await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( `api/projects/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: '' }) @@ -204,7 +206,7 @@ describe('savedInsightsLogic', () => { const sourceInsight = createInsight(123, 'hello') sourceInsight.name = 'should be copied' sourceInsight.derived_name = '' - await logic.actions.duplicateInsight(sourceInsight) + await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( `api/projects/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: 'should be copied (copy)' }) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index c1bb0bfdb91a5..cb1f40ff676d0 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -1,22 +1,24 @@ +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners } from 'kea' -import { router, actionToUrl, urlToAction } from 'kea-router' +import { actionToUrl, router, urlToAction } from 'kea-router' import api from 'lib/api' -import { objectDiffShallow, objectsEqual, toParams } from 'lib/utils' -import { InsightModel, LayoutView, SavedInsightsTabs } from '~/types' -import type { savedInsightsLogicType } from './savedInsightsLogicType' import { dayjs } from 'lib/dayjs' -import { insightsModel } from '~/models/insightsModel' -import { teamLogic } from '../teamLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { Sorting } from 'lib/lemon-ui/LemonTable' -import { urls } from 'scenes/urls' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { PaginationManual } from 'lib/lemon-ui/PaginationControl' -import { dashboardsModel } from '~/models/dashboardsModel' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { objectDiffShallow, objectsEqual, toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { urls } from 'scenes/urls' + +import { dashboardsModel } from '~/models/dashboardsModel' +import { insightsModel } from '~/models/insightsModel' +import { InsightModel, LayoutView, SavedInsightsTabs } from '~/types' + +import { teamLogic } from '../teamLogic' +import type { savedInsightsLogicType } from './savedInsightsLogicType' export const INSIGHTS_PER_PAGE = 30 @@ -38,7 +40,7 @@ export interface SavedInsightFilters { search: string insightType: string createdBy: number | 'All users' - dateFrom: string | dayjs.Dayjs | undefined | 'all' | null + dateFrom: string | dayjs.Dayjs | undefined | null dateTo: string | dayjs.Dayjs | undefined | null page: number dashboardId: number | undefined | null diff --git a/frontend/src/scenes/sceneLogic.test.tsx b/frontend/src/scenes/sceneLogic.test.tsx index 478b3047d70ed..0ca7d96dd0e28 100644 --- a/frontend/src/scenes/sceneLogic.test.tsx +++ b/frontend/src/scenes/sceneLogic.test.tsx @@ -1,13 +1,14 @@ -import { sceneLogic } from './sceneLogic' -import { initKeaTests } from '~/test/init' +import { kea, path } from 'kea' +import { router } from 'kea-router' import { expectLogic, partial, truth } from 'kea-test-utils' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' -import { kea, path } from 'kea' +import { initKeaTests } from '~/test/init' + +import { sceneLogic } from './sceneLogic' import type { logicType } from './sceneLogic.testType' export const Component = (): JSX.Element =>
    diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index bd5c46206d001..a08bfab9ef878 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -1,20 +1,20 @@ -import { BuiltLogic, kea, props, path, connect, actions, reducers, selectors, listeners } from 'kea' +import { actions, BuiltLogic, connect, kea, listeners, path, props, reducers, selectors } from 'kea' import { router, urlToAction } from 'kea-router' -import posthog from 'posthog-js' -import type { sceneLogicType } from './sceneLogicType' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { preflightLogic } from './PreflightCheck/preflightLogic' +import posthog from 'posthog-js' +import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations } from 'scenes/scenes' +import { LoadedScene, Params, Scene, SceneConfig, SceneExport, SceneParams } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { AvailableFeature } from '~/types' -import { userLogic } from './userLogic' + +import { appContextLogic } from './appContextLogic' import { handleLoginRedirect } from './authentication/loginLogic' -import { teamLogic } from './teamLogic' -import { urls } from 'scenes/urls' -import { LoadedScene, Params, Scene, SceneConfig, SceneExport, SceneParams } from 'scenes/sceneTypes' -import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations } from 'scenes/scenes' import { organizationLogic } from './organizationLogic' -import { appContextLogic } from './appContextLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import { preflightLogic } from './PreflightCheck/preflightLogic' +import type { sceneLogicType } from './sceneLogicType' +import { teamLogic } from './teamLogic' +import { userLogic } from './userLogic' /** Mapping of some scenes that aren't directly accessible from the sidebar to ones that are - for the sidebar. */ const sceneNavAlias: Partial> = { @@ -256,21 +256,12 @@ export const sceneLogic = kea([ !location.pathname.startsWith('/settings') ) { if ( - featureFlagLogic.values.featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] === - 'test' && + !teamLogic.values.currentTeam.completed_snippet_onboarding && !Object.keys(teamLogic.values.currentTeam.has_completed_onboarding_for || {}).length ) { - console.warn('No onboarding completed, redirecting to products') + console.warn('No onboarding completed, redirecting to /products') router.actions.replace(urls.products()) return - } else if ( - featureFlagLogic.values.featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== - 'test' && - !teamLogic.values.currentTeam.completed_snippet_onboarding - ) { - console.warn('Ingestion tutorial not completed, redirecting to it') - router.actions.replace(urls.ingestion()) - return } } } diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 5d5ed7a89c3ff..3f41023e13f63 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -22,6 +22,7 @@ export enum Scene { PersonsManagement = 'PersonsManagement', Person = 'Person', Pipeline = 'Pipeline', + PipelineApp = 'PipelineApp', Group = 'Group', Action = 'Action', Experiments = 'Experiments', @@ -64,7 +65,6 @@ export enum Scene { PasswordReset = 'PasswordReset', PasswordResetComplete = 'PasswordResetComplete', PreflightCheck = 'PreflightCheck', - Ingestion = 'IngestionWizard', OrganizationCreationConfirm = 'OrganizationCreationConfirm', Unsubscribe = 'Unsubscribe', DebugQuery = 'DebugQuery', diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index dc5aa42bb885e..370847c6f4479 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -1,14 +1,15 @@ +import { combineUrl } from 'kea-router' +import { dayjs } from 'lib/dayjs' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { getDefaultEventsSceneQuery } from 'scenes/events/defaults' import { LoadedScene, Params, Scene, SceneConfig } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { Error404 as Error404Component } from '~/layout/Error404' import { ErrorNetwork as ErrorNetworkComponent } from '~/layout/ErrorNetwork' import { ErrorProjectUnavailable as ErrorProjectUnavailableComponent } from '~/layout/ErrorProjectUnavailable' -import { urls } from 'scenes/urls' -import { InsightShortId, PipelineTabs, PropertyFilterType, ReplayTabs } from '~/types' -import { combineUrl } from 'kea-router' -import { getDefaultEventsSceneQuery } from 'scenes/events/defaults' import { EventsQuery } from '~/queries/schema' -import { dayjs } from 'lib/dayjs' -import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { InsightShortId, PipelineAppTabs, PipelineTabs, PropertyFilterType, ReplayTabs } from '~/types' export const emptySceneParams = { params: {}, searchParams: {}, hashParams: {} } @@ -45,7 +46,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.WebAnalytics]: { projectBased: true, - name: 'Web Analytics', + name: 'Web analytics', layout: 'app-container', }, [Scene.Cohort]: { @@ -54,43 +55,43 @@ export const sceneConfigurations: Partial> = { }, [Scene.Events]: { projectBased: true, - name: 'Event Explorer', + name: 'Event explorer', }, [Scene.BatchExports]: { projectBased: true, - name: 'Batch Exports', + name: 'Batch exports', }, [Scene.BatchExportEdit]: { projectBased: true, - name: 'Edit Batch Export', + name: 'Edit batch export', }, [Scene.BatchExport]: { projectBased: true, - name: 'Batch Export', + name: 'Batch export', }, [Scene.DataManagement]: { projectBased: true, - name: 'Data Management', + name: 'Data management', }, [Scene.EventDefinition]: { projectBased: true, - name: 'Data Management', + name: 'Data management', }, [Scene.PropertyDefinition]: { projectBased: true, - name: 'Data Management', + name: 'Data management', }, [Scene.Replay]: { projectBased: true, - name: 'Session Replay', + name: 'Session replay', }, [Scene.ReplaySingle]: { projectBased: true, - name: 'Replay Recording', + name: 'Replay recording', }, [Scene.ReplayPlaylist]: { projectBased: true, - name: 'Replay Playlist', + name: 'Replay playlist', }, [Scene.Person]: { projectBased: true, @@ -98,7 +99,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.PersonsManagement]: { projectBased: true, - name: 'Persons & Groups', + name: 'People & groups', }, [Scene.Action]: { projectBased: true, @@ -106,15 +107,19 @@ export const sceneConfigurations: Partial> = { }, [Scene.Group]: { projectBased: true, - name: 'Persons & Groups', + name: 'People & groups', }, [Scene.Pipeline]: { projectBased: true, name: 'Pipeline', }, + [Scene.PipelineApp]: { + projectBased: true, + name: 'Pipeline app', + }, [Scene.Experiments]: { projectBased: true, - name: 'Experiments', + name: 'A/B testing', }, [Scene.Experiment]: { projectBased: true, @@ -122,7 +127,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.FeatureFlags]: { projectBased: true, - name: 'Feature Flags', + name: 'Feature flags', }, [Scene.FeatureFlag]: { projectBased: true, @@ -141,27 +146,27 @@ export const sceneConfigurations: Partial> = { }, [Scene.DataWarehouse]: { projectBased: true, - name: 'Data Warehouse', + name: 'Data warehouse', }, [Scene.DataWarehousePosthog]: { projectBased: true, - name: 'Data Warehouse', + name: 'Data warehouse', }, [Scene.DataWarehouseExternal]: { projectBased: true, - name: 'Data Warehouse', + name: 'Data warehouse', }, [Scene.DataWarehouseSavedQueries]: { projectBased: true, - name: 'Data Warehouse', + name: 'Data warehouse', }, [Scene.DataWarehouseSettings]: { projectBased: true, - name: 'Data Warehouse Settings', + name: 'Data warehouse settings', }, [Scene.DataWarehouseTable]: { projectBased: true, - name: 'Data Warehouse Table', + name: 'Data warehouse table', }, [Scene.EarlyAccessFeatures]: { projectBased: true, @@ -183,18 +188,14 @@ export const sceneConfigurations: Partial> = { }, [Scene.SavedInsights]: { projectBased: true, - name: 'Insights', + name: 'Product analytics', }, [Scene.ProjectHomepage]: { projectBased: true, name: 'Homepage', }, [Scene.IntegrationsRedirect]: { - name: 'Integrations Redirect', - }, - [Scene.Ingestion]: { - projectBased: true, - layout: 'plain', + name: 'Integrations redirect', }, [Scene.Products]: { projectBased: true, @@ -206,7 +207,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.ToolbarLaunch]: { projectBased: true, - name: 'Launch Toolbar', + name: 'Launch toolbar', }, [Scene.Site]: { projectBased: true, @@ -405,10 +406,14 @@ export const routes: Record = { [urls.persons()]: Scene.PersonsManagement, [urls.pipeline()]: Scene.Pipeline, // One entry for every available tab - ...Object.values(PipelineTabs).reduce((acc, tab) => { - acc[urls.pipeline(tab)] = Scene.Pipeline - return acc - }, {} as Record), + ...(Object.fromEntries(Object.values(PipelineTabs).map((tab) => [urls.pipeline(tab), Scene.Pipeline])) as Record< + string, + Scene + >), + // One entry for each available tab (key by app config id) + ...(Object.fromEntries( + Object.values(PipelineAppTabs).map((tab) => [urls.pipelineApp(':id', tab), Scene.PipelineApp]) + ) as Record), [urls.groups(':groupTypeIndex')]: Scene.PersonsManagement, [urls.group(':groupTypeIndex', ':groupKey', false)]: Scene.Group, [urls.group(':groupTypeIndex', ':groupKey', false, ':groupTab')]: Scene.Group, @@ -464,8 +469,6 @@ export const routes: Record = { [urls.inviteSignup(':id')]: Scene.InviteSignup, [urls.passwordReset()]: Scene.PasswordReset, [urls.passwordResetComplete(':uuid', ':token')]: Scene.PasswordResetComplete, - [urls.ingestion()]: Scene.Ingestion, - [urls.ingestion() + '/*']: Scene.Ingestion, [urls.products()]: Scene.Products, [urls.onboarding(':productKey')]: Scene.Onboarding, [urls.verifyEmail()]: Scene.VerifyEmail, diff --git a/frontend/src/scenes/session-recordings/SessionRecordings.tsx b/frontend/src/scenes/session-recordings/SessionRecordings.tsx index fa3277a7283e1..a11e6c7c11acf 100644 --- a/frontend/src/scenes/session-recordings/SessionRecordings.tsx +++ b/frontend/src/scenes/session-recordings/SessionRecordings.tsx @@ -1,30 +1,32 @@ -import { PageHeader } from 'lib/components/PageHeader' -import { teamLogic } from 'scenes/teamLogic' -import { useActions, useValues } from 'kea' -import { urls } from 'scenes/urls' -import { SceneExport } from 'scenes/sceneTypes' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from '@posthog/lemon-ui' -import { AvailableFeature, NotebookNodeType, ReplayTabs } from '~/types' -import { SavedSessionRecordingPlaylists } from './saved-playlists/SavedSessionRecordingPlaylists' -import { humanFriendlyTabName, sessionRecordingsLogic } from './sessionRecordingsLogic' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { IconSettings } from 'lib/lemon-ui/icons' +import { useActions, useValues } from 'kea' import { router } from 'kea-router' -import { SessionRecordingFilePlayback } from './file-playback/SessionRecordingFilePlayback' -import { createPlaylist } from './playlist/playlistUtils' +import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { PageHeader } from 'lib/components/PageHeader' +import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' import { useAsyncHandler } from 'lib/hooks/useAsyncHandler' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { IconSettings } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { sceneLogic } from 'scenes/sceneLogic' -import { savedSessionRecordingPlaylistsLogic } from './saved-playlists/savedSessionRecordingPlaylistsLogic' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { SceneExport } from 'scenes/sceneTypes' import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' -import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' -import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { SessionRecordingsPlaylist } from './playlist/SessionRecordingsPlaylist' -import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' + import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' +import { AvailableFeature, NotebookNodeType, ReplayTabs } from '~/types' + +import { SessionRecordingFilePlayback } from './file-playback/SessionRecordingFilePlayback' +import { createPlaylist } from './playlist/playlistUtils' +import { SessionRecordingsPlaylist } from './playlist/SessionRecordingsPlaylist' +import { SavedSessionRecordingPlaylists } from './saved-playlists/SavedSessionRecordingPlaylists' +import { savedSessionRecordingPlaylistsLogic } from './saved-playlists/savedSessionRecordingPlaylistsLogic' +import { humanFriendlyTabName, sessionRecordingsLogic } from './sessionRecordingsLogic' export function SessionsRecordings(): JSX.Element { const { currentTeam } = useValues(teamLogic) diff --git a/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx b/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx index 501351427ee2a..5aea487a76d0b 100644 --- a/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx +++ b/frontend/src/scenes/session-recordings/SessionsRecordings-player-success.stories.tsx @@ -1,23 +1,25 @@ import { Meta } from '@storybook/react' -import recordings from './__mocks__/recordings.json' -import { useEffect } from 'react' -import { mswDecorator } from '~/mocks/browser' import { combineUrl, router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' -import { snapshotsAsJSONLines } from 'scenes/session-recordings/__mocks__/recording_snapshots' -import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' import recordingEventsJson from 'scenes/session-recordings/__mocks__/recording_events_query' +import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' +import { snapshotsAsJSONLines } from 'scenes/session-recordings/__mocks__/recording_snapshots' +import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' + import recording_playlists from './__mocks__/recording_playlists.json' +import recordings from './__mocks__/recordings.json' const meta: Meta = { title: 'Scenes-App/Recordings', + tags: ['test-skip'], // TODO: Fix the flakey rendering due to player playback parameters: { layout: 'fullscreen', viewMode: 'story', mockDate: '2023-02-01', waitForSelector: '.PlayerFrame__content .replayer-wrapper iframe', - testOptions: { skip: true }, // TODO: Fix the flakey rendering due to player playback }, decorators: [ mswDecorator({ diff --git a/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx b/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx index 657fbccf4bc29..d1df2f5fe537a 100644 --- a/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx +++ b/frontend/src/scenes/session-recordings/SessionsRecordings-playlist-listing.stories.tsx @@ -1,13 +1,15 @@ import { Meta } from '@storybook/react' -import { useEffect } from 'react' -import { mswDecorator } from '~/mocks/browser' import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { useEffect } from 'react' import { App } from 'scenes/App' -import recording_playlists from './__mocks__/recording_playlists.json' -import { ReplayTabs } from '~/types' -import recordings from 'scenes/session-recordings/__mocks__/recordings.json' import recordingEventsJson from 'scenes/session-recordings/__mocks__/recording_events_query' +import recordings from 'scenes/session-recordings/__mocks__/recordings.json' +import { urls } from 'scenes/urls' + +import { mswDecorator } from '~/mocks/browser' +import { ReplayTabs } from '~/types' + +import recording_playlists from './__mocks__/recording_playlists.json' const meta: Meta = { title: 'Scenes-App/Recordings', diff --git a/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx b/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx index 721a17c735e9c..6b735227f6376 100644 --- a/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx +++ b/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx @@ -1,18 +1,18 @@ +import './SessionRecordingScene.scss' + import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { PageHeader } from 'lib/components/PageHeader' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' import { SceneExport } from 'scenes/sceneTypes' -import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' import { sessionRecordingDetailLogic, SessionRecordingDetailLogicProps, } from 'scenes/session-recordings/detail/sessionRecordingDetailLogic' import { RecordingNotFound } from 'scenes/session-recordings/player/RecordingNotFound' - -import './SessionRecordingScene.scss' +import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' export const scene: SceneExport = { logic: sessionRecordingDetailLogic, diff --git a/frontend/src/scenes/session-recordings/detail/sessionRecordingDetailLogic.ts b/frontend/src/scenes/session-recordings/detail/sessionRecordingDetailLogic.ts index a302c21fbdb76..aad8e724401e5 100644 --- a/frontend/src/scenes/session-recordings/detail/sessionRecordingDetailLogic.ts +++ b/frontend/src/scenes/session-recordings/detail/sessionRecordingDetailLogic.ts @@ -1,7 +1,10 @@ -import { kea, props, path, selectors } from 'kea' +import { kea, path, props, selectors } from 'kea' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + import { Breadcrumb, SessionRecordingType } from '~/types' + import type { sessionRecordingDetailLogicType } from './sessionRecordingDetailLogicType' -import { urls } from 'scenes/urls' export interface SessionRecordingDetailLogicProps { id?: SessionRecordingType['id'] @@ -12,13 +15,15 @@ export const sessionRecordingDetailLogic = kea( props({} as SessionRecordingDetailLogicProps), selectors({ breadcrumbs: [ - () => [(_, props) => props.id], + () => [(_, props) => props.id as SessionRecordingType['id']], (sessionRecordingId): Breadcrumb[] => [ { + key: Scene.Replay, name: `Replay`, path: urls.replay(), }, { + key: sessionRecordingId, name: sessionRecordingId ?? 'Not Found', path: sessionRecordingId ? urls.replaySingle(sessionRecordingId) : undefined, }, diff --git a/frontend/src/scenes/session-recordings/file-playback/SessionRecordingFilePlayback.tsx b/frontend/src/scenes/session-recordings/file-playback/SessionRecordingFilePlayback.tsx index 21f2f6ff83877..0d0f0bd3a5346 100644 --- a/frontend/src/scenes/session-recordings/file-playback/SessionRecordingFilePlayback.tsx +++ b/frontend/src/scenes/session-recordings/file-playback/SessionRecordingFilePlayback.tsx @@ -1,13 +1,15 @@ +import Dragger from 'antd/lib/upload/Dragger' import { useActions, useValues } from 'kea' +import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' import { IconUploadFile } from 'lib/lemon-ui/icons' -import Dragger from 'antd/lib/upload/Dragger' -import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' -import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { sessionRecordingFilePlaybackLogic } from './sessionRecordingFilePlaybackLogic' +import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { userLogic } from 'scenes/userLogic' + import { AvailableFeature } from '~/types' -import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' + +import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' +import { sessionRecordingFilePlaybackLogic } from './sessionRecordingFilePlaybackLogic' export function SessionRecordingFilePlayback(): JSX.Element { const { loadFromFile, resetSessionRecording } = useActions(sessionRecordingFilePlaybackLogic) diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.test.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.test.ts index 91ac5efa25cd9..05b3265f43757 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.test.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.test.ts @@ -1,5 +1,7 @@ -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' + +import { initKeaTests } from '~/test/init' + import { sessionRecordingFilePlaybackLogic } from './sessionRecordingFilePlaybackLogic' describe('sessionRecordingFilePlaybackLogic', () => { diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts index 54ef82ab8da18..8126bf6a97c35 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts @@ -1,19 +1,19 @@ +import { lemonToast } from '@posthog/lemon-ui' +import { eventWithTime } from '@rrweb/types' import { BuiltLogic, connect, kea, listeners, path, reducers, selectors } from 'kea' -import { Breadcrumb, PersonType, RecordingSnapshot, SessionRecordingType } from '~/types' -import { urls } from 'scenes/urls' import { loaders } from 'kea-loaders' - import { beforeUnload } from 'kea-router' -import { lemonToast } from '@posthog/lemon-ui' - -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { dayjs } from 'lib/dayjs' import { uuid } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb, PersonType, RecordingSnapshot, ReplayTabs, SessionRecordingType } from '~/types' -import type { sessionRecordingFilePlaybackLogicType } from './sessionRecordingFilePlaybackLogicType' -import { eventWithTime } from '@rrweb/types' -import type { sessionRecordingDataLogicType } from '../player/sessionRecordingDataLogicType' import { prepareRecordingSnapshots, sessionRecordingDataLogic } from '../player/sessionRecordingDataLogic' -import { dayjs } from 'lib/dayjs' +import type { sessionRecordingDataLogicType } from '../player/sessionRecordingDataLogicType' +import type { sessionRecordingFilePlaybackLogicType } from './sessionRecordingFilePlaybackLogicType' export type ExportedSessionRecordingFileV1 = { version: '2022-12-02' @@ -196,10 +196,12 @@ export const sessionRecordingFilePlaybackLogic = kea [], (): Breadcrumb[] => [ { - name: `Recordings`, + key: Scene.Replay, + name: `Session replay`, path: urls.replay(), }, { + key: ReplayTabs.FilePlayback, name: 'Import', }, ], diff --git a/frontend/src/scenes/session-recordings/filters/AdvancedSessionRecordingsFilters.tsx b/frontend/src/scenes/session-recordings/filters/AdvancedSessionRecordingsFilters.tsx index e9c45d8d2efb7..45725e31c5a1e 100644 --- a/frontend/src/scenes/session-recordings/filters/AdvancedSessionRecordingsFilters.tsx +++ b/frontend/src/scenes/session-recordings/filters/AdvancedSessionRecordingsFilters.tsx @@ -1,18 +1,19 @@ -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' - -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { EntityTypes, FilterableLogLevel, FilterType, RecordingDurationFilter, RecordingFilters } from '~/types' -import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { DurationFilter } from './DurationFilter' import { LemonButtonWithDropdown, LemonCheckbox, LemonInput, LemonTag, Tooltip } from '@posthog/lemon-ui' -import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' import { useValues } from 'kea' -import { FEATURE_FLAGS } from 'lib/constants' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { FEATURE_FLAGS } from 'lib/constants' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' + import { groupsModel } from '~/models/groupsModel' +import { EntityTypes, FilterableLogLevel, FilterType, RecordingDurationFilter, RecordingFilters } from '~/types' + +import { DurationFilter } from './DurationFilter' export const AdvancedSessionRecordingsFilters = ({ filters, diff --git a/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts b/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts index ec8b402d74710..bfc4fb0efff0a 100644 --- a/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts +++ b/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts @@ -1,4 +1,5 @@ import { DurationType, PropertyFilterType, PropertyOperator, RecordingDurationFilter } from '~/types' + import { humanFriendlyDurationFilter } from './DurationFilter' describe('DurationFilter', () => { @@ -33,7 +34,7 @@ describe('DurationFilter', () => { [PropertyOperator.GreaterThan, 3601, 'inactive_seconds', '> 3601 inactive seconds'], [PropertyOperator.GreaterThan, 3660, 'inactive_seconds', '> 61 inactive minutes'], [PropertyOperator.LessThan, 0, 'active_seconds', '< 0 active seconds'], - ])('converts the value correctly for total duration', async (operator, value, durationType, expectation) => { + ])('converts the value correctly for total duration', (operator, value, durationType, expectation) => { const filter: RecordingDurationFilter = { type: PropertyFilterType.Recording, key: 'duration', diff --git a/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx b/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx index cd0ae27d24f60..8b3788e1bc801 100644 --- a/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx +++ b/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx @@ -1,11 +1,12 @@ -import { DurationType, PropertyOperator, RecordingDurationFilter } from '~/types' +import { LemonButton } from '@posthog/lemon-ui' +import { convertSecondsToDuration, DurationPicker } from 'lib/components/DurationPicker/DurationPicker' import { OperatorSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect' import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { DurationPicker, convertSecondsToDuration } from 'lib/components/DurationPicker/DurationPicker' -import { LemonButton } from '@posthog/lemon-ui' import { useMemo, useState } from 'react' import { DurationTypeSelect } from 'scenes/session-recordings/filters/DurationTypeSelect' +import { DurationType, PropertyOperator, RecordingDurationFilter } from '~/types' + interface Props { recordingDurationFilter: RecordingDurationFilter durationTypeFilter: DurationType diff --git a/frontend/src/scenes/session-recordings/filters/DurationTypeSelect.tsx b/frontend/src/scenes/session-recordings/filters/DurationTypeSelect.tsx index 9b72d6355b4c4..e6585bf4306d4 100644 --- a/frontend/src/scenes/session-recordings/filters/DurationTypeSelect.tsx +++ b/frontend/src/scenes/session-recordings/filters/DurationTypeSelect.tsx @@ -1,7 +1,8 @@ import { LemonSelect } from '@posthog/lemon-ui' -import { DurationType } from '~/types' import { posthog } from 'posthog-js' +import { DurationType } from '~/types' + interface DurationTypeFilterProps { // what to call this when reporting analytics to PostHog onChangeEventDescription?: string diff --git a/frontend/src/scenes/session-recordings/filters/SessionRecordingsFilters.tsx b/frontend/src/scenes/session-recordings/filters/SessionRecordingsFilters.tsx index bc1e108bf7c19..89c7ceb424313 100644 --- a/frontend/src/scenes/session-recordings/filters/SessionRecordingsFilters.tsx +++ b/frontend/src/scenes/session-recordings/filters/SessionRecordingsFilters.tsx @@ -1,10 +1,12 @@ +import { LemonButton } from '@posthog/lemon-ui' +import equal from 'fast-deep-equal' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' -import { EntityTypes, FilterType, LocalRecordingFilters, RecordingFilters } from '~/types' import { useEffect, useState } from 'react' -import equal from 'fast-deep-equal' -import { LemonButton } from '@posthog/lemon-ui' -import { SimpleSessionRecordingsFilters } from './SimpleSessionRecordingsFilters' + +import { EntityTypes, FilterType, LocalRecordingFilters, RecordingFilters } from '~/types' + import { AdvancedSessionRecordingsFilters } from './AdvancedSessionRecordingsFilters' +import { SimpleSessionRecordingsFilters } from './SimpleSessionRecordingsFilters' interface SessionRecordingsFiltersProps { filters: RecordingFilters diff --git a/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx b/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx index 372e813f6cbdd..7020e27e22404 100644 --- a/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx +++ b/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx @@ -1,3 +1,16 @@ +import { urls } from '@posthog/apps-common' +import { LemonButton, Link } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' +import { TaxonomicPropertyFilter } from 'lib/components/PropertyFilters/components/TaxonomicPropertyFilter' +import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' +import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' +import { PropertyFilterLogicProps } from 'lib/components/PropertyFilters/types' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { IconPlus } from 'lib/lemon-ui/icons' +import { Popover } from 'lib/lemon-ui/Popover/Popover' +import { useMemo, useState } from 'react' +import { teamLogic } from 'scenes/teamLogic' + import { AnyPropertyFilter, EntityTypes, @@ -6,18 +19,6 @@ import { PropertyOperator, RecordingFilters, } from '~/types' -import { useMemo, useState } from 'react' -import { BindLogic, useActions, useValues } from 'kea' -import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' -import { TaxonomicPropertyFilter } from 'lib/components/PropertyFilters/components/TaxonomicPropertyFilter' -import { PropertyFilterLogicProps } from 'lib/components/PropertyFilters/types' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { teamLogic } from 'scenes/teamLogic' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { LemonButton, Link } from '@posthog/lemon-ui' -import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' -import { urls } from '@posthog/apps-common' -import { IconPlus } from 'lib/lemon-ui/icons' export const SimpleSessionRecordingsFilters = ({ filters, diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrame.scss b/frontend/src/scenes/session-recordings/player/PlayerFrame.scss index 78bbe50f457d0..28a6555789e01 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrame.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerFrame.scss @@ -14,7 +14,7 @@ position: absolute; iframe { - border: 0px; + border: 0; background-color: white; } @@ -40,14 +40,14 @@ display: inline-block; width: 20px; height: 20px; - background: rgb(73, 80, 246); + background: rgb(73 80 246); border-radius: 100%; transform: translate(-50%, -50%); opacity: 0.3; } .replayer-mouse.active::after { - animation: click 0.2s ease-in-out 1; + animation: PlayerFrame__click 0.2s ease-in-out 1; } .replayer-mouse.touch-device { @@ -59,12 +59,12 @@ border-radius: 100%; margin-left: -37px; margin-top: -37px; - border-color: rgba(73, 80, 246, 0); + border-color: rgb(73 80 246 / 0%); transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out; } .replayer-mouse.touch-device.touch-active { - border-color: rgba(73, 80, 246, 1); + border-color: rgb(73 80 246 / 100%); transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out; } @@ -73,7 +73,7 @@ } .replayer-mouse.touch-device.active::after { - animation: touch-click 0.2s ease-in-out 1; + animation: PlayerFrame__touch-click 0.2s ease-in-out 1; } .replayer-mouse-tail { @@ -81,12 +81,13 @@ pointer-events: none; } - @keyframes click { + @keyframes PlayerFrame__click { 0% { opacity: 0.3; width: 20px; height: 20px; } + 50% { opacity: 0.5; width: 10px; @@ -94,12 +95,13 @@ } } - @keyframes touch-click { + @keyframes PlayerFrame__touch-click { 0% { opacity: 0; width: 20px; height: 20px; } + 50% { opacity: 0.5; width: 10px; diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrame.tsx b/frontend/src/scenes/session-recordings/player/PlayerFrame.tsx index 47981346968b8..e31013c36e82b 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrame.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerFrame.tsx @@ -1,9 +1,10 @@ -import { useEffect, useRef } from 'react' +import './PlayerFrame.scss' + +import useSize from '@react-hook/size' import { Handler, viewportResizeDimension } from '@rrweb/types' import { useActions, useValues } from 'kea' +import { useEffect, useRef } from 'react' import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import useSize from '@react-hook/size' -import './PlayerFrame.scss' export const PlayerFrame = (): JSX.Element => { const replayDimensionRef = useRef() diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.scss b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.scss index 4ff8665202c6b..ad3bc2a5ae2bf 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.scss @@ -1,25 +1,17 @@ .PlayerFrameOverlay { position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; + inset: 0; z-index: 10; .PlayerFrameOverlay__content { position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - + inset: 0; display: flex; justify-content: center; align-items: center; z-index: 1; - transition: opacity 100ms; - background-color: rgba(0, 0, 0, 0.15); + background-color: rgb(0 0 0 / 15%); opacity: 0.8; &:hover { diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx index a1bec5bc8c8e2..84af7b84828eb 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx @@ -1,13 +1,16 @@ +import './PlayerFrameOverlay.scss' + +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { SessionPlayerState } from '~/types' import { IconErrorOutline, IconPlay } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import './PlayerFrameOverlay.scss' -import { PlayerUpNext } from './PlayerUpNext' import { useState } from 'react' -import clsx from 'clsx' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' + import { getCurrentExporterData } from '~/exporter/exporterViewLogic' +import { SessionPlayerState } from '~/types' + +import { PlayerUpNext } from './PlayerUpNext' const PlayerFrameOverlayContent = ({ currentPlayerState, diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss b/frontend/src/scenes/session-recordings/player/PlayerMeta.scss index 1864a9b8b3660..2089db1b92dd8 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerMeta.scss @@ -18,7 +18,7 @@ transition: 200ms height ease-out, 200ms border-bottom-color ease-out; &--enter { - height: 0px; + height: 0; } &--enter-active, @@ -33,7 +33,7 @@ } &--exit-active { - height: 0px; + height: 0; } } @@ -53,7 +53,7 @@ .PlayerMetaPersonProperties { position: fixed; top: 48px; - left: 0px; + left: 0; bottom: 97px; // NOTE: This isn't perfect but for now hardcoded to match the bottom area size. z-index: 1; max-width: 40rem; @@ -79,4 +79,14 @@ } } } + + .Link { + .posthog-3000 & { + color: var(--default); + + &:hover { + color: var(--primary-3000); + } + } + } } diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx index 1baf6cfc23832..354dd801ba128 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx @@ -1,26 +1,29 @@ import './PlayerMeta.scss' -import { dayjs } from 'lib/dayjs' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' + +import { Link } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useValues } from 'kea' -import { PersonDisplay } from 'scenes/persons/PersonDisplay' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' -import { TZLabel } from 'lib/components/TZLabel' -import { percentage } from 'lib/utils' -import { IconWindow } from 'scenes/session-recordings/player/icons' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import clsx from 'clsx' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { Link } from '@posthog/lemon-ui' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { PropertyIcon } from 'lib/components/PropertyIcon' +import { TZLabel } from 'lib/components/TZLabel' +import { dayjs } from 'lib/dayjs' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { PlayerMetaLinks } from './PlayerMetaLinks' -import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from './sessionRecordingPlayerLogic' -import { getCurrentExporterData } from '~/exporter/exporterViewLogic' -import { Logo } from '~/toolbar/assets/Logo' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { percentage } from 'lib/utils' +import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' import { asDisplay } from 'scenes/persons/person-utils' +import { PersonDisplay } from 'scenes/persons/PersonDisplay' +import { IconWindow } from 'scenes/session-recordings/player/icons' +import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' import { urls } from 'scenes/urls' -import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' + +import { getCurrentExporterData } from '~/exporter/exporterViewLogic' +import { Logo } from '~/toolbar/assets/Logo' + +import { PlayerMetaLinks } from './PlayerMetaLinks' +import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from './sessionRecordingPlayerLogic' function SessionPropertyMeta(props: { fullScreen: boolean @@ -99,7 +102,7 @@ export function PlayerMeta(): JSX.Element { const whitelabel = getCurrentExporterData()?.whitelabel ?? false const resolutionView = sessionPlayerMetaDataLoading ? ( - + ) : resolution ? (
    -
    +
    {!sessionPerson || !startTime ? ( - + ) : (
    - + {'·'} @@ -186,7 +189,7 @@ export function PlayerMeta(): JSX.Element {
    {sessionPlayerMetaDataLoading ? ( - + ) : sessionProperties ? ( {sessionPlayerMetaDataLoading ? ( - + ) : ( <> diff --git a/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx b/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx index d3b067a32eac4..059d7c2ca89eb 100644 --- a/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx +++ b/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx @@ -1,6 +1,6 @@ +import { NotFound } from 'lib/components/NotFound' import { Link } from 'lib/lemon-ui/Link' import { urls } from 'scenes/urls' -import { NotFound } from 'lib/components/NotFound' export function RecordingNotFound(): JSX.Element { return ( diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.scss b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.scss index 7d5471d26e345..dadd79e777ec8 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.scss +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.scss @@ -16,7 +16,7 @@ &--no-border { border: none; - border-radius: 0px; + border-radius: 0; } .SessionRecordingPlayer__body { @@ -30,23 +30,21 @@ .SessionRecordingPlayer__main { flex: 1; + padding-right: var(--inspector-peek-width); } &--fullscreen { position: fixed; - left: 0px; - top: 0px; - right: 0px; - bottom: 0px; + inset: 0; overflow-y: auto; background-color: var(--bg-light); z-index: var(--z-modal); border: none; - border-radius: 0px; + border-radius: 0; .SessionRecordingPlayer__body { height: 100%; - margin: 0rem; + margin: 0; border-radius: 0; } } @@ -64,14 +62,9 @@ } } - .SessionRecordingPlayer__main { - padding-right: var(--inspector-peek-width); - } - .SessionRecordingPlayer__inspector { flex-shrink: 0; border-left: 1px solid var(--border); - position: absolute; right: 0; top: 0; @@ -79,27 +72,31 @@ z-index: 10; width: var(--inspector-width); max-width: 95%; - transform: translateX(calc(100% - var(--inspector-peek-width))); transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; - box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 5px rgb(0 0 0 / 10%); .PlayerInspectorPreview { position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; + inset: 0; z-index: 1; pointer-events: none; transition: opacity 0.2s ease-in-out; } + + .LemonButton--tertiary { + .posthog-3000 & { + &:hover { + color: var(--primary-3000); + } + } + } } &--inspector-focus { .SessionRecordingPlayer__inspector { transform: translateX(0); - box-shadow: -10px 0px 20px rgba(0, 0, 0, 0.2); + box-shadow: -10px 0 20px rgb(0 0 0 / 20%); .PlayerInspectorPreview { opacity: 0; @@ -112,7 +109,7 @@ .SessionRecordingPlayer__main { padding-right: 0; } - .SessionRecordingPlayer__inspector, + .SessionRecordingPlayer__inspector { min-width: 30%; width: 40%; @@ -148,10 +145,7 @@ font-size: 6px; font-weight: bold; text-align: center; - left: 0; - right: 0; - top: 0; - bottom: 0; + inset: 0; display: flex; align-items: center; justify-content: center; diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx index 483ba76debca2..092213dd4ea4d 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx @@ -1,6 +1,21 @@ import './SessionRecordingPlayer.scss' -import { useMemo, useRef, useState } from 'react' + +import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' +import { HotkeysInterface, useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' +import { usePageVisibility } from 'lib/hooks/usePageVisibility' +import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { useMemo, useRef, useState } from 'react' +import { PlayerController } from 'scenes/session-recordings/player/controller/PlayerController' +import { PlayerInspector } from 'scenes/session-recordings/player/inspector/PlayerInspector' +import { PlayerFrame } from 'scenes/session-recordings/player/PlayerFrame' +import { RecordingNotFound } from 'scenes/session-recordings/player/RecordingNotFound' +import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' + +import { PlayerFrameOverlay } from './PlayerFrameOverlay' +import { PlayerMeta } from './PlayerMeta' +import { sessionRecordingDataLogic } from './sessionRecordingDataLogic' import { ONE_FRAME_MS, PLAYBACK_SPEEDS, @@ -8,20 +23,7 @@ import { SessionRecordingPlayerLogicProps, SessionRecordingPlayerMode, } from './sessionRecordingPlayerLogic' -import { PlayerFrame } from 'scenes/session-recordings/player/PlayerFrame' -import { PlayerController } from 'scenes/session-recordings/player/controller/PlayerController' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { PlayerInspector } from 'scenes/session-recordings/player/inspector/PlayerInspector' -import { PlayerMeta } from './PlayerMeta' -import { sessionRecordingDataLogic } from './sessionRecordingDataLogic' -import clsx from 'clsx' -import { HotkeysInterface, useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' -import { usePageVisibility } from 'lib/hooks/usePageVisibility' -import { RecordingNotFound } from 'scenes/session-recordings/player/RecordingNotFound' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { PlayerFrameOverlay } from './PlayerFrameOverlay' import { SessionRecordingPlayerExplorer } from './view-explorer/SessionRecordingPlayerExplorer' -import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' export interface SessionRecordingPlayerProps extends SessionRecordingPlayerLogicProps { noMeta?: boolean diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx index 20b75e2795fc9..465fe05a39377 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx @@ -1,19 +1,21 @@ +import clsx from 'clsx' import { useActions, useValues } from 'kea' +import { IconExport, IconFullScreen, IconMagnifier, IconPause, IconPlay, IconSkipInactivity } from 'lib/lemon-ui/icons' +import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { PLAYBACK_SPEEDS, - SessionRecordingPlayerMode, sessionRecordingPlayerLogic, + SessionRecordingPlayerMode, } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { SessionPlayerState } from '~/types' -import { Seekbar } from './Seekbar' -import { SeekSkip } from './PlayerControllerTime' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { IconExport, IconFullScreen, IconMagnifier, IconPause, IconPlay, IconSkipInactivity } from 'lib/lemon-ui/icons' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import clsx from 'clsx' + import { playerSettingsLogic } from '../playerSettingsLogic' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' +import { SeekSkip } from './PlayerControllerTime' +import { Seekbar } from './Seekbar' export function PlayerController(): JSX.Element { const { playingState, logicProps, isFullScreen } = useValues(sessionRecordingPlayerLogic) diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx index 0ff0f9e3db16b..4cb78f98cd348 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx @@ -1,13 +1,14 @@ -import { capitalizeFirstLetter, colonDelimitedDuration } from 'lib/utils' -import { useActions, useValues } from 'kea' -import { ONE_FRAME_MS, sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { seekbarLogic } from './seekbarLogic' +import { TZLabel } from '@posthog/apps-common' import { LemonButton, Tooltip } from '@posthog/lemon-ui' -import { useKeyHeld } from 'lib/hooks/useKeyHeld' -import { IconSkipBackward } from 'lib/lemon-ui/icons' import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { dayjs } from 'lib/dayjs' -import { TZLabel } from '@posthog/apps-common' +import { useKeyHeld } from 'lib/hooks/useKeyHeld' +import { IconSkipBackward } from 'lib/lemon-ui/icons' +import { capitalizeFirstLetter, colonDelimitedDuration } from 'lib/utils' +import { ONE_FRAME_MS, sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' + +import { seekbarLogic } from './seekbarLogic' export function Timestamp(): JSX.Element { const { logicProps, currentPlayerTime, currentTimestamp, sessionPlayerData } = diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx index 44fd8f6763c69..01b857d66bbef 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx @@ -1,14 +1,15 @@ +import { BindLogic, useActions, useValues } from 'kea' +import useIsHovering from 'lib/hooks/useIsHovering' import { colonDelimitedDuration } from 'lib/utils' import { MutableRefObject, useEffect, useRef, useState } from 'react' +import { useDebouncedCallback } from 'use-debounce' + import { PlayerFrame } from '../PlayerFrame' -import { BindLogic, useActions, useValues } from 'kea' import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps, SessionRecordingPlayerMode, } from '../sessionRecordingPlayerLogic' -import { useDebouncedCallback } from 'use-debounce' -import useIsHovering from 'lib/hooks/useIsHovering' export type PlayerSeekbarPreviewProps = { minMs: number diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarTicks.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarTicks.tsx index 6791b33405a02..89820398dba35 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarTicks.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarTicks.tsx @@ -1,7 +1,8 @@ import clsx from 'clsx' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { capitalizeFirstLetter, autoCaptureEventToDescription } from 'lib/utils' +import { autoCaptureEventToDescription, capitalizeFirstLetter } from 'lib/utils' import { memo } from 'react' + import { InspectorListItemEvent } from '../inspector/playerInspectorLogic' function PlayerSeekbarTick(props: { diff --git a/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss b/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss index ca0ef9bd77843..53e2b3f3ca9a0 100644 --- a/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss +++ b/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss @@ -11,17 +11,20 @@ &:hover, &--scrubbing { --bar-height: 8px; + + .PlayerSeekBarPreview { + opacity: 1; + } } .PlayerSeekbar__slider { z-index: 2; - position: relative; height: var(--bar-height); background-color: var(--border-light); border-radius: var(--bar-height); position: absolute; - left: 0px; - right: 0px; + left: 0; + right: 0; top: calc((var(--slider-height) - var(--bar-height)) / 2); transition: height 150ms ease-in-out, top 150ms ease-in-out; cursor: pointer; @@ -29,12 +32,11 @@ .PlayerSeekbar__bufferbar, .PlayerSeekbar__currentbar, .PlayerSeekbar__segments { - width: 100%; position: absolute; height: 100%; - left: 0px; - top: 0px; - width: 0px; + left: 0; + top: 0; + width: 0; } .PlayerSeekbar__bufferbar { @@ -102,19 +104,11 @@ font-weight: 600; color: #fff; background-color: var(--muted-dark); - transform: translateX(-50%); margin-bottom: 0.5rem; } } } - - &:hover, - &--scrubbing { - .PlayerSeekBarPreview { - opacity: 1; - } - } } .PlayerSeekbarTicks { @@ -130,7 +124,7 @@ cursor: pointer; position: absolute; height: 100%; - top: 0px; + top: 0; transition: transform 150ms ease-in-out; &--warning { @@ -142,7 +136,7 @@ } &--primary { - --tick-color: var(--primary); + --tick-color: var(--primary-3000); } .PlayerSeekbarTick__line { @@ -179,6 +173,7 @@ opacity: 1; visibility: visible; } + .PlayerSeekbarTick__line { opacity: 1; } diff --git a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx index 56e771c9531d7..670279f1f6d0c 100644 --- a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx @@ -1,15 +1,18 @@ import './Seekbar.scss' -import { useEffect, useRef } from 'react' -import { useActions, useValues } from 'kea' + import clsx from 'clsx' -import { seekbarLogic } from './seekbarLogic' +import { useActions, useValues } from 'kea' +import { useEffect, useRef } from 'react' + import { RecordingSegment } from '~/types' + +import { playerInspectorLogic } from '../inspector/playerInspectorLogic' import { sessionRecordingDataLogic } from '../sessionRecordingDataLogic' import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import { Timestamp } from './PlayerControllerTime' -import { playerInspectorLogic } from '../inspector/playerInspectorLogic' import { PlayerSeekbarPreview } from './PlayerSeekbarPreview' import { PlayerSeekbarTicks } from './PlayerSeekbarTicks' +import { seekbarLogic } from './seekbarLogic' export function Seekbar(): JSX.Element { const { sessionRecordingId, logicProps } = useValues(sessionRecordingPlayerLogic) diff --git a/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts b/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts index b736e418e4663..15f8dca161329 100644 --- a/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts +++ b/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts @@ -1,13 +1,13 @@ -import { MutableRefObject } from 'react' import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import type { seekbarLogicType } from './seekbarLogicType' +import { clamp } from 'lib/utils' +import { MutableRefObject } from 'react' import { - SessionRecordingPlayerLogicProps, sessionRecordingPlayerLogic, + SessionRecordingPlayerLogicProps, } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { clamp } from 'lib/utils' import { getXPos, InteractEvent, ReactInteractEvent, THUMB_OFFSET, THUMB_SIZE } from '../utils/playerUtils' +import type { seekbarLogicType } from './seekbarLogicType' export const seekbarLogic = kea([ path((key) => ['scenes', 'session-recordings', 'player', 'seekbarLogic', key]), diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx index 345f6eeb0bab4..508498af73630 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx @@ -1,6 +1,7 @@ -import { PlayerInspectorList } from './PlayerInspectorList' -import { PlayerInspectorControls } from './PlayerInspectorControls' import { useEffect, useRef, useState } from 'react' + +import { PlayerInspectorControls } from './PlayerInspectorControls' +import { PlayerInspectorList } from './PlayerInspectorList' import { PlayerInspectorPreview } from './PlayerInspectorPreview' const MOUSE_ENTER_DELAY = 100 diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx index af21f1f660a75..5a3829d1f9678 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx @@ -1,22 +1,24 @@ -import { LemonButton, LemonInput, LemonSelect, LemonCheckbox, Tooltip } from '@posthog/lemon-ui' -import { useValues, useActions } from 'kea' +import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, Tooltip } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { + IconGauge, IconInfo, - IconSchedule, + IconPause, IconPlayCircle, - IconGauge, + IconSchedule, IconTerminal, IconUnverifiedEvent, - IconPause, } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { capitalizeFirstLetter } from 'lib/utils' -import { SessionRecordingPlayerTab } from '~/types' import { IconWindow } from 'scenes/session-recordings/player/icons' + +import { SessionRecordingPlayerTab } from '~/types' + import { playerSettingsLogic } from '../playerSettingsLogic' -import { SessionRecordingPlayerMode, sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' -import { playerInspectorLogic } from './playerInspectorLogic' +import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from '../sessionRecordingPlayerLogic' import { InspectorSearchInfo } from './components/InspectorSearchInfo' +import { playerInspectorLogic } from './playerInspectorLogic' const TabToIcon = { [SessionRecordingPlayerTab.ALL]: undefined, diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss index e9d183d550ee8..b2a7191f56ce2 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.scss @@ -8,7 +8,11 @@ height: 0.5rem; margin-top: 0.25rem; background-color: var(--primary); - border-radius: var(--radius) 0px 0px var(--radius); + 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/inspector/PlayerInspectorList.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.tsx index dfbd7566ef440..dcebd362ee685 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorList.tsx @@ -1,21 +1,24 @@ +import './PlayerInspectorList.scss' + +import { LemonButton, Link } from '@posthog/lemon-ui' +import { range } from 'd3' import { useActions, useValues } from 'kea' +import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { useEffect, useMemo, useRef } from 'react' -import { List, ListRowRenderer } from 'react-virtualized/dist/es/List' -import { CellMeasurer, CellMeasurerCache } from 'react-virtualized/dist/es/CellMeasurer' -import { AvailableFeature, SessionRecordingPlayerTab } from '~/types' -import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' -import { playerInspectorLogic } from './playerInspectorLogic' import AutoSizer from 'react-virtualized/dist/es/AutoSizer' -import { LemonButton, Link } from '@posthog/lemon-ui' -import './PlayerInspectorList.scss' -import { range } from 'd3' +import { CellMeasurer, CellMeasurerCache } from 'react-virtualized/dist/es/CellMeasurer' +import { List, ListRowRenderer } from 'react-virtualized/dist/es/List' import { teamLogic } from 'scenes/teamLogic' -import { playerSettingsLogic } from '../playerSettingsLogic' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { userLogic } from 'scenes/userLogic' -import { PayGatePage } from 'lib/components/PayGatePage/PayGatePage' -import { PlayerInspectorListItem } from './components/PlayerInspectorListItem' + import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' +import { AvailableFeature, SessionRecordingPlayerTab } from '~/types' + +import { playerSettingsLogic } from '../playerSettingsLogic' +import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' +import { PlayerInspectorListItem } from './components/PlayerInspectorListItem' +import { playerInspectorLogic } from './playerInspectorLogic' function isLocalhost(url: string | null | undefined): boolean { return !!url && ['localhost', '127.0.0.1'].includes(new URL(url).hostname) diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorPreview.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorPreview.tsx index 1ed7217a36aa7..97cff5a43bc1f 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorPreview.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorPreview.tsx @@ -1,9 +1,11 @@ +import clsx from 'clsx' import { useValues } from 'kea' -import { IconGauge, IconTerminal, IconUnverifiedEvent, IconMagnifier } from 'lib/lemon-ui/icons' +import { IconGauge, IconMagnifier, IconTerminal, IconUnverifiedEvent } from 'lib/lemon-ui/icons' + import { SessionRecordingPlayerTab } from '~/types' + import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import { playerInspectorLogic } from './playerInspectorLogic' -import clsx from 'clsx' const TabToIcon = { [SessionRecordingPlayerTab.ALL]: IconMagnifier, diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemConsoleLog.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemConsoleLog.tsx index 7a1c3cec6bbe3..878df0624374d 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemConsoleLog.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemConsoleLog.tsx @@ -1,6 +1,7 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' + import { InspectorListItemConsole } from '../playerInspectorLogic' export interface ItemConsoleLogProps { diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx index 119951b6a6160..3ab3f926a967c 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx @@ -1,11 +1,13 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' -import { IconOpenInNew } from 'lib/lemon-ui/icons' +import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { capitalizeFirstLetter, autoCaptureEventToDescription, insightUrlForEvent } from 'lib/utils' +import { IconOpenInNew } from 'lib/lemon-ui/icons' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { autoCaptureEventToDescription, capitalizeFirstLetter } from 'lib/utils' +import { insightUrlForEvent } from 'scenes/insights/utils' + import { InspectorListItemEvent } from '../playerInspectorLogic' import { SimpleKeyValueList } from './SimpleKeyValueList' -import { Spinner } from 'lib/lemon-ui/Spinner' -import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' export interface ItemEventProps { item: InspectorListItemEvent diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx index 4dbd3cd4b3f2c..73fac6683aa02 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -1,16 +1,18 @@ import { LemonButton, LemonDivider, LemonTabs, LemonTag, LemonTagType, Link } from '@posthog/lemon-ui' import clsx from 'clsx' -import { dayjs, Dayjs } from 'lib/dayjs' -import { humanizeBytes, humanFriendlyMilliseconds, isURL } from 'lib/utils' -import { Body, PerformanceEvent } from '~/types' -import { SimpleKeyValueList } from './SimpleKeyValueList' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Fragment, useState } from 'react' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' +import { Dayjs, dayjs } from 'lib/dayjs' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { humanFriendlyMilliseconds, humanizeBytes, isURL } from 'lib/utils' +import { Fragment, useState } from 'react' import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' +import { Body, PerformanceEvent } from '~/types' + +import { SimpleKeyValueList } from './SimpleKeyValueList' + const friendlyHttpStatus = { '0': 'Request not sent', '200': 'OK', @@ -140,7 +142,7 @@ export function ItemPerformanceEvent({ expanded, setExpanded, }: ItemPerformanceEvent): JSX.Element { - const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body'>('timings') + const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body' | 'raw'>('timings') const bytes = humanizeBytes(item.encoded_body_size || item.decoded_body_size || 0) const startTime = item.start_time || item.fetch_start || 0 @@ -176,7 +178,11 @@ export function ItemPerformanceEvent({ return acc } - if (['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status'].includes(key)) { + if ( + ['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status', 'raw'].includes( + key + ) + ) { return acc } @@ -317,7 +323,7 @@ export function ItemPerformanceEvent({ ) : ( <> - +

    Request started at{' '} @@ -392,6 +398,17 @@ export function ItemPerformanceEvent({ ), } : false, + // raw is only available if the feature flag is enabled + // TODO before proper release we should put raw behind its own flag + { + key: 'raw', + label: 'Json', + content: ( + + {JSON.stringify(item.raw, null, 2)} + + ), + }, ]} /> @@ -466,29 +483,52 @@ function HeadersDisplay({ ) } -function StatusRow({ status }: { status: number | undefined }): JSX.Element | null { - if (status === undefined) { - return null +function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null { + let statusRow = null + let methodRow = null + + let fromDiskCache = false + if (item.transfer_size === 0 && item.response_body && item.response_status && item.response_status < 400) { + fromDiskCache = true } - const statusDescription = `${status} ${friendlyHttpStatus[status] || ''}` + if (item.response_status) { + const statusDescription = `${item.response_status} ${friendlyHttpStatus[item.response_status] || ''}` + + let statusType: LemonTagType = 'success' + if (item.response_status >= 400 || item.response_status < 100) { + statusType = 'warning' + } else if (item.response_status >= 500) { + statusType = 'danger' + } - let statusType: LemonTagType = 'success' - if (status >= 400 || status < 100) { - statusType = 'warning' - } else if (status >= 500) { - statusType = 'danger' + statusRow = ( +

    +
    Status code
    +
    + {statusDescription} + {fromDiskCache && (from cache)} +
    +
    + ) } - return ( + if (item.method) { + methodRow = ( +
    +
    Request method
    +
    {item.method}
    +
    + ) + } + + return methodRow || statusRow ? (

    -
    - Status code - {statusDescription} -
    + {methodRow} + {statusRow}

    - ) + ) : null } diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/PlayerInspectorListItem.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/PlayerInspectorListItem.tsx index 9478a2ceca19a..892debba08ae9 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/PlayerInspectorListItem.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/PlayerInspectorListItem.tsx @@ -1,12 +1,16 @@ import { TZLabel } from '@posthog/apps-common' -import { LemonDivider, LemonButton } from '@posthog/lemon-ui' +import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import clsx from 'clsx' -import { useValues, useActions } from 'kea' +import { useActions, useValues } from 'kea' +import { IconGauge, IconTerminal, IconUnverifiedEvent } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { ceilMsToClosestSecond, colonDelimitedDuration } from 'lib/utils' import { useEffect } from 'react' import { useDebouncedCallback } from 'use-debounce' import useResizeObserver from 'use-resize-observer' + import { SessionRecordingPlayerTab } from '~/types' + import { IconWindow } from '../../icons' import { playerSettingsLogic } from '../../playerSettingsLogic' import { sessionRecordingPlayerLogic } from '../../sessionRecordingPlayerLogic' @@ -14,8 +18,6 @@ import { InspectorListItem, playerInspectorLogic } from '../playerInspectorLogic import { ItemConsoleLog } from './ItemConsoleLog' import { ItemEvent } from './ItemEvent' import { ItemPerformanceEvent } from './ItemPerformanceEvent' -import { IconGauge, IconTerminal, IconUnverifiedEvent } from 'lib/lemon-ui/icons' -import { Tooltip } from 'lib/lemon-ui/Tooltip' const typeToIconAndDescription = { [SessionRecordingPlayerTab.ALL]: { diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx index a02e9bf3dce03..d2511521d3b7e 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx @@ -1,8 +1,9 @@ -import { mswDecorator } from '~/mocks/browser' import { Meta } from '@storybook/react' -import { PerformanceEvent } from '~/types' import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' +import { mswDecorator } from '~/mocks/browser' +import { PerformanceEvent } from '~/types' + const meta: Meta = { title: 'Components/NetworkRequestTiming', component: NetworkRequestTiming, @@ -19,27 +20,30 @@ export function Basic(): JSX.Element { = { 'dns lookup': 'The time taken to complete any DNS lookup for the resource.', 'connection time': 'The time taken to establish a connection to the server to retrieve the resource.', 'tls time': 'The time taken for the SSL/TLS handshake.', - 'waiting for first byte (TTFB)': 'The time taken waiting for the server to start returning a response.', + 'request queuing time': "The time taken waiting in the browser's task queue once ready to make a request.", + 'waiting for first byte': + 'The time taken waiting for the server to start returning a response. Also known as TTFB or time to first byte.', 'receiving response': 'The time taken to receive the response from the server.', 'document processing': 'The time taken to process the document after the response from the server has been received.', @@ -71,29 +45,38 @@ const perfDescriptions: Record<(typeof perfSections)[number], string> = { function colorForSection(section: (typeof perfSections)[number]): string { switch (section) { case 'redirect': - return getSeriesColor(1) - case 'app cache': return getSeriesColor(2) - case 'dns lookup': + case 'app cache': return getSeriesColor(3) - case 'connection time': + case 'dns lookup': return getSeriesColor(4) + case 'connection time': + return getSeriesColor(5) case 'tls time': return getSeriesColor(6) - case 'waiting for first byte (TTFB)': + case 'request queuing time': return getSeriesColor(7) - case 'receiving response': + case 'waiting for first byte': return getSeriesColor(8) - case 'document processing': + case 'receiving response': return getSeriesColor(9) - default: + case 'document processing': return getSeriesColor(10) + default: + return getSeriesColor(11) } } +type PerformanceMeasures = Record + /** * There are defined sections to performance measurement. We may have data for some or all of them * + * + * 0) Queueing + * - from start_time + * - until the first item with activity + * * 1) Redirect * - from startTime which would also be redirectStart * - until redirect_end @@ -127,81 +110,115 @@ function colorForSection(section: (typeof perfSections)[number]): string { * - until load_event_end * * see https://nicj.net/resourcetiming-in-practice/ - * - * @param perfEntry - * @param maxTime */ -function calculatePerformanceParts(perfEntry: PerformanceEvent): Record { +export function calculatePerformanceParts(perfEntry: PerformanceEvent): PerformanceMeasures { const performanceParts: Record = {} - if (perfEntry.redirect_start && perfEntry.redirect_end) { - performanceParts['redirect'] = { - start: perfEntry.redirect_start, - end: perfEntry.redirect_end, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.redirect_start) && isPresent(perfEntry.redirect_end)) { + if (perfEntry.redirect_end - perfEntry.redirect_start > 0) { + performanceParts['redirect'] = { + start: perfEntry.redirect_start, + end: perfEntry.redirect_end, + color: colorForSection('redirect'), + } } } - if (perfEntry.fetch_start && perfEntry.domain_lookup_start) { - performanceParts['app cache'] = { - start: perfEntry.fetch_start, - end: perfEntry.domain_lookup_start, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.fetch_start) && isPresent(perfEntry.domain_lookup_start)) { + if (perfEntry.domain_lookup_start - perfEntry.fetch_start > 0) { + performanceParts['app cache'] = { + start: perfEntry.fetch_start, + end: perfEntry.domain_lookup_start, + color: colorForSection('app cache'), + } } } - if (perfEntry.domain_lookup_end && perfEntry.domain_lookup_start) { - performanceParts['dns lookup'] = { - start: perfEntry.domain_lookup_start, - end: perfEntry.domain_lookup_end, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.domain_lookup_end) && isPresent(perfEntry.domain_lookup_start)) { + if (perfEntry.domain_lookup_end - perfEntry.domain_lookup_start > 0) { + performanceParts['dns lookup'] = { + start: perfEntry.domain_lookup_start, + end: perfEntry.domain_lookup_end, + color: colorForSection('dns lookup'), + } } } - if (perfEntry.connect_end && perfEntry.connect_start) { - performanceParts['connection time'] = { - start: perfEntry.connect_start, - end: perfEntry.connect_end, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.connect_end) && isPresent(perfEntry.connect_start)) { + if (perfEntry.connect_end - perfEntry.connect_start > 0) { + performanceParts['connection time'] = { + start: perfEntry.connect_start, + end: perfEntry.connect_end, + color: colorForSection('connection time'), + } + + if (isPresent(perfEntry.secure_connection_start) && perfEntry.secure_connection_start > 0) { + performanceParts['tls time'] = { + start: perfEntry.secure_connection_start, + end: perfEntry.connect_end, + color: colorForSection('tls time'), + reducedHeight: true, + } + } } + } - if (perfEntry.secure_connection_start) { - performanceParts['tls time'] = { - start: perfEntry.secure_connection_start, - end: perfEntry.connect_end, - color: colorForEntry(perfEntry.initiator_type), - reducedHeight: true, + if ( + isPresent(perfEntry.connect_end) && + isPresent(perfEntry.request_start) && + perfEntry.connect_end !== perfEntry.request_start + ) { + if (perfEntry.request_start - perfEntry.connect_end > 0) { + performanceParts['request queuing time'] = { + start: perfEntry.connect_end, + end: perfEntry.request_start, + color: colorForSection('request queuing time'), } } } - if (perfEntry.response_start && perfEntry.request_start) { - performanceParts['waiting for first byte (TTFB)'] = { - start: perfEntry.request_start, - end: perfEntry.response_start, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.response_start) && isPresent(perfEntry.request_start)) { + if (perfEntry.response_start - perfEntry.request_start > 0) { + performanceParts['waiting for first byte'] = { + start: perfEntry.request_start, + end: perfEntry.response_start, + color: colorForSection('waiting for first byte'), + } } } - if (perfEntry.response_start && perfEntry.response_end) { - performanceParts['receiving response'] = { - start: perfEntry.response_start, - end: perfEntry.response_end, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.response_start) && isPresent(perfEntry.response_end)) { + if (perfEntry.response_end - perfEntry.response_start > 0) { + // if loading from disk cache then response_start is 0 but fetch_start is not + let start = perfEntry.response_start + if (perfEntry.response_start === 0 && isPresent(perfEntry.fetch_start)) { + start = perfEntry.fetch_start + } + performanceParts['receiving response'] = { + start: start, + end: perfEntry.response_end, + color: colorForSection('receiving response'), + } } } - if (perfEntry.response_end && perfEntry.load_event_end) { - performanceParts['document processing'] = { - start: perfEntry.response_end, - end: perfEntry.load_event_end, - color: colorForEntry(perfEntry.initiator_type), + if (isPresent(perfEntry.response_end) && isPresent(perfEntry.load_event_end)) { + if (perfEntry.load_event_end - perfEntry.response_end > 0) { + performanceParts['document processing'] = { + start: perfEntry.response_end, + end: perfEntry.load_event_end, + color: colorForSection('document processing'), + } } } return performanceParts } +function percentage(partDuration: number, totalDuration: number, min: number): number { + return Math.min(Math.max(min, (partDuration / totalDuration) * 100), 100) +} + function percentagesWithinEventRange({ partStart, partEnd, @@ -217,20 +234,20 @@ function percentagesWithinEventRange({ const partStartRelativeToTimeline = partStart - rangeStart const partDuration = partEnd - partStart - const partPercentage = (partDuration / totalDuration) * 100 - const partStartPercentage = (partStartRelativeToTimeline / totalDuration) * 100 + const partPercentage = percentage(partDuration, totalDuration, 0.1) + const partStartPercentage = percentage(partStartRelativeToTimeline, totalDuration, 0) return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` } } -const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => { +const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element | null => { const rangeStart = performanceEvent.start_time - const rangeEnd = performanceEvent.response_end + const rangeEnd = performanceEvent.load_event_end ? performanceEvent.load_event_end : performanceEvent.response_end if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') { - const performanceParts = calculatePerformanceParts(performanceEvent) + const timings = calculatePerformanceParts(performanceEvent) return (
    {perfSections.map((section) => { - const matchedSection = performanceParts[section] + const matchedSection = timings[section] const start = matchedSection?.start const end = matchedSection?.end const partDuration = end - start @@ -277,7 +294,7 @@ const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent
    ) } - return Cannot render performance timeline for this request + return null } const TableView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => { @@ -297,11 +314,15 @@ export const NetworkRequestTiming = ({ }): JSX.Element | null => { const [timelineMode, setTimelineMode] = useState(true) + // if timeline view renders null then we fall back to table view + const timelineView = timelineMode ? : null + return (
    setTimelineMode(!timelineMode)} data-attr={`switch-timing-to-${timelineMode ? 'table' : 'timeline'}-view`} @@ -310,11 +331,11 @@ export const NetworkRequestTiming = ({
    - {timelineMode ? ( - - ) : ( - - )} + {timelineMode && timelineView ? timelineView : }
    ) } + +function isPresent(x: number | undefined): x is number { + return typeof x === 'number' +} diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts new file mode 100644 index 0000000000000..ceb129e8d8e9a --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts @@ -0,0 +1,192 @@ +import { InitiatorType } from 'posthog-js' +import { calculatePerformanceParts } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' +import { mapRRWebNetworkRequest } from 'scenes/session-recordings/player/inspector/performance-event-utils' + +jest.mock('lib/colors', () => { + return { + getSeriesColor: jest.fn(() => '#000000'), + } +}) + +describe('calculatePerformanceParts', () => { + it('can calculate TTFB', () => { + const perfEvent = { + connect_end: 9525.599999964237, + connect_start: 9525.599999964237, + decoded_body_size: 18260, + domain_lookup_end: 9525.599999964237, + domain_lookup_start: 9525.599999964237, + duration: 935.5, + encoded_body_size: 18260, + entry_type: 'resource', + fetch_start: 9525.599999964237, + initiator_type: 'fetch', + name: 'http://localhost:8000/api/organizations/@current/plugins/repository/', + next_hop_protocol: 'http/1.1', + redirect_end: 0, + redirect_start: 0, + render_blocking_status: 'non-blocking', + request_start: 9803.099999964237, + response_end: 10461.099999964237, + response_start: 10428.399999976158, + response_status: 200, + secure_connection_start: 0, + start_time: 9525.599999964237, + time_origin: '1699990397357', + timestamp: 1699990406882, + transfer_size: 18560, + window_id: '018bcf51-b1f0-7fe0-ac05-10543621f4f2', + worker_start: 0, + uuid: '12345', + distinct_id: '23456', + session_id: 'abcde', + pageview_id: 'fghij', + current_url: 'http://localhost:8000/insights', + } + + expect(calculatePerformanceParts(perfEvent)).toEqual({ + 'request queuing time': { + color: '#000000', + end: 9803.099999964237, + start: 9525.599999964237, + }, + + 'waiting for first byte': { + color: '#000000', + end: 10428.399999976158, + start: 9803.099999964237, + }, + 'receiving response': { + color: '#000000', + end: 10461.099999964237, + start: 10428.399999976158, + }, + }) + }) + + it('can handle gravatar timings', () => { + const gravatarReqRes = { + name: 'https://www.gravatar.com/avatar/2e7d95b60efbe947f71009a1af1ba8d0?s=96&d=404', + entryType: 'resource', + initiatorType: 'fetch' as InitiatorType, + deliveryType: '', + nextHopProtocol: '', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + domainLookupStart: 0, + domainLookupEnd: 0, + connectStart: 0, + secureConnectionStart: 0, + connectEnd: 0, + requestStart: 0, + responseStart: 0, + firstInterimResponseStart: 0, + // only fetch start and response end + // and transfer size is 0 + // loaded from disk cache + startTime: 18229, + fetchStart: 18228.5, + responseEnd: 18267.5, + endTime: 18268, + duration: 39, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + responseStatus: 200, + serverTiming: [], + timeOrigin: 1700296048424, + timestamp: 1700296066652, + method: 'GET', + status: 200, + requestHeaders: {}, + requestBody: null, + responseHeaders: { + 'cache-control': 'max-age=300', + 'content-length': '13127', + 'content-type': 'image/png', + expires: 'Sat, 18 Nov 2023 08:32:46 GMT', + 'last-modified': 'Wed, 02 Feb 2022 09:11:05 GMT', + }, + responseBody: '�PNGblah', + } + const mappedToPerfEvent = mapRRWebNetworkRequest(gravatarReqRes, 'windowId', 1700296066652) + expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + // 'app cache' not included - end would be before beginning + // 'connection time' has 0 length + // 'dns lookup' has 0 length + // 'redirect has 0 length + // 'tls time' has 0 length + // TTFB has 0 length + 'receiving response': { + color: '#000000', + end: 18267.5, + start: 18228.5, + }, + }) + }) + + it('can handle no TLS connection timing', () => { + const tlsFreeReqRes = { + name: 'http://localhost:8000/decide/?v=3&ip=1&_=1700319068450&ver=1.91.1', + entryType: 'resource', + startTime: 6648, + duration: 93.40000003576279, + initiatorType: 'xmlhttprequest' as InitiatorType, + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 6647.699999988079, + domainLookupStart: 6648.800000011921, + domainLookupEnd: 6648.800000011921, + connectStart: 6648.800000011921, + secureConnectionStart: 0, + connectEnd: 6649.300000011921, + requestStart: 6649.5, + responseStart: 6740.800000011921, + firstInterimResponseStart: 0, + responseEnd: 6741.100000023842, + transferSize: 2383, + encodedBodySize: 2083, + decodedBodySize: 2083, + responseStatus: 200, + serverTiming: [], + endTime: 6741, + timeOrigin: 1700319061802, + timestamp: 1700319068449, + isInitial: true, + } + const mappedToPerfEvent = mapRRWebNetworkRequest(tlsFreeReqRes, 'windowId', 1700319068449) + expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + 'app cache': { + color: '#000000', + end: 6648.800000011921, + start: 6647.699999988079, + }, + 'connection time': { + color: '#000000', + end: 6649.300000011921, + start: 6648.800000011921, + }, + 'waiting for first byte': { + color: '#000000', + end: 6740.800000011921, + start: 6649.5, + }, + 'receiving response': { + color: '#000000', + end: 6741.100000023842, + start: 6740.800000011921, + }, + 'request queuing time': { + color: '#000000', + end: 6649.5, + start: 6649.300000011921, + }, + }) + }) +}) diff --git a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts index fe77868977a11..502b85434f9e2 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts @@ -1,10 +1,10 @@ import { eventWithTime } from '@rrweb/types' -import posthog from 'posthog-js' +import { CapturedNetworkRequest } from 'posthog-js' + import { PerformanceEvent } from '~/types' const NETWORK_PLUGIN_NAME = 'posthog/network@1' const RRWEB_NETWORK_PLUGIN_NAME = 'rrweb/network@1' -const IGNORED_POSTHOG_PATHS = ['/s/', '/e/', '/i/v0/e/'] export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceEvent } = { // BASE_PERFORMANCE_EVENT_COLUMNS @@ -58,8 +58,97 @@ export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceE 40: 'timestamp', } +export const RRWebPerformanceEventReverseMapping: Record = { + // BASE_PERFORMANCE_EVENT_COLUMNS + entryType: 'entry_type', + timeOrigin: 'time_origin', + name: 'name', + + // RESOURCE_EVENT_COLUMNS + startTime: 'start_time', + redirectStart: 'redirect_start', + redirectEnd: 'redirect_end', + workerStart: 'worker_start', + fetchStart: 'fetch_start', + domainLookupStart: 'domain_lookup_start', + domainLookupEnd: 'domain_lookup_end', + connectStart: 'connect_start', + secureConnectionStart: 'secure_connection_start', + connectEnd: 'connect_end', + requestStart: 'request_start', + responseStart: 'response_start', + responseEnd: 'response_end', + decodedBodySize: 'decoded_body_size', + encodedBodySize: 'encoded_body_size', + initiatorType: 'initiator_type', + nextHopProtocol: 'next_hop_protocol', + renderBlockingStatus: 'render_blocking_status', + responseStatus: 'response_status', + transferSize: 'transfer_size', + + // LARGEST_CONTENTFUL_PAINT_EVENT_COLUMNS + largestContentfulPaintElement: 'largest_contentful_paint_element', + largestContentfulPaintRenderTime: 'largest_contentful_paint_render_time', + largestContentfulPaintLoadTime: 'largest_contentful_paint_load_time', + largestContentfulPaintSize: 'largest_contentful_paint_size', + largestContentfulPaintId: 'largest_contentful_paint_id', + largestContentfulPaintUrl: 'largest_contentful_paint_url', + + // NAVIGATION_EVENT_COLUMNS + domComplete: 'dom_complete', + domContentLoadedEvent: 'dom_content_loaded_event', + domInteractive: 'dom_interactive', + loadEventEnd: 'load_event_end', + loadEventStart: 'load_event_start', + redirectCount: 'redirect_count', + navigationType: 'navigation_type', + unloadEventEnd: 'unload_event_end', + unloadEventStart: 'unload_event_start', + + // Added after v1 + duration: 'duration', + timestamp: 'timestamp', + + //rrweb/network@1 + isInitial: 'is_initial', + requestHeaders: 'request_headers', + responseHeaders: 'response_headers', + requestBody: 'request_body', + responseBody: 'response_body', + method: 'method', +} + +export function mapRRWebNetworkRequest( + capturedRequest: CapturedNetworkRequest, + windowId: string, + timestamp: PerformanceEvent['timestamp'] +): PerformanceEvent { + const data: Partial = { + timestamp: timestamp, + window_id: windowId, + raw: capturedRequest, + } + + Object.entries(RRWebPerformanceEventReverseMapping).forEach(([key, value]) => { + if (key in capturedRequest) { + data[value] = capturedRequest[key] + } + }) + + // KLUDGE: this shouldn't be necessary but let's display correctly while we figure out why it is. + if (!data.name && 'url' in capturedRequest) { + data.name = capturedRequest.url as string | undefined + } + + return data as PerformanceEvent +} + export function matchNetworkEvents(snapshotsByWindowId: Record): PerformanceEvent[] { - const eventsMapping: Record> = {} + // we only support rrweb/network@1 events or posthog/network@1 events in any one recording + // apart from during testing, where we might have both + // if we have both, we only display posthog/network@1 events + const events: PerformanceEvent[] = [] + const rrwebEvents: PerformanceEvent[] = [] // we could do this in one pass, but it's easier to log missing events // when we have all the posthog/network@1 events first @@ -83,91 +172,27 @@ export function matchNetworkEvents(snapshotsByWindowId: Record { - const snapshots = snapshotsByWindowId[1] - snapshots.forEach((snapshot: eventWithTime) => { if ( snapshot.type === 6 && // RRWeb plugin event type snapshot.data.plugin === RRWEB_NETWORK_PLUGIN_NAME ) { const payload = snapshot.data.payload as any + if (!Array.isArray(payload.requests) || payload.requests.length === 0) { return } payload.requests.forEach((capturedRequest: any) => { - const matchedURL = eventsMapping[capturedRequest.url] - - const matchedStartTime = matchedURL ? matchedURL[capturedRequest.startTime] : null - - if (matchedStartTime && matchedStartTime.length === 1) { - matchedStartTime[0].response_status = capturedRequest.status - matchedStartTime[0].request_headers = capturedRequest.requestHeaders - matchedStartTime[0].request_body = capturedRequest.requestBody - matchedStartTime[0].response_headers = capturedRequest.responseHeaders - matchedStartTime[0].response_body = capturedRequest.responseBody - } else if (matchedStartTime && matchedStartTime.length > 1) { - // find in eventsMapping[capturedRequest.url][capturedRequest.startTime] by matching capturedRequest.endTime and element.response_end - const matchedEndTime = matchedStartTime.find( - (x) => - typeof x.response_end === 'number' && - Math.round(x.response_end) === capturedRequest.endTime - ) - if (matchedEndTime) { - matchedEndTime.response_status = capturedRequest.status - matchedEndTime.request_headers = capturedRequest.requestHeaders - matchedEndTime.request_body = capturedRequest.requestBody - matchedEndTime.response_headers = capturedRequest.responseHeaders - matchedEndTime.response_body = capturedRequest.responseBody - } else { - const capturedURL = new URL(capturedRequest.url) - const capturedPath = capturedURL.pathname - - if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) { - posthog.capture('Had matches but still could not match rrweb/network@1 event', { - rrwebNetworkEvent: payload, - possibleMatches: matchedStartTime, - totalMatchedURLs: Object.keys(eventsMapping).length, - }) - } - } - } else { - const capturedURL = new URL(capturedRequest.url) - const capturedPath = capturedURL.pathname - if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) { - posthog.capture('Could not match rrweb/network@1 event', { - rrwebNetworkEvent: payload, - possibleMatches: eventsMapping[capturedRequest.url], - totalMatchedURLs: Object.keys(eventsMapping).length, - }) - } - } + const data: PerformanceEvent = mapRRWebNetworkRequest(capturedRequest, windowId, snapshot.timestamp) + + rrwebEvents.push(data) }) } }) }) - // now flatten the eventsMapping into a single array - return Object.values(eventsMapping).reduce((acc: PerformanceEvent[], eventsByURL) => { - Object.values(eventsByURL).forEach((eventsByTime) => { - acc.push(...eventsByTime) - }) - return acc - }, []) + return events.length ? events : rrwebEvents } diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts index c74b2b8f5149f..236cc3b5b8dc5 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts @@ -1,7 +1,8 @@ -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' -import { playerInspectorLogic } from 'scenes/session-recordings/player/inspector/playerInspectorLogic' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { playerInspectorLogic } from 'scenes/session-recordings/player/inspector/playerInspectorLogic' + +import { initKeaTests } from '~/test/init' const playerLogicProps = { sessionRecordingId: '1', playerKey: 'playlist' } diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts index 8c72b8e07afaa..2f0c0533b9785 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts @@ -1,4 +1,16 @@ +import { eventWithTime } from '@rrweb/types' +import FuseClass from 'fuse.js' import { actions, connect, events, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { Dayjs, dayjs } from 'lib/dayjs' +import { getKeyMapping } from 'lib/taxonomy' +import { eventToDescription, objectsEqual, toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { matchNetworkEvents } from 'scenes/session-recordings/player/inspector/performance-event-utils' +import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' +import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' + import { MatchedRecordingEvent, PerformanceEvent, @@ -7,20 +19,10 @@ import { RRWebRecordingConsoleLogPayload, SessionRecordingPlayerTab, } from '~/types' -import type { playerInspectorLogicType } from './playerInspectorLogicType' -import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' -import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' + import { sessionRecordingDataLogic } from '../sessionRecordingDataLogic' -import FuseClass from 'fuse.js' -import { Dayjs, dayjs } from 'lib/dayjs' -import { getKeyMapping } from 'lib/taxonomy' -import { eventToDescription, objectsEqual, toParams } from 'lib/utils' -import { eventWithTime } from '@rrweb/types' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' -import { loaders } from 'kea-loaders' -import api from 'lib/api' -import { matchNetworkEvents } from 'scenes/session-recordings/player/inspector/performance-event-utils' +import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' +import type { playerInspectorLogicType } from './playerInspectorLogicType' const CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1' diff --git a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx index c4300a01c0906..204f5b4674ad8 100644 --- a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx +++ b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx @@ -1,9 +1,10 @@ -import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' -import { BindLogic, useActions, useValues } from 'kea' -import { sessionPlayerModalLogic } from './sessionPlayerModalLogic' import { LemonModal } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' +import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' + import { PlayerMeta } from '../PlayerMeta' -import { SessionRecordingPlayerLogicProps, sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' +import { sessionPlayerModalLogic } from './sessionPlayerModalLogic' export function SessionPlayerModal(): JSX.Element | null { const { activeSessionRecording } = useValues(sessionPlayerModalLogic()) diff --git a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts index 0f3d4220cf518..838f51c78e6f5 100644 --- a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts @@ -1,8 +1,10 @@ import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' + +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' + import { sessionPlayerModalLogic } from './sessionPlayerModalLogic' -import { useMocks } from '~/mocks/jest' describe('sessionPlayerModalLogic', () => { let logic: ReturnType @@ -27,7 +29,7 @@ describe('sessionPlayerModalLogic', () => { it('starts as null', () => { expectLogic(logic).toMatchValues({ activeSessionRecording: null }) }) - it('is set by openSessionPlayer and cleared by closeSessionPlayer', async () => { + it('is set by openSessionPlayer and cleared by closeSessionPlayer', () => { expectLogic(logic, () => logic.actions.openSessionPlayer({ id: 'abc' })) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ diff --git a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.ts b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.ts index 8f9fa214e4de3..6b70ccbd939d3 100644 --- a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.ts +++ b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.ts @@ -1,7 +1,8 @@ import { actions, kea, path, reducers } from 'kea' -import { SessionRecordingId, SessionRecordingType } from '~/types' import { actionToUrl, router, urlToAction } from 'kea-router' +import { SessionRecordingId, SessionRecordingType } from '~/types' + import type { sessionPlayerModalLogicType } from './sessionPlayerModalLogicType' interface HashParams { diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts b/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts index f6c4b38c3f2b7..f21c3f7189f6e 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts @@ -1,13 +1,15 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' -import recordingMetaJson from '../__mocks__/recording_meta.json' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + import recordingEventsJson from '../__mocks__/recording_events_query' +import recordingMetaJson from '../__mocks__/recording_meta.json' import { snapshotsAsJSONLines } from '../__mocks__/recording_snapshots' -import { useMocks } from '~/mocks/jest' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' const playerProps = { sessionRecordingId: '1', playerKey: 'playlist' } diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts index 534b324194276..5486455605458 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts @@ -1,14 +1,16 @@ +import { eventWithTime } from '@rrweb/types' import { connect, kea, key, listeners, path, props, selectors } from 'kea' -import type { playerMetaLogicType } from './playerMetaLogicType' +import { ceilMsToClosestSecond, findLastIndex, objectsEqual } from 'lib/utils' import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' import { - SessionRecordingPlayerLogicProps, sessionRecordingPlayerLogic, + SessionRecordingPlayerLogicProps, } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { eventWithTime } from '@rrweb/types' + import { PersonType } from '~/types' -import { ceilMsToClosestSecond, findLastIndex, objectsEqual } from 'lib/utils' + import { sessionRecordingsListPropertiesLogic } from '../playlist/sessionRecordingsListPropertiesLogic' +import type { playerMetaLogicType } from './playerMetaLogicType' export const playerMetaLogic = kea([ path((key) => ['scenes', 'session-recordings', 'player', 'playerMetaLogic', key]), diff --git a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts index d11218217e817..da212eb7a77bd 100644 --- a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts @@ -1,6 +1,8 @@ import { expectLogic } from 'kea-test-utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' + import { initKeaTests } from '~/test/init' + import { playerSettingsLogic } from './playerSettingsLogic' describe('playerSettingsLogic', () => { @@ -57,7 +59,7 @@ describe('playerSettingsLogic', () => { afterEach(() => { localStorage.clear() }) - it('should start with the first entry selected', async () => { + it('should start with the first entry selected', () => { expect(logic.values.selectedMiniFilters).toEqual([ 'all-automatic', 'console-all', @@ -66,7 +68,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should remove other selected filters if alone', async () => { + it('should remove other selected filters if alone', () => { logic.actions.setMiniFilter('all-errors', true) expect(logic.values.selectedMiniFilters.sort()).toEqual([ @@ -77,7 +79,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should allow multiple filters if not alone', async () => { + it('should allow multiple filters if not alone', () => { logic.actions.setMiniFilter('console-warn', true) logic.actions.setMiniFilter('console-info', true) @@ -90,7 +92,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should reset to first in tab if empty', async () => { + it('should reset to first in tab if empty', () => { expect(logic.values.selectedMiniFilters.sort()).toEqual([ 'all-automatic', 'console-all', diff --git a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts index b8166cc4da64a..1454fa2b9a4b1 100644 --- a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts @@ -1,8 +1,9 @@ import { actions, kea, listeners, path, reducers, selectors } from 'kea' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' + import { AutoplayDirection, DurationType, SessionRecordingPlayerTab } from '~/types' import type { playerSettingsLogicType } from './playerSettingsLogicType' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export type SharedListMiniFilter = { tab: SessionRecordingPlayerTab 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 f862c183bad59..1d19209eabcfc 100644 --- a/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx +++ b/frontend/src/scenes/session-recordings/player/playlist-popover/PlaylistPopover.tsx @@ -1,14 +1,15 @@ import { LemonCheckbox, LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' -import { IconPlus, IconOpenInNew, IconWithCount } from 'lib/lemon-ui/icons' +import { Field } from 'lib/forms/Field' +import { IconOpenInNew, IconPlus, IconWithCount } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { Popover } from 'lib/lemon-ui/Popover' import { Spinner } from 'lib/lemon-ui/Spinner' -import { Field } from 'lib/forms/Field' import { urls } from 'scenes/urls' + import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import { playlistPopoverLogic } from './playlistPopoverLogic' @@ -109,7 +110,7 @@ export function PlaylistPopoverButton(props: LemonButtonProps): JSX.Element { ))}
    ) : playlistsLoading ? ( - + ) : (
    No playlists found
    )} diff --git a/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts b/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts index a5c1fa85f5967..9b26654539c24 100644 --- a/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts @@ -1,18 +1,19 @@ -import { kea, props, path, key, actions, reducers, selectors, listeners, connect, afterMount } from 'kea' +import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import api from 'lib/api' import { toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { - SessionRecordingPlayerLogicProps, sessionRecordingPlayerLogic, + SessionRecordingPlayerLogicProps, } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' - -import type { playlistPopoverLogicType } from './playlistPopoverLogicType' -import { SessionRecordingPlaylistType } from '~/types' -import { forms } from 'kea-forms' import { addRecordingToPlaylist, removeRecordingFromPlaylist } from 'scenes/session-recordings/player/utils/playerUtils' import { createPlaylist } from 'scenes/session-recordings/playlist/playlistUtils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' + +import { SessionRecordingPlaylistType } from '~/types' + +import type { playlistPopoverLogicType } from './playlistPopoverLogicType' export const playlistPopoverLogic = kea([ path((key) => ['scenes', 'session-recordings', 'player', 'playlist-popover', 'playlistPopoverLogic', key]), diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.ts index 72f65e69c9551..3db8982a24aa0 100644 --- a/frontend/src/scenes/session-recordings/player/rrweb/index.ts +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.ts @@ -1,4 +1,4 @@ -import { ReplayPlugin, playerConfig } from 'rrweb/typings/types' +import { playerConfig, ReplayPlugin } from 'rrweb/typings/types' const PROXY_URL = 'https://replay.ph-proxy.com' as const diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts index a98649dc2e8b4..14ad52c0a62c4 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts @@ -1,21 +1,22 @@ +import { expectLogic } from 'kea-test-utils' +import { api, MOCK_TEAM_ID } from 'lib/api.mock' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { + convertSnapshotsByWindowId, prepareRecordingSnapshots, sessionRecordingDataLogic, - convertSnapshotsByWindowId, } from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { api, MOCK_TEAM_ID } from 'lib/api.mock' -import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import recordingMetaJson from '../__mocks__/recording_meta.json' -import recordingEventsJson from '../__mocks__/recording_events_query' -import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' -import { useMocks } from '~/mocks/jest' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' -import { AvailableFeature } from '~/types' + +import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' import { useAvailableFeatures } from '~/mocks/features' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { AvailableFeature } from '~/types' +import recordingEventsJson from '../__mocks__/recording_events_query' +import recordingMetaJson from '../__mocks__/recording_meta.json' import { snapshotsAsJSONLines, sortedRecordingSnapshots } from '../__mocks__/recording_snapshots' const sortedRecordingSnapshotsJson = sortedRecordingSnapshots() @@ -23,7 +24,7 @@ const sortedRecordingSnapshotsJson = sortedRecordingSnapshots() describe('sessionRecordingDataLogic', () => { let logic: ReturnType - beforeEach(async () => { + beforeEach(() => { useAvailableFeatures([AvailableFeature.RECORDINGS_PERFORMANCE]) useMocks({ get: { @@ -66,7 +67,7 @@ describe('sessionRecordingDataLogic', () => { it('mounts other logics', async () => { await expectLogic(logic).toMount([eventUsageLogic, teamLogic, userLogic]) }) - it('has default values', async () => { + it('has default values', () => { expect(logic.values).toMatchObject({ bufferedToTime: null, durationMs: 0, diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index d9028d9034c1c..623a902ae428e 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -1,7 +1,15 @@ +import { EventType, eventWithTime } from '@rrweb/types' +import { captureException } from '@sentry/react' import { actions, connect, defaults, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' +import { Dayjs, dayjs } from 'lib/dayjs' import { toParams } from 'lib/utils' +import { chainToElements } from 'lib/utils/elements-chain' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import posthog from 'posthog-js' + +import { NodeKind } from '~/queries/schema' import { AnyPropertyFilter, EncodedRecordingSnapshot, @@ -20,15 +28,9 @@ import { SessionRecordingType, SessionRecordingUsageType, } from '~/types' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { EventType, eventWithTime } from '@rrweb/types' -import { Dayjs, dayjs } from 'lib/dayjs' + import type { sessionRecordingDataLogicType } from './sessionRecordingDataLogicType' -import { chainToElements } from 'lib/utils/elements-chain' -import { captureException } from '@sentry/react' import { createSegments, mapSnapshotsToWindowId } from './utils/segmenter' -import posthog from 'posthog-js' -import { NodeKind } from '~/queries/schema' const IS_TEST_MODE = process.env.NODE_ENV === 'test' const BUFFER_MS = 60000 // +- before and after start and end of a recording to query for. @@ -265,7 +267,7 @@ export const sessionRecordingDataLogic = kea([ reportViewed: async (_, breakpoint) => { const durations = generateRecordingReportDurations(cache, values) - await breakpoint() + breakpoint() // Triggered on first paint eventUsageLogic.actions.reportRecording( values.sessionPlayerData, diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts index 8b4526d450338..bd985c6da05d5 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.test.ts @@ -1,21 +1,22 @@ -import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' -import { initKeaTests } from '~/test/init' +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' -import { useMocks } from '~/mocks/jest' -import { snapshotsAsJSONLines } from 'scenes/session-recordings/__mocks__/recording_snapshots' -import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' -import recordingEventsJson from 'scenes/session-recordings/__mocks__/recording_events_query' -import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import api from 'lib/api' import { MOCK_TEAM_ID } from 'lib/api.mock' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import recordingEventsJson from 'scenes/session-recordings/__mocks__/recording_events_query' +import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' +import { snapshotsAsJSONLines } from 'scenes/session-recordings/__mocks__/recording_snapshots' +import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' +import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' +import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + describe('sessionRecordingPlayerLogic', () => { let logic: ReturnType const mockWarn = jest.fn() diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index b510c46732e3f..5d865c9dda4c8 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -1,8 +1,9 @@ +import { lemonToast } from '@posthog/lemon-ui' import { - BuiltLogic, actions, afterMount, beforeUnmount, + BuiltLogic, connect, kea, key, @@ -12,38 +13,39 @@ import { reducers, selectors, } from 'kea' +import { router } from 'kea-router' +import { delay } from 'kea-test-utils' import { windowValues } from 'kea-window-values' -import type { sessionRecordingPlayerLogicType } from './sessionRecordingPlayerLogicType' -import { Replayer } from 'rrweb' +import { FEATURE_FLAGS } from 'lib/constants' +import { now } from 'lib/dayjs' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { clamp, downloadFile, fromParamsGivenUrl } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { AvailableFeature, RecordingSegment, SessionPlayerData, SessionPlayerState } from '~/types' import { getBreakpoint } from 'lib/utils/responsiveUtils' +import { wrapConsole } from 'lib/utils/wrapConsole' +import posthog from 'posthog-js' +import { RefObject } from 'react' +import { Replayer } from 'rrweb' +import { ReplayPlugin } from 'rrweb/typings/types' +import { openBillingPopupModal } from 'scenes/billing/BillingPopup' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { - SessionRecordingDataLogicProps, sessionRecordingDataLogic, + SessionRecordingDataLogicProps, } from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { deleteRecording } from './utils/playerUtils' -import { playerSettingsLogic } from './playerSettingsLogic' -import { clamp, downloadFile, fromParamsGivenUrl } from 'lib/utils' -import { lemonToast } from '@posthog/lemon-ui' -import { delay } from 'kea-test-utils' -import { userLogic } from 'scenes/userLogic' -import { openBillingPopupModal } from 'scenes/billing/BillingPopup' import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' -import { router } from 'kea-router' import { urls } from 'scenes/urls' -import { wrapConsole } from 'lib/utils/wrapConsole' -import { SessionRecordingPlayerExplorerProps } from './view-explorer/SessionRecordingPlayerExplorer' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature, RecordingSegment, SessionPlayerData, SessionPlayerState } from '~/types' + import { createExportedSessionRecording } from '../file-playback/sessionRecordingFilePlaybackLogic' -import { RefObject } from 'react' -import posthog from 'posthog-js' -import { COMMON_REPLAYER_CONFIG, CorsPlugin } from './rrweb' -import { now } from 'lib/dayjs' -import { ReplayPlugin } from 'rrweb/typings/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' import type { sessionRecordingsPlaylistLogicType } from '../playlist/sessionRecordingsPlaylistLogicType' +import { playerSettingsLogic } from './playerSettingsLogic' +import { COMMON_REPLAYER_CONFIG, CorsPlugin } from './rrweb' +import type { sessionRecordingPlayerLogicType } from './sessionRecordingPlayerLogicType' +import { deleteRecording } from './utils/playerUtils' +import { SessionRecordingPlayerExplorerProps } from './view-explorer/SessionRecordingPlayerExplorer' export const PLAYBACK_SPEEDS = [0.5, 1, 2, 3, 4, 8, 16] export const ONE_FRAME_MS = 100 // We don't really have frames but this feels granular enough @@ -605,7 +607,7 @@ export const sessionRecordingPlayerLogic = kea( // If replayer isn't initialized, it will be initialized with the already loaded snapshots if (values.player?.replayer) { for (const event of eventsToAdd) { - await values.player?.replayer?.addEvent(event) + values.player?.replayer?.addEvent(event) } } @@ -615,7 +617,7 @@ export const sessionRecordingPlayerLogic = kea( actions.checkBufferingCompleted() breakpoint() }, - loadRecordingMetaSuccess: async () => { + loadRecordingMetaSuccess: () => { // As the connected data logic may be preloaded we call a shared function here and on mount actions.updateFromMetadata() if (props.autoPlay) { @@ -624,7 +626,7 @@ export const sessionRecordingPlayerLogic = kea( } }, - loadRecordingSnapshotsSuccess: async () => { + loadRecordingSnapshotsSuccess: () => { // As the connected data logic may be preloaded we call a shared function here and on mount actions.updateFromMetadata() }, @@ -690,7 +692,7 @@ export const sessionRecordingPlayerLogic = kea( actions.reportRecordingPlayerSpeedChanged(speed) actions.syncPlayerSpeed() }, - seekToTimestamp: async ({ timestamp, forcePlay }, breakpoint) => { + seekToTimestamp: ({ timestamp, forcePlay }, breakpoint) => { actions.stopAnimation() actions.setCurrentTimestamp(timestamp) @@ -959,7 +961,7 @@ export const sessionRecordingPlayerLogic = kea( console.warn('Failed to enable native full-screen mode:', e) } } else if (document.fullscreenElement === props.playerRef?.current) { - document.exitFullscreen() + await document.exitFullscreen() } }, })), diff --git a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx index 37a53c2235b79..e0a02e72fb626 100644 --- a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx +++ b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx @@ -1,13 +1,15 @@ import { LemonButton, LemonCheckbox, LemonDivider, LemonInput } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' +import { SharingModalContent } from 'lib/components/Sharing/SharingModal' +import { Field } from 'lib/forms/Field' import { IconCopy } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { Field } from 'lib/forms/Field' -import { copyToClipboard } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' + import { playerShareLogic, PlayerShareLogicProps } from './playerShareLogic' -import { SharingModalContent } from 'lib/components/Sharing/SharingModal' export function PlayerShareRecording(props: PlayerShareLogicProps): JSX.Element { const logic = playerShareLogic(props) @@ -27,7 +29,7 @@ export function PlayerShareRecording(props: PlayerShareLogicProps): JSX.Element fullWidth center sideIcon={} - onClick={async () => await copyToClipboard(url, 'recording link')} + onClick={() => void copyToClipboard(url, 'recording link').then(captureException)} title={url} > {url} diff --git a/frontend/src/scenes/session-recordings/player/share/playerShareLogic.ts b/frontend/src/scenes/session-recordings/player/share/playerShareLogic.ts index 3aa3a2994e5cc..851bcaf9d16bd 100644 --- a/frontend/src/scenes/session-recordings/player/share/playerShareLogic.ts +++ b/frontend/src/scenes/session-recordings/player/share/playerShareLogic.ts @@ -1,10 +1,10 @@ import { kea, key, path, props, selectors } from 'kea' import { forms } from 'kea-forms' +import { combineUrl } from 'kea-router' import { colonDelimitedDuration, reverseColonDelimitedDuration } from 'lib/utils' import { urls } from 'scenes/urls' import type { playerShareLogicType } from './playerShareLogicType' -import { combineUrl } from 'kea-router' export type PlayerShareLogicProps = { seconds: number | null diff --git a/frontend/src/scenes/session-recordings/player/utils/playerUtils.ts b/frontend/src/scenes/session-recordings/player/utils/playerUtils.ts index d0a877ade4c92..7fe1d5ecedceb 100644 --- a/frontend/src/scenes/session-recordings/player/utils/playerUtils.ts +++ b/frontend/src/scenes/session-recordings/player/utils/playerUtils.ts @@ -1,11 +1,12 @@ -import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react' -import { SessionRecordingPlaylistType, SessionRecordingType } from '~/types' -import { ExpandableConfig } from 'lib/lemon-ui/LemonTable' +import { router } from 'kea-router' import api from 'lib/api' +import { ExpandableConfig } from 'lib/lemon-ui/LemonTable' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { router } from 'kea-router' +import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react' import { urls } from 'scenes/urls' +import { SessionRecordingPlaylistType, SessionRecordingType } from '~/types' + export const THUMB_SIZE = 15 export const THUMB_OFFSET = THUMB_SIZE / 2 diff --git a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts index 4272a67b256fd..e08cedf8f036f 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts @@ -1,12 +1,14 @@ -import { sortedRecordingSnapshots } from 'scenes/session-recordings/__mocks__/recording_snapshots' -import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' -import { createSegments } from './segmenter' -import { convertSnapshotsResponse } from '../sessionRecordingDataLogic' import { dayjs } from 'lib/dayjs' +import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json' +import { sortedRecordingSnapshots } from 'scenes/session-recordings/__mocks__/recording_snapshots' + import { RecordingSnapshot } from '~/types' +import { convertSnapshotsResponse } from '../sessionRecordingDataLogic' +import { createSegments } from './segmenter' + describe('segmenter', () => { - it('matches snapshots', async () => { + it('matches snapshots', () => { const snapshots = convertSnapshotsResponse(sortedRecordingSnapshots().snapshot_data_by_window_id) const segments = createSegments( snapshots, diff --git a/frontend/src/scenes/session-recordings/player/utils/segmenter.ts b/frontend/src/scenes/session-recordings/player/utils/segmenter.ts index f2e43b459f7a1..2549c35965671 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.ts @@ -1,5 +1,6 @@ -import { EventType, IncrementalSource, eventWithTime } from '@rrweb/types' +import { EventType, eventWithTime, IncrementalSource } from '@rrweb/types' import { Dayjs } from 'lib/dayjs' + import { RecordingSegment, RecordingSnapshot } from '~/types' /** diff --git a/frontend/src/scenes/session-recordings/player/view-explorer/SessionRecordingPlayerExplorer.tsx b/frontend/src/scenes/session-recordings/player/view-explorer/SessionRecordingPlayerExplorer.tsx index 4b77d35ecfa2b..df745dc31c43f 100644 --- a/frontend/src/scenes/session-recordings/player/view-explorer/SessionRecordingPlayerExplorer.tsx +++ b/frontend/src/scenes/session-recordings/player/view-explorer/SessionRecordingPlayerExplorer.tsx @@ -1,8 +1,9 @@ +import './SessionRecordingPlayerExplorer.scss' + import { LemonButton } from '@posthog/lemon-ui' import { useResizeObserver } from 'lib/hooks/useResizeObserver' -import { useState } from 'react' -import './SessionRecordingPlayerExplorer.scss' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { useState } from 'react' export type SessionRecordingPlayerExplorerProps = { html: string diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx index 41b2eb89817f2..e8a2327076805 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx @@ -1,16 +1,18 @@ -import { DurationType, SessionRecordingType } from '~/types' -import { colonDelimitedDuration } from 'lib/utils' import clsx from 'clsx' +import { useValues } from 'kea' import { PropertyIcon } from 'lib/components/PropertyIcon' -import { IconAutocapture, IconKeyboard, IconPinFilled, IconSchedule } from 'lib/lemon-ui/icons' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { TZLabel } from 'lib/components/TZLabel' +import { IconAutocapture, IconKeyboard, IconPinFilled, IconSchedule } from 'lib/lemon-ui/icons' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { colonDelimitedDuration } from 'lib/utils' import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' -import { urls } from 'scenes/urls' -import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' -import { useValues } from 'kea' import { asDisplay } from 'scenes/persons/person-utils' +import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' +import { urls } from 'scenes/urls' + +import { DurationType, SessionRecordingType } from '~/types' + import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPropertiesLogic' export interface SessionRecordingPreviewProps { @@ -110,7 +112,7 @@ function ActivityIndicators({ ) }) ) : ( - + )}
    ) @@ -162,7 +164,7 @@ function PinnedIndicator(): JSX.Element | null { function ViewedIndicator(props: { viewed: boolean }): JSX.Element | null { return !props.viewed ? ( -
    +
    ) : null } @@ -236,8 +238,8 @@ export function SessionRecordingPreview({ export function SessionRecordingPreviewSkeleton(): JSX.Element { return (
    - - + +
    ) } diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss index afdbf12c51d5c..379fe80fd42cc 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss @@ -5,7 +5,6 @@ display: flex; flex-direction: row; justify-content: flex-start; - align-items: flex-start; overflow: hidden; border: 1px solid var(--border); @@ -21,6 +20,12 @@ width: 25%; overflow: hidden; height: 100%; + + .text-link { + .posthog-3000 & { + color: var(--default); + } + } } .SessionRecordingsPlaylist__player { @@ -65,11 +70,11 @@ transition: background-color 200ms ease, border 200ms ease; &--active { - border-left-color: var(--primary); + border-left-color: var(--primary-3000); } &:hover { - background-color: var(--primary-highlight); + background-color: var(--primary-3000-highlight); } .SessionRecordingPreview__property-icon:hover { diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index ec3aa4b9a723c..3072bee23e7dd 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -1,6 +1,27 @@ -import React, { useEffect, useRef } from 'react' +import './SessionRecordingsPlaylist.scss' + +import { LemonButton, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { range } from 'd3' import { BindLogic, useActions, useValues } from 'kea' -import { SessionRecordingType, ReplayTabs } from '~/types' +import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { IconFilter, IconSettings, IconWithCount } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonTableLoader } from 'lib/lemon-ui/LemonTable/LemonTableLoader' +import { Spinner } from 'lib/lemon-ui/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import React, { useEffect, useRef } from 'react' +import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' +import { urls } from 'scenes/urls' + +import { ReplayTabs, SessionRecordingType } from '~/types' + +import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters' +import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' +import { SessionRecordingPreview, SessionRecordingPreviewSkeleton } from './SessionRecordingPreview' import { DEFAULT_RECORDING_FILTERS, defaultPageviewPropertyEntityFilter, @@ -8,26 +29,8 @@ import { SessionRecordingPlaylistLogicProps, sessionRecordingsPlaylistLogic, } from './sessionRecordingsPlaylistLogic' -import './SessionRecordingsPlaylist.scss' -import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' -import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' -import { LemonButton, Link } from '@posthog/lemon-ui' -import { IconFilter, IconSettings, IconWithCount } from 'lib/lemon-ui/icons' -import clsx from 'clsx' -import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' -import { Spinner } from 'lib/lemon-ui/Spinner' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters' -import { urls } from 'scenes/urls' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SessionRecordingsPlaylistSettings } from './SessionRecordingsPlaylistSettings' import { SessionRecordingsPlaylistTroubleshooting } from './SessionRecordingsPlaylistTroubleshooting' -import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic' -import { LemonTableLoader } from 'lib/lemon-ui/LemonTable/LemonTableLoader' -import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' -import { range } from 'd3' -import { SessionRecordingPreview, SessionRecordingPreviewSkeleton } from './SessionRecordingPreview' const SCROLL_TRIGGER_OFFSET = 100 diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.tsx index 447fe87662123..6d8c5b8ac6330 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.tsx @@ -1,16 +1,18 @@ -import { useActions, useValues } from 'kea' import './SessionRecordingsPlaylist.scss' + import { LemonButton, LemonDivider } from '@posthog/lemon-ui' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { SceneExport } from 'scenes/sceneTypes' +import { useActions, useValues } from 'kea' import { EditableField } from 'lib/components/EditableField/EditableField' -import { PageHeader } from 'lib/components/PageHeader' -import { sessionRecordingsPlaylistSceneLogic } from './sessionRecordingsPlaylistSceneLogic' import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { SceneExport } from 'scenes/sceneTypes' import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' + import { SessionRecordingsPlaylist } from './SessionRecordingsPlaylist' +import { sessionRecordingsPlaylistSceneLogic } from './sessionRecordingsPlaylistSceneLogic' export const scene: SceneExport = { component: SessionRecordingsPlaylistScene, @@ -35,8 +37,8 @@ export function SessionRecordingsPlaylistScene(): JSX.Element { return (
    - - + +
    diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx index 8b8f8e3ddfc56..17616e82d4059 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx @@ -1,10 +1,11 @@ -import { useActions, useValues } from 'kea' -import { DurationTypeSelect } from 'scenes/session-recordings/filters/DurationTypeSelect' -import { playerSettingsLogic } from '../player/playerSettingsLogic' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { LemonSwitch } from '@posthog/lemon-ui' import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { IconPause, IconPlay } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { DurationTypeSelect } from 'scenes/session-recordings/filters/DurationTypeSelect' + +import { playerSettingsLogic } from '../player/playerSettingsLogic' export function SessionRecordingsPlaylistSettings(): JSX.Element { const { autoplayDirection, durationTypeToShow, hideViewedRecordings } = useValues(playerSettingsLogic) diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx index 7232b38d2a9e2..37ef17da6ae54 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx @@ -1,6 +1,15 @@ import { Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' + +import { playerSettingsLogic } from '../player/playerSettingsLogic' +import { sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic' export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => { + const { hideViewedRecordings } = useValues(playerSettingsLogic) + const { setHideViewedRecordings } = useActions(playerSettingsLogic) + const { otherRecordings } = useValues(sessionRecordingsPlaylistLogic) + const { setShowSettings } = useActions(sessionRecordingsPlaylistLogic) + return ( <>

    No matching recordings

    @@ -10,6 +19,19 @@ export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => {

      + {otherRecordings.length > 0 && hideViewedRecordings && ( +
    • + Viewed recordings hidden.{' '} + { + setShowSettings(true) + setHideViewedRecordings(false) + }} + > + Toggle option + +
    • + )}
    • They are outside the retention period diff --git a/frontend/src/scenes/session-recordings/playlist/playlistUtils.test.ts b/frontend/src/scenes/session-recordings/playlist/playlistUtils.test.ts index fc4096abff609..d7b4025c7b20a 100644 --- a/frontend/src/scenes/session-recordings/playlist/playlistUtils.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/playlistUtils.test.ts @@ -1,6 +1,7 @@ -import { CohortType, FilterLogicalOperator, PropertyFilterType, PropertyOperator } from '~/types' import { summarizePlaylistFilters } from 'scenes/session-recordings/playlist/playlistUtils' +import { CohortType, FilterLogicalOperator, PropertyFilterType, PropertyOperator } from '~/types' + describe('summarizePlaylistFilters()', () => { const cohortIdsMapped: Partial> = { 1: { diff --git a/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts b/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts index 9767d4b41809a..c12e3a950cdd8 100644 --- a/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts +++ b/frontend/src/scenes/session-recordings/playlist/playlistUtils.ts @@ -1,16 +1,19 @@ -import { PropertyOperator, RecordingFilters, SessionRecordingPlaylistType } from '~/types' -import { cohortsModelType } from '~/models/cohortsModelType' -import { toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' -import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' -import { convertPropertyGroupToProperties, deleteWithUndo, genericOperatorMap } from 'lib/utils' -import { getKeyMapping } from 'lib/taxonomy' +import { router } from 'kea-router' import api from 'lib/api' +import { convertPropertyGroupToProperties } from 'lib/components/PropertyFilters/utils' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { DEFAULT_RECORDING_FILTERS } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' -import { router } from 'kea-router' -import { urls } from 'scenes/urls' +import { getKeyMapping } from 'lib/taxonomy' +import { genericOperatorMap } from 'lib/utils' +import { deleteWithUndo } from 'lib/utils/deleteWithUndo' import { openBillingPopupModal } from 'scenes/billing/BillingPopup' +import { toLocalFilters } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' +import { getDisplayNameFromEntityFilter } from 'scenes/insights/utils' +import { DEFAULT_RECORDING_FILTERS } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' import { PLAYLIST_LIMIT_REACHED_MESSAGE } from 'scenes/session-recordings/sessionRecordingsLogic' +import { urls } from 'scenes/urls' + +import { cohortsModelType } from '~/models/cohortsModelType' +import { PropertyOperator, RecordingFilters, SessionRecordingPlaylistType } from '~/types' function getOperatorSymbol(operator: PropertyOperator | null): string { if (!operator) { diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts index 959e885399cc6..8a37208f27945 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts @@ -1,7 +1,8 @@ -import { useMocks } from '~/mocks/jest' -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' import { sessionRecordingsListPropertiesLogic } from 'scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic' + +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' import { SessionRecordingType } from '~/types' const mockSessons: SessionRecordingType[] = [ @@ -52,7 +53,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }) it('loads properties', async () => { - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.loadPropertiesForSessions(mockSessons) }).toDispatchActions(['loadPropertiesForSessionsSuccess']) @@ -69,7 +70,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }) it('does not loads cached properties', async () => { - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.loadPropertiesForSessions(mockSessons) }).toDispatchActions(['loadPropertiesForSessionsSuccess']) @@ -80,7 +81,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }, }) - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.maybeLoadPropertiesForSessions(mockSessons) }).toNotHaveDispatchedActions(['loadPropertiesForSessionsSuccess']) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.ts index 627e99beff165..578b9a5be6523 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.ts @@ -1,12 +1,14 @@ -import { connect, kea, path, reducers, actions, listeners } from 'kea' +import { actions, connect, kea, listeners, path, reducers } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' -import { SessionRecordingPropertiesType, SessionRecordingType } from '~/types' +import { dayjs } from 'lib/dayjs' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import type { sessionRecordingsListPropertiesLogicType } from './sessionRecordingsListPropertiesLogicType' + import { HogQLQuery, NodeKind } from '~/queries/schema' -import { dayjs } from 'lib/dayjs' import { hogql } from '~/queries/utils' +import { SessionRecordingPropertiesType, SessionRecordingType } from '~/types' + +import type { sessionRecordingsListPropertiesLogicType } from './sessionRecordingsListPropertiesLogicType' // This logic is used to fetch properties for a list of recordings // It is used in a global way as the cached values can be re-used diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts index bbe331b1f9c08..36bef7ea8faf8 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts @@ -1,15 +1,17 @@ -import { - sessionRecordingsPlaylistLogic, - RECORDINGS_LIMIT, - DEFAULT_RECORDING_FILTERS, - defaultRecordingDurationFilter, -} from './sessionRecordingsPlaylistLogic' +import { router } from 'kea-router' import { expectLogic } from 'kea-test-utils' + +import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import { router } from 'kea-router' import { PropertyFilterType, PropertyOperator, RecordingFilters } from '~/types' -import { useMocks } from '~/mocks/jest' + import { sessionRecordingDataLogic } from '../player/sessionRecordingDataLogic' +import { + DEFAULT_RECORDING_FILTERS, + defaultRecordingDurationFilter, + RECORDINGS_LIMIT, + sessionRecordingsPlaylistLogic, +} from './sessionRecordingsPlaylistLogic' describe('sessionRecordingsPlaylistLogic', () => { let logic: ReturnType @@ -165,7 +167,7 @@ describe('sessionRecordingsPlaylistLogic', () => { it('starts as null', () => { expectLogic(logic).toMatchValues({ activeSessionRecording: undefined }) }) - it('is set by setSessionRecordingId', async () => { + it('is set by setSessionRecordingId', () => { expectLogic(logic, () => logic.actions.setSelectedRecordingId('abc')) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ @@ -175,7 +177,7 @@ describe('sessionRecordingsPlaylistLogic', () => { expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc') }) - it('is partial if sessionRecordingId not in list', async () => { + it('is partial if sessionRecordingId not in list', () => { expectLogic(logic, () => logic.actions.setSelectedRecordingId('not-in-list')) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ @@ -198,7 +200,7 @@ describe('sessionRecordingsPlaylistLogic', () => { }) it('mounts and loads the recording when a recording is opened', () => { - expectLogic(logic, async () => await logic.actions.setSelectedRecordingId('abcd')) + expectLogic(logic, async () => logic.asyncActions.setSelectedRecordingId('abcd')) .toMount(sessionRecordingDataLogic({ sessionRecordingId: 'abcd' })) .toDispatchActions(['loadEntireRecording']) }) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index b07d55e3f9d10..130cad3bf210f 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -1,6 +1,13 @@ +import equal from 'fast-deep-equal' import { actions, afterMount, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { actionToUrl, router, urlToAction } from 'kea-router' import api from 'lib/api' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { objectClean, objectsEqual } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import posthog from 'posthog-js' + import { AnyPropertyFilter, PropertyFilterType, @@ -11,15 +18,9 @@ import { SessionRecordingsResponse, SessionRecordingType, } from '~/types' -import { actionToUrl, router, urlToAction } from 'kea-router' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import equal from 'fast-deep-equal' -import { loaders } from 'kea-loaders' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPropertiesLogic' -import { playerSettingsLogic } from '../player/playerSettingsLogic' -import posthog from 'posthog-js' +import { playerSettingsLogic } from '../player/playerSettingsLogic' +import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPropertiesLogic' import type { sessionRecordingsPlaylistLogicType } from './sessionRecordingsPlaylistLogicType' export type PersonUUID = string @@ -350,7 +351,7 @@ export const sessionRecordingsPlaylistLogic = kea ({ ...state, diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts index 4530486fb5ed0..4eecf62e6634f 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts @@ -1,8 +1,9 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' -import { useMocks } from '~/mocks/jest' import { sessionRecordingsPlaylistSceneLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' + describe('sessionRecordingsPlaylistSceneLogic', () => { let logic: ReturnType const mockPlaylist = { @@ -64,9 +65,9 @@ describe('sessionRecordingsPlaylistSceneLogic', () => { }, ], } - expectLogic(logic, async () => { - await logic.actions.setFilters(newFilter) - await logic.actions.updatePlaylist({}) + expectLogic(logic, () => { + logic.actions.setFilters(newFilter) + logic.actions.updatePlaylist({}) }) .toDispatchActions(['setFilters']) .toMatchValues({ filters: expect.objectContaining(newFilter), hasChanges: true }) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts index f5e310872f570..a8e9b07eb3144 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts @@ -1,9 +1,9 @@ -import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { Breadcrumb, RecordingFilters, SessionRecordingPlaylistType, ReplayTabs, SessionRecordingType } from '~/types' -import { urls } from 'scenes/urls' import equal from 'fast-deep-equal' +import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import { beforeUnload, router } from 'kea-router' -import { cohortsModel } from '~/models/cohortsModel' +import api from 'lib/api' +import { Scene } from 'scenes/sceneTypes' import { deletePlaylist, duplicatePlaylist, @@ -11,12 +11,14 @@ import { summarizePlaylistFilters, updatePlaylist, } from 'scenes/session-recordings/playlist/playlistUtils' -import { loaders } from 'kea-loaders' +import { urls } from 'scenes/urls' + +import { cohortsModel } from '~/models/cohortsModel' +import { Breadcrumb, RecordingFilters, ReplayTabs, SessionRecordingPlaylistType, SessionRecordingType } from '~/types' -import type { sessionRecordingsPlaylistSceneLogicType } from './sessionRecordingsPlaylistSceneLogicType' -import { PINNED_RECORDINGS_LIMIT } from './sessionRecordingsPlaylistLogic' -import api from 'lib/api' import { addRecordingToPlaylist, removeRecordingFromPlaylist } from '../player/utils/playerUtils' +import { PINNED_RECORDINGS_LIMIT } from './sessionRecordingsPlaylistLogic' +import type { sessionRecordingsPlaylistSceneLogicType } from './sessionRecordingsPlaylistSceneLogicType' export interface SessionRecordingsPlaylistLogicProps { shortId: string @@ -135,14 +137,17 @@ export const sessionRecordingsPlaylistSceneLogic = kea [s.playlist], (playlist): Breadcrumb[] => [ { + key: Scene.Replay, name: 'Replay', path: urls.replay(), }, { + key: ReplayTabs.Playlists, name: 'Playlists', path: urls.replay(ReplayTabs.Playlists), }, { + key: playlist?.short_id || 'new', name: playlist?.name || playlist?.derived_name || '(Untitled)', path: urls.replayPlaylist(playlist?.short_id || ''), }, diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx index e5a8a07ba2eec..98cc4d7f89c61 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx @@ -1,17 +1,19 @@ -import { useActions, useValues } from 'kea' -import { ReplayTabs, SessionRecordingPlaylistType } from '~/types' -import { PLAYLISTS_PER_PAGE, savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic' +import { TZLabel } from '@posthog/apps-common' import { LemonButton, LemonDivider, LemonInput, LemonSelect, LemonTable, Link } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { IconCalendar, IconPinFilled, IconPinOutline } from 'lib/lemon-ui/icons' +import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { urls } from 'scenes/urls' import { createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { membersLogic } from 'scenes/organization/membersLogic' -import { TZLabel } from '@posthog/apps-common' import { SavedSessionRecordingPlaylistsEmptyState } from 'scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState' -import clsx from 'clsx' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { IconPinOutline, IconPinFilled, IconCalendar } from 'lib/lemon-ui/icons' +import { urls } from 'scenes/urls' + +import { ReplayTabs, SessionRecordingPlaylistType } from '~/types' + +import { PLAYLISTS_PER_PAGE, savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic' export type SavedSessionRecordingPlaylistsProps = { tab: ReplayTabs.Playlists diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx index 5883934034c1e..8397d40f303b8 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx @@ -1,11 +1,13 @@ -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconPlus } from 'lib/lemon-ui/icons' -import { createPlaylist } from '../playlist/playlistUtils' import { useActions, useValues } from 'kea' +import { IconPlus } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { sceneLogic } from 'scenes/sceneLogic' -import { savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic' + import { AvailableFeature, ReplayTabs } from '~/types' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' + +import { createPlaylist } from '../playlist/playlistUtils' +import { savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic' export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element { const { guardAvailableFeature } = useActions(sceneLogic) @@ -27,7 +29,7 @@ export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element { AvailableFeature.RECORDINGS_PLAYLISTS, 'recording playlists', "Playlists allow you to save certain session recordings as a group to easily find and watch them again in the future. You've unfortunately run out of playlists on your current subscription plan.", - () => createPlaylist({}, true), + () => void createPlaylist({}, true), undefined, playlists.count ) diff --git a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.test.ts b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.test.ts index 52ec11c4db620..9121b4b65fb9e 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.test.ts +++ b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.test.ts @@ -1,8 +1,5 @@ -import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' import { router } from 'kea-router' -import { ReplayTabs } from '~/types' -import { useMocks } from '~/mocks/jest' +import { expectLogic } from 'kea-test-utils' import { DEFAULT_PLAYLIST_FILTERS, PLAYLISTS_PER_PAGE, @@ -10,6 +7,10 @@ import { } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' import { urls } from 'scenes/urls' +import { useMocks } from '~/mocks/jest' +import { initKeaTests } from '~/test/init' +import { ReplayTabs } from '~/types' + describe('savedSessionRecordingPlaylistsLogic', () => { let logic: ReturnType const mockPlaylistsResponse = { diff --git a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts index 970d191b5227e..b4b4d214c62b6 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts +++ b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts @@ -1,17 +1,19 @@ +import { lemonToast } from '@posthog/lemon-ui' import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' +import { actionToUrl, router, urlToAction } from 'kea-router' import api, { PaginatedResponse } from 'lib/api' -import { objectClean, objectsEqual, toParams } from 'lib/utils' -import { SessionRecordingPlaylistType, ReplayTabs } from '~/types' import { dayjs } from 'lib/dayjs' -import type { savedSessionRecordingPlaylistsLogicType } from './savedSessionRecordingPlaylistsLogicType' import { Sorting } from 'lib/lemon-ui/LemonTable' import { PaginationManual } from 'lib/lemon-ui/PaginationControl' -import { actionToUrl, router, urlToAction } from 'kea-router' +import { objectClean, objectsEqual, toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { urls } from 'scenes/urls' + +import { ReplayTabs, SessionRecordingPlaylistType } from '~/types' + import { createPlaylist, deletePlaylist } from '../playlist/playlistUtils' -import { lemonToast } from '@posthog/lemon-ui' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import type { savedSessionRecordingPlaylistsLogicType } from './savedSessionRecordingPlaylistsLogicType' export const PLAYLISTS_PER_PAGE = 30 @@ -25,7 +27,7 @@ export interface SavedSessionRecordingPlaylistsFilters { order: string search: string createdBy: number | 'All users' - dateFrom: string | dayjs.Dayjs | undefined | 'all' | null + dateFrom: string | dayjs.Dayjs | undefined | null dateTo: string | dayjs.Dayjs | undefined | null page: number pinned: boolean @@ -227,7 +229,7 @@ export const savedSessionRecordingPlaylistsLogic = kea ({ - [urls.replay(ReplayTabs.Playlists)]: async (_, searchParams) => { + [urls.replay(ReplayTabs.Playlists)]: (_, searchParams) => { const currentFilters = values.filters const nextFilters = objectClean(searchParams) if (!objectsEqual(currentFilters, nextFilters)) { diff --git a/frontend/src/scenes/session-recordings/sessionRecordingsLogic.ts b/frontend/src/scenes/session-recordings/sessionRecordingsLogic.ts index 10d58cdcb3d07..c321f32b89b22 100644 --- a/frontend/src/scenes/session-recordings/sessionRecordingsLogic.ts +++ b/frontend/src/scenes/session-recordings/sessionRecordingsLogic.ts @@ -1,15 +1,18 @@ import { actions, kea, path, reducers, selectors } from 'kea' -import { Breadcrumb, ReplayTabs } from '~/types' -import { urls } from 'scenes/urls' import { actionToUrl, router, urlToAction } from 'kea-router' -import type { sessionRecordingsLogicType } from './sessionRecordingsLogicType' import { SESSION_RECORDINGS_PLAYLIST_FREE_COUNT } from 'lib/constants' import { capitalizeFirstLetter } from 'lib/utils' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb, ReplayTabs } from '~/types' + +import type { sessionRecordingsLogicType } from './sessionRecordingsLogicType' export const humanFriendlyTabName = (tab: ReplayTabs): string => { switch (tab) { case ReplayTabs.Recent: - return 'Recent Recordings' + return 'Recent recordings' case ReplayTabs.Playlists: return 'Playlists' case ReplayTabs.FilePlayback: @@ -48,11 +51,13 @@ export const sessionRecordingsLogic = kea([ const breadcrumbs: Breadcrumb[] = [] if (tab !== ReplayTabs.Recent) { breadcrumbs.push({ + key: Scene.Replay, name: 'Replay', path: urls.replay(), }) } breadcrumbs.push({ + key: tab, name: humanFriendlyTabName(tab), }) diff --git a/frontend/src/scenes/settings/Settings.scss b/frontend/src/scenes/settings/Settings.scss index 84bc262bb7016..ac3e09d44c3e1 100644 --- a/frontend/src/scenes/settings/Settings.scss +++ b/frontend/src/scenes/settings/Settings.scss @@ -22,7 +22,6 @@ &--compact { gap: 0; - flex-direction: column; .Settings__sections { diff --git a/frontend/src/scenes/settings/Settings.tsx b/frontend/src/scenes/settings/Settings.tsx index 80097630a0486..27bc7f669d350 100644 --- a/frontend/src/scenes/settings/Settings.tsx +++ b/frontend/src/scenes/settings/Settings.tsx @@ -1,15 +1,16 @@ +import './Settings.scss' + import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui' -import { IconChevronRight, IconLink } from 'lib/lemon-ui/icons' -import { SettingsLogicProps, settingsLogic } from './settingsLogic' -import { useActions, useValues } from 'kea' -import { SettingLevelIds } from './types' import clsx from 'clsx' -import { capitalizeFirstLetter } from 'lib/utils' +import { useActions, useValues } from 'kea' +import { NotFound } from 'lib/components/NotFound' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { IconChevronRight, IconLink } from 'lib/lemon-ui/icons' +import { capitalizeFirstLetter } from 'lib/utils' import { teamLogic } from 'scenes/teamLogic' -import './Settings.scss' -import { NotFound } from 'lib/components/NotFound' +import { settingsLogic } from './settingsLogic' +import { SettingLevelIds, SettingsLogicProps } from './types' export function Settings({ hideSections = false, diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index f5015ea7f5681..e64b368496d5a 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -1,16 +1,19 @@ -import { ChangePassword } from './user/ChangePassword' -import { OptOutCapture } from './user/OptOutCapture' -import { PersonalAPIKeys } from './user/PersonalAPIKeys' -import { TwoFactorAuthentication } from './user/TwoFactorAuthentication' -import { UpdateEmailPreferences } from './user/UpdateEmailPreferences' -import { UserDetails } from './user/UserDetails' -import { OrganizationDisplayName } from './organization/OrgDisplayName' import { Invites } from './organization/Invites' import { Members } from './organization/Members' -import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' -import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' import { OrganizationDangerZone } from './organization/OrganizationDangerZone' +import { OrganizationDisplayName } from './organization/OrgDisplayName' +import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' import { PermissionsGrid } from './organization/Permissions/PermissionsGrid' +import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' +import { AutocaptureSettings, ExceptionAutocaptureSettings } from './project/AutocaptureSettings' +import { CorrelationConfig } from './project/CorrelationConfig' +import { DataAttributes } from './project/DataAttributes' +import { GroupAnalyticsConfig } from './project/GroupAnalyticsConfig' +import { IPCapture } from './project/IPCapture' +import { PathCleaningFiltersConfig } from './project/PathCleaningFiltersConfig' +import { PersonDisplayNameProperties } from './project/PersonDisplayNameProperties' +import { ProjectAccessControl } from './project/ProjectAccessControl' +import { ProjectDangerZone } from './project/ProjectDangerZone' import { Bookmarklet, ProjectDisplayName, @@ -19,22 +22,19 @@ import { ProjectVariables, WebSnippet, } from './project/ProjectSettings' -import { AutocaptureSettings, ExceptionAutocaptureSettings } from './project/AutocaptureSettings' -import { DataAttributes } from './project/DataAttributes' import { ReplayAuthorizedDomains, ReplayCostControl, ReplayGeneral } from './project/SessionRecordingSettings' -import { ProjectDangerZone } from './project/ProjectDangerZone' -import { ProjectAccessControl } from './project/ProjectAccessControl' -import { ProjectAccountFiltersSetting } from './project/TestAccountFiltersConfig' -import { CorrelationConfig } from './project/CorrelationConfig' -import { PersonDisplayNameProperties } from './project/PersonDisplayNameProperties' -import { IPCapture } from './project/IPCapture' -import { WebhookIntegration } from './project/WebhookIntegration' +import { SettingPersonsOnEvents } from './project/SettingPersonsOnEvents' import { SlackIntegration } from './project/SlackIntegration' -import { PathCleaningFiltersConfig } from './project/PathCleaningFiltersConfig' -import { GroupAnalyticsConfig } from './project/GroupAnalyticsConfig' import { SurveySettings } from './project/SurveySettings' -import { SettingPersonsOnEvents } from './project/SettingPersonsOnEvents' +import { ProjectAccountFiltersSetting } from './project/TestAccountFiltersConfig' +import { WebhookIntegration } from './project/WebhookIntegration' import { SettingSection } from './types' +import { ChangePassword } from './user/ChangePassword' +import { OptOutCapture } from './user/OptOutCapture' +import { PersonalAPIKeys } from './user/PersonalAPIKeys' +import { TwoFactorAuthentication } from './user/TwoFactorAuthentication' +import { UpdateEmailPreferences } from './user/UpdateEmailPreferences' +import { UserDetails } from './user/UserDetails' export const SettingsMap: SettingSection[] = [ // PROJECT diff --git a/frontend/src/scenes/settings/SettingsScene.stories.tsx b/frontend/src/scenes/settings/SettingsScene.stories.tsx index bcfb160b24bad..404b8551df447 100644 --- a/frontend/src/scenes/settings/SettingsScene.stories.tsx +++ b/frontend/src/scenes/settings/SettingsScene.stories.tsx @@ -1,11 +1,12 @@ import { Meta } from '@storybook/react' -import { mswDecorator } from '~/mocks/browser' -import preflightJson from '~/mocks/fixtures/_preflight.json' -import { App } from 'scenes/App' -import { useEffect } from 'react' import { router } from 'kea-router' +import { useEffect } from 'react' +import { App } from 'scenes/App' import { urls } from 'scenes/urls' +import { mswDecorator } from '~/mocks/browser' +import preflightJson from '~/mocks/fixtures/_preflight.json' + const meta: Meta = { title: 'Scenes-Other/Settings', parameters: { diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx index 29695feeefa37..76b82f2c1fd96 100644 --- a/frontend/src/scenes/settings/SettingsScene.tsx +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -1,9 +1,10 @@ -import { SceneExport } from 'scenes/sceneTypes' import { useValues } from 'kea' -import { settingsSceneLogic } from './settingsSceneLogic' -import { useAnchor } from 'lib/hooks/useAnchor' import { router } from 'kea-router' +import { useAnchor } from 'lib/hooks/useAnchor' +import { SceneExport } from 'scenes/sceneTypes' + import { Settings } from './Settings' +import { settingsSceneLogic } from './settingsSceneLogic' export const scene: SceneExport = { component: SettingsScene, diff --git a/frontend/src/scenes/settings/organization/InviteModal.tsx b/frontend/src/scenes/settings/organization/InviteModal.tsx index 9e9743512547f..d0c719a294d5d 100644 --- a/frontend/src/scenes/settings/organization/InviteModal.tsx +++ b/frontend/src/scenes/settings/organization/InviteModal.tsx @@ -1,17 +1,20 @@ -import { useActions, useValues } from 'kea' import './InviteModal.scss' -import { isEmail, pluralize } from 'lib/utils' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { inviteLogic } from './inviteLogic' + +import { LemonInput, LemonTextArea, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' import { IconDelete, IconPlus } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { LemonTextArea, LemonInput, Link } from '@posthog/lemon-ui' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { OrganizationInviteType } from '~/types' -import { userLogic } from 'scenes/userLogic' -import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { isEmail, pluralize } from 'lib/utils' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { OrganizationInviteType } from '~/types' + +import { inviteLogic } from './inviteLogic' /** Shuffled placeholder names */ const PLACEHOLDER_NAMES: string[] = [...Array(10).fill('Jane'), ...Array(10).fill('John'), 'Sonic'].sort( diff --git a/frontend/src/scenes/settings/organization/Invites.tsx b/frontend/src/scenes/settings/organization/Invites.tsx index 41be87d491cf6..132184c745226 100644 --- a/frontend/src/scenes/settings/organization/Invites.tsx +++ b/frontend/src/scenes/settings/organization/Invites.tsx @@ -1,15 +1,17 @@ -import { useValues, useActions } from 'kea' -import { OrganizationInviteType } from '~/types' +import { useActions, useValues } from 'kea' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { inviteLogic } from './inviteLogic' -import { EmailUnavailableMessage } from './InviteModal' +import { IconClose } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconClose } from 'lib/lemon-ui/icons' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' + +import { OrganizationInviteType } from '~/types' + +import { inviteLogic } from './inviteLogic' +import { EmailUnavailableMessage } from './InviteModal' function InviteLinkComponent(id: string, invite: OrganizationInviteType): JSX.Element { const url = new URL(`/signup/${id}`, document.baseURI).href diff --git a/frontend/src/scenes/settings/organization/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx index ea468523c95e2..c892bbd7dc4e9 100644 --- a/frontend/src/scenes/settings/organization/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -1,27 +1,28 @@ -import { useValues, useActions } from 'kea' +import { LemonInput, LemonModal, LemonSwitch } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { TZLabel } from 'lib/components/TZLabel' import { OrganizationMembershipLevel } from 'lib/constants' -import { OrganizationMemberType } from '~/types' -import { organizationLogic } from 'scenes/organizationLogic' -import { userLogic } from 'scenes/userLogic' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { getReasonForAccessLevelChangeProhibition, - organizationMembershipLevelIntegers, membershipLevelToName, + organizationMembershipLevelIntegers, } from 'lib/utils/permissioning' -import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { TZLabel } from 'lib/components/TZLabel' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { More } from 'lib/lemon-ui/LemonButton/More' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { LemonInput, LemonModal, LemonSwitch } from '@posthog/lemon-ui' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { useState } from 'react' import { Setup2FA } from 'scenes/authentication/Setup2FA' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { membersLogic } from 'scenes/organization/membersLogic' +import { organizationLogic } from 'scenes/organizationLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { OrganizationMemberType } from '~/types' function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element | null { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/organization/OrgDisplayName.tsx b/frontend/src/scenes/settings/organization/OrgDisplayName.tsx index 2b65751496e6c..6f1836245ad59 100644 --- a/frontend/src/scenes/settings/organization/OrgDisplayName.tsx +++ b/frontend/src/scenes/settings/organization/OrgDisplayName.tsx @@ -1,5 +1,5 @@ -import { LemonInput, LemonButton } from '@posthog/lemon-ui' -import { useValues, useActions } from 'kea' +import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { useRestrictedArea } from 'lib/components/RestrictedArea' import { OrganizationMembershipLevel } from 'lib/constants' import { useState } from 'react' diff --git a/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx index e2e4a1524e9e8..f427f74e684e5 100644 --- a/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx +++ b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx @@ -1,5 +1,5 @@ import { LemonSwitch } from '@posthog/lemon-ui' -import { useValues, useActions } from 'kea' +import { useActions, useValues } from 'kea' import { useRestrictedArea } from 'lib/components/RestrictedArea' import { OrganizationMembershipLevel } from 'lib/constants' import { organizationLogic } from 'scenes/organizationLogic' diff --git a/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx b/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx index 5425d9b67c298..937a6a9e05683 100644 --- a/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx +++ b/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx @@ -1,10 +1,10 @@ +import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { organizationLogic } from 'scenes/organizationLogic' import { useRestrictedArea } from 'lib/components/RestrictedArea' -import { Dispatch, SetStateAction, useState } from 'react' -import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' -import { IconDelete } from 'lib/lemon-ui/icons' import { OrganizationMembershipLevel } from 'lib/constants' +import { IconDelete } from 'lib/lemon-ui/icons' +import { Dispatch, SetStateAction, useState } from 'react' +import { organizationLogic } from 'scenes/organizationLogic' export function DeleteOrganizationModal({ isOpen, diff --git a/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx b/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx index 7adcbed6a3621..6fda5defbf015 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx @@ -1,10 +1,12 @@ import { LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { capitalizeFirstLetter } from 'lib/utils' + import { AccessLevel } from '~/types' -import { permissionsLogic, FormattedResourceLevel, ResourcePermissionMapping } from './permissionsLogic' + +import { FormattedResourceLevel, permissionsLogic, ResourcePermissionMapping } from './permissionsLogic' export function Permissions({ isRestricted }: RestrictedComponentProps): JSX.Element { const { allPermissions } = useValues(permissionsLogic) diff --git a/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx index 5f97a4c778e31..e2607e420d1e3 100644 --- a/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx @@ -1,18 +1,20 @@ import { LemonButton, LemonCheckbox, LemonTable } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { IconInfo } from 'lib/lemon-ui/icons' -import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { useRestrictedArea } from 'lib/components/RestrictedArea' import { TitleWithIcon } from 'lib/components/TitleWithIcon' +import { OrganizationMembershipLevel } from 'lib/constants' +import { IconInfo } from 'lib/lemon-ui/icons' +import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { organizationLogic } from 'scenes/organizationLogic' + import { AccessLevel, AvailableFeature, Resource, RoleType } from '~/types' + import { permissionsLogic } from './permissionsLogic' import { CreateRoleModal } from './Roles/CreateRoleModal' import { rolesLogic } from './Roles/rolesLogic' import { getSingularType } from './utils' -import { OrganizationMembershipLevel } from 'lib/constants' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' export function PermissionsGrid(): JSX.Element { const { resourceRolesAccess, organizationResourcePermissionsLoading } = useValues(permissionsLogic) diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx index e4cf21322dd11..a0b97bb94302b 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx @@ -1,15 +1,17 @@ import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { IconDelete } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { useState } from 'react' import { organizationLogic } from 'scenes/organizationLogic' + import { RoleMemberType, UserType } from '~/types' + import { rolesLogic } from './rolesLogic' export function CreateRoleModal(): JSX.Element { diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx index 915433298a2d3..bb8c276eb6dc6 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx @@ -1,14 +1,16 @@ import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { RestrictedComponentProps } from 'lib/components/RestrictedArea' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { useState } from 'react' import { urls } from 'scenes/urls' + import { AccessLevel, RoleType } from '~/types' + import { CreateRoleModal } from './CreateRoleModal' import { rolesLogic } from './rolesLogic' -import { useState } from 'react' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' export function Roles({ isRestricted }: RestrictedComponentProps): JSX.Element { const { roles, rolesLoading } = useValues(rolesLogic) diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx index 32276a71df837..018ba2ba5e589 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx @@ -1,10 +1,12 @@ -import { actions, kea, reducers, path, connect, selectors, afterMount, listeners } from 'kea' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { teamMembersLogic } from 'scenes/settings/project/teamMembersLogic' + import { AccessLevel, Resource, RoleMemberType, RoleType, UserBasicType } from '~/types' + import type { rolesLogicType } from './rolesLogicType' -import { teamMembersLogic } from 'scenes/settings/project/teamMembersLogic' export const rolesLogic = kea([ path(['scenes', 'organization', 'rolesLogic']), @@ -53,7 +55,7 @@ export const rolesLogic = kea([ }, ], }), - loaders(({ values, actions }) => ({ + loaders(({ values, actions, asyncActions }) => ({ roles: { loadRoles: async () => { const response = await api.roles.list() @@ -62,7 +64,7 @@ export const rolesLogic = kea([ createRole: async (roleName: string) => { const { roles, roleMembersToAdd } = values const newRole = await api.roles.create(roleName) - await actions.addRoleMembers({ role: newRole, membersToAdd: roleMembersToAdd }) + await asyncActions.addRoleMembers({ role: newRole, membersToAdd: roleMembersToAdd }) eventUsageLogic.actions.reportRoleCreated(roleName) actions.setRoleMembersInFocus([]) actions.setRoleMembersToAdd([]) diff --git a/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx index 6fccb1bd9311c..654b5b05caaa5 100644 --- a/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx @@ -1,14 +1,16 @@ -import { afterMount, kea, selectors, path, connect, actions, listeners } from 'kea' +import { lemonToast } from '@posthog/lemon-ui' +import { actions, afterMount, connect, kea, listeners, path, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' -import { OrganizationResourcePermissionType, Resource, AccessLevel, RoleType } from '~/types' -import type { permissionsLogicType } from './permissionsLogicType' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' -import { rolesLogic } from './Roles/rolesLogic' -import { lemonToast } from '@posthog/lemon-ui' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { AccessLevel, OrganizationResourcePermissionType, Resource, RoleType } from '~/types' + +import type { permissionsLogicType } from './permissionsLogicType' +import { rolesLogic } from './Roles/rolesLogic' + const ResourceDisplayMapping: Record = { [Resource.FEATURE_FLAGS]: 'Feature Flags', } diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx index b1abc4d7f185a..128ac627bb1f2 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx @@ -1,9 +1,10 @@ import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { DOMAIN_REGEX } from 'lib/constants' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { DOMAIN_REGEX } from 'lib/constants' import { useState } from 'react' + import { verifiedDomainsLogic } from './verifiedDomainsLogic' export function AddDomainModal(): JSX.Element { diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx index 2d8304b40ca7f..654a2f83e2b2d 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx @@ -1,15 +1,16 @@ +import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { verifiedDomainsLogic } from './verifiedDomainsLogic' +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 { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { Form } from 'kea-forms' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { Link } from '@posthog/lemon-ui' + +import { verifiedDomainsLogic } from './verifiedDomainsLogic' export function ConfigureSAMLModal(): JSX.Element { const { configureSAMLModalId, isSamlConfigSubmitting, samlConfig } = useValues(verifiedDomainsLogic) @@ -40,7 +41,7 @@ export function ConfigureSAMLModal(): JSX.Element { {`${siteUrl}/complete/saml/`} - {configureSAMLModalId ?? undefined} + {configureSAMLModalId || 'unknown'} {siteUrl} diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx index d1ea2e91c1b11..4b668f60c47a0 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx @@ -1,9 +1,11 @@ +import { Meta, StoryFn } from '@storybook/react' import { useState } from 'react' -import { StoryFn, Meta } from '@storybook/react' -import { SSOSelect } from './SSOSelect' -import { SSOProvider } from '~/types' + import { useStorybookMocks } from '~/mocks/browser' import preflightJSON from '~/mocks/fixtures/_preflight.json' +import { SSOProvider } from '~/types' + +import { SSOSelect } from './SSOSelect' const meta: Meta = { title: 'Components/SSO Select', diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx index f8072bdc57d32..abdb5ab7dfe02 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx @@ -3,6 +3,7 @@ import { useValues } from 'kea' import { SocialLoginIcon } from 'lib/components/SocialLoginButton/SocialLoginIcon' import { SSO_PROVIDER_NAMES } from 'lib/constants' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + import { SSOProvider } from '~/types' export interface SSOSelectInterface { diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx index 548a89ad5ca5a..479e57c3057f6 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx @@ -1,22 +1,24 @@ +import { IconInfo } from '@posthog/icons' import { useActions, useValues } from 'kea' -import { IconCheckmark, IconDelete, IconExclamation, IconWarning, IconLock, IconOffline } from 'lib/lemon-ui/icons' +import { IconCheckmark, IconDelete, IconExclamation, IconLock, IconOffline, IconWarning } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { More } from 'lib/lemon-ui/LemonButton/More' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { urls } from 'scenes/urls' + import { OrganizationDomainType } from '~/types' -import { verifiedDomainsLogic } from './verifiedDomainsLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { More } from 'lib/lemon-ui/LemonButton/More' + import { AddDomainModal } from './AddDomainModal' +import { ConfigureSAMLModal } from './ConfigureSAMLModal' import { SSOSelect } from './SSOSelect' +import { verifiedDomainsLogic } from './verifiedDomainsLogic' import { VerifyDomainModal } from './VerifyDomainModal' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { Link } from 'lib/lemon-ui/Link' -import { UPGRADE_LINK } from 'lib/constants' -import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' -import { ConfigureSAMLModal } from './ConfigureSAMLModal' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' -import { IconInfo } from '@posthog/icons' const iconStyle = { marginRight: 4, fontSize: '1.15em', paddingTop: 2 } @@ -140,11 +142,7 @@ function VerifiedDomainsTable(): JSX.Element { render: function SSOEnforcement(_, { sso_enforcement, is_verified, id, has_saml }, index) { if (!isSSOEnforcementAvailable) { return index === 0 ? ( - + Upgrade to enable SSO enforcement @@ -170,11 +168,7 @@ function VerifiedDomainsTable(): JSX.Element { render: function SAML(_, { is_verified, saml_acs_url, saml_entity_id, saml_x509_cert, has_saml }, index) { if (!isSAMLAvailable) { return index === 0 ? ( - + Upgrade to enable SAML ) : ( diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx index 6fcc0606652c1..6eaa28fa0f252 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx @@ -1,9 +1,10 @@ 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 { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { PureField } from 'lib/forms/Field' + import { verifiedDomainsLogic } from './verifiedDomainsLogic' export function VerifyDomainModal(): JSX.Element { @@ -51,9 +52,11 @@ export function VerifyDomainModal(): JSX.Element {
      {domainBeingVerified?.verification_challenge}
      - + {domainBeingVerified && ( + + )}
    diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts index ecddf961652d7..6149a36e9f6d7 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts @@ -1,9 +1,11 @@ -import { isSecureURL, verifiedDomainsLogic } from './verifiedDomainsLogic' -import { initKeaTests } from '~/test/init' +import { expectLogic } from 'kea-test-utils' + import { useAvailableFeatures } from '~/mocks/features' -import { AvailableFeature } from '~/types' import { useMocks } from '~/mocks/jest' -import { expectLogic } from 'kea-test-utils' +import { initKeaTests } from '~/test/init' +import { AvailableFeature } from '~/types' + +import { isSecureURL, verifiedDomainsLogic } from './verifiedDomainsLogic' describe('verifiedDomainsLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts index 39746e2109b3b..fbc0a829bc5ba 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts @@ -1,12 +1,14 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' import api from 'lib/api' -import { lemonToast } from 'lib/lemon-ui/lemonToast' import { SECURE_URL_REGEX } from 'lib/constants' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { organizationLogic } from 'scenes/organizationLogic' -import { OrganizationDomainType, AvailableFeature } from '~/types' + +import { AvailableFeature, OrganizationDomainType } from '~/types' + import type { verifiedDomainsLogicType } from './verifiedDomainsLogicType' -import { forms } from 'kea-forms' -import { loaders } from 'kea-loaders' export type OrganizationDomainUpdatePayload = Partial< Pick @@ -109,7 +111,7 @@ export const verifiedDomainsLogic = kea([ 'We could not verify your domain yet. DNS propagation may take up to 72 hours. Please try again later.' ) } - actions.replaceDomain(response as OrganizationDomainType) + actions.replaceDomain(response) actions.setVerifyModal(null) return false }, diff --git a/frontend/src/scenes/settings/organization/inviteLogic.ts b/frontend/src/scenes/settings/organization/inviteLogic.ts index 8ebc6a1a72ef3..32865643d3a21 100644 --- a/frontend/src/scenes/settings/organization/inviteLogic.ts +++ b/frontend/src/scenes/settings/organization/inviteLogic.ts @@ -1,13 +1,15 @@ +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import { OrganizationInviteType } from '~/types' +import { router, urlToAction } from 'kea-router' import api from 'lib/api' -import { organizationLogic } from 'scenes/organizationLogic' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import type { inviteLogicType } from './inviteLogicType' +import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { router, urlToAction } from 'kea-router' -import { lemonToast } from 'lib/lemon-ui/lemonToast' + +import { OrganizationInviteType } from '~/types' + +import type { inviteLogicType } from './inviteLogicType' /** State of a single invite row (with input data) in bulk invite creation. */ export interface InviteRowState { diff --git a/frontend/src/scenes/settings/organization/invitesLogic.tsx b/frontend/src/scenes/settings/organization/invitesLogic.tsx index 1b119f9a52da9..0cb80e1dacca3 100644 --- a/frontend/src/scenes/settings/organization/invitesLogic.tsx +++ b/frontend/src/scenes/settings/organization/invitesLogic.tsx @@ -1,11 +1,13 @@ +import { events, kea, listeners, path } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, listeners, events } from 'kea' import api from 'lib/api' -import { OrganizationInviteType } from '~/types' -import type { invitesLogicType } from './invitesLogicType' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { lemonToast } from 'lib/lemon-ui/lemonToast' + +import { OrganizationInviteType } from '~/types' + +import type { invitesLogicType } from './invitesLogicType' export const invitesLogic = kea([ path(['scenes', 'organization', 'Settings', 'invitesLogic']), @@ -44,7 +46,7 @@ export const invitesLogic = kea([ }, })), listeners({ - createInviteSuccess: async () => { + createInviteSuccess: () => { const nameProvided = false // TODO: Change when adding support for names on invites eventUsageLogic.actions.reportInviteAttempted( nameProvided, diff --git a/frontend/src/scenes/settings/project/AddMembersModal.tsx b/frontend/src/scenes/settings/project/AddMembersModal.tsx index 65ca193f8bb00..b049fcb372fde 100644 --- a/frontend/src/scenes/settings/project/AddMembersModal.tsx +++ b/frontend/src/scenes/settings/project/AddMembersModal.tsx @@ -1,16 +1,17 @@ -import { useState } from 'react' +import { LemonButton, LemonModal, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { teamMembersLogic } from './teamMembersLogic' -import { teamLogic } from 'scenes/teamLogic' -import { membershipLevelToName, teamMembershipLevelIntegers } from 'lib/utils/permissioning' +import { Form } from 'kea-forms' import { RestrictedComponentProps } from 'lib/components/RestrictedArea' -import { LemonButton, LemonModal, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' -import { Form } from 'kea-forms' +import { TeamMembershipLevel } from 'lib/constants' import { Field } from 'lib/forms/Field' import { IconPlus } from 'lib/lemon-ui/icons' -import { TeamMembershipLevel } from 'lib/constants' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { membershipLevelToName, teamMembershipLevelIntegers } from 'lib/utils/permissioning' +import { useState } from 'react' +import { teamLogic } from 'scenes/teamLogic' + +import { teamMembersLogic } from './teamMembersLogic' export function AddMembersModalWithButton({ isRestricted }: RestrictedComponentProps): JSX.Element { const { addableMembers, allMembersLoading } = useValues(teamMembersLogic) diff --git a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx index e65c24e6f76b5..8e155bbb77323 100644 --- a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx +++ b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx @@ -1,17 +1,18 @@ -import { useValues, useActions } from 'kea' -import { userLogic } from 'scenes/userLogic' import { LemonSwitch, LemonTag, LemonTextArea, Link } from '@posthog/lemon-ui' -import { teamLogic } from 'scenes/teamLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import clsx from 'clsx' +import { useActions, useValues } from 'kea' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + import { autocaptureExceptionsLogic } from './autocaptureExceptionsLogic' export function AutocaptureSettings(): JSX.Element { const { userLoading } = useValues(userLogic) const { currentTeam } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) - const { reportIngestionAutocaptureToggled } = useActions(eventUsageLogic) + const { reportAutocaptureToggled } = useActions(eventUsageLogic) return ( <> @@ -33,7 +34,7 @@ export function AutocaptureSettings(): JSX.Element { updateCurrentTeam({ autocapture_opt_out: !checked, }) - reportIngestionAutocaptureToggled(!checked) + reportAutocaptureToggled(!checked) }} checked={!currentTeam?.autocapture_opt_out} disabled={userLoading} @@ -49,7 +50,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element { const { userLoading } = useValues(userLogic) const { currentTeam } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) - const { reportIngestionAutocaptureExceptionsToggled } = useActions(eventUsageLogic) + const { reportAutocaptureExceptionsToggled } = useActions(eventUsageLogic) const { errorsToIgnoreRules, rulesCharacters } = useValues(autocaptureExceptionsLogic) const { setErrorsToIgnoreRules } = useActions(autocaptureExceptionsLogic) @@ -62,7 +63,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element { updateCurrentTeam({ autocapture_exceptions_opt_in: checked, }) - reportIngestionAutocaptureExceptionsToggled(checked) + reportAutocaptureExceptionsToggled(checked) }} checked={!!currentTeam?.autocapture_exceptions_opt_in} disabled={userLoading} @@ -81,7 +82,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element {

    You can enter a regular expression that matches values of{' '} here to ignore them. One per line. For example, if you - want to drop all errors that contain the word "bot", or you can enter "bot" here. Or if you want to drop + want to drop all errors that contain the word "bot", you can enter "bot" here. Or if you want to drop all errors that are exactly "bot", you can enter "^bot$".

    Only up to 300 characters of config are allowed here.

    diff --git a/frontend/src/scenes/settings/project/CorrelationConfig.tsx b/frontend/src/scenes/settings/project/CorrelationConfig.tsx index 83bf547ca3913..fd2af0493a6e1 100644 --- a/frontend/src/scenes/settings/project/CorrelationConfig.tsx +++ b/frontend/src/scenes/settings/project/CorrelationConfig.tsx @@ -1,11 +1,11 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' import { EventSelect } from 'lib/components/EventSelect/EventSelect' +import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' import { IconPlus, IconSelectEvents, IconSelectProperties } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' -import { LemonButton } from '@posthog/lemon-ui' +import { teamLogic } from 'scenes/teamLogic' export function CorrelationConfig(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) diff --git a/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx b/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx index ea893db7132b7..061c84cdb764e 100644 --- a/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx +++ b/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx @@ -1,9 +1,11 @@ +import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { GroupType } from '~/types' -import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' -import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' + +import { GroupType } from '~/types' + import { groupAnalyticsConfigLogic } from './groupAnalyticsConfigLogic' export function GroupAnalyticsConfig(): JSX.Element | null { diff --git a/frontend/src/scenes/settings/project/IPCapture.tsx b/frontend/src/scenes/settings/project/IPCapture.tsx index 07311b4c58e7a..7b12e6b062fe3 100644 --- a/frontend/src/scenes/settings/project/IPCapture.tsx +++ b/frontend/src/scenes/settings/project/IPCapture.tsx @@ -1,6 +1,6 @@ +import { LemonSwitch } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' -import { LemonSwitch } from '@posthog/lemon-ui' export function IPCapture(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) diff --git a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx index 076ac078020a0..c18cc609e07af 100644 --- a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx +++ b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx @@ -1,10 +1,11 @@ +import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' -import { Link } from '@posthog/lemon-ui' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' + import { AvailableFeature, InsightType } from '~/types' -import { urls } from 'scenes/urls' export function PathCleaningFiltersConfig(): JSX.Element | null { const { updateCurrentTeam } = useActions(teamLogic) diff --git a/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx b/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx index df85da1d32db3..14a070b3e66d9 100644 --- a/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx +++ b/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx @@ -1,8 +1,8 @@ import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { PERSON_DEFAULT_DISPLAY_NAME_PROPERTIES } from 'lib/constants' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { useEffect, useState } from 'react' import { teamLogic } from 'scenes/teamLogic' @@ -17,7 +17,7 @@ export function PersonDisplayNameProperties(): JSX.Element { ) if (!currentTeam) { - return + return } return ( diff --git a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx index edfdb92742a04..b34d3a52fd94c 100644 --- a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx +++ b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx @@ -1,27 +1,29 @@ -import { useValues, useActions } from 'kea' -import { MINIMUM_IMPLICIT_ACCESS_LEVEL, teamMembersLogic } from './teamMembersLogic' // eslint-disable-next-line no-restricted-imports -import { CloseCircleOutlined, LogoutOutlined, CrownFilled } from '@ant-design/icons' -import { humanFriendlyDetailedTime } from 'lib/utils' +import { CloseCircleOutlined, CrownFilled, LogoutOutlined } from '@ant-design/icons' +import { LemonButton, LemonSelect, LemonSelectOption, LemonSwitch, LemonTable } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { RestrictedArea, RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' import { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' -import { FusedTeamMemberType, AvailableFeature } from '~/types' -import { userLogic } from 'scenes/userLogic' +import { IconLock, IconLockOpen } from 'lib/lemon-ui/icons' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { humanFriendlyDetailedTime } from 'lib/utils' import { getReasonForAccessLevelChangeProhibition, membershipLevelToName, teamMembershipLevelIntegers, } from 'lib/utils/permissioning' -import { AddMembersModalWithButton } from './AddMembersModal' -import { RestrictedArea, RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' -import { LemonButton, LemonSelect, LemonSelectOption, LemonSwitch, LemonTable } from '@posthog/lemon-ui' -import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { organizationLogic } from 'scenes/organizationLogic' import { sceneLogic } from 'scenes/sceneLogic' -import { IconLock, IconLockOpen } from 'lib/lemon-ui/icons' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature, FusedTeamMemberType } from '~/types' + +import { AddMembersModalWithButton } from './AddMembersModal' +import { MINIMUM_IMPLICIT_ACCESS_LEVEL, teamMembersLogic } from './teamMembersLogic' function LevelComponent(member: FusedTeamMemberType): JSX.Element | null { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/project/ProjectDangerZone.tsx b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx index 82dd00ba309ef..c9807560c1c23 100644 --- a/frontend/src/scenes/settings/project/ProjectDangerZone.tsx +++ b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx @@ -1,11 +1,12 @@ -import { Dispatch, SetStateAction, useState } from 'react' +import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' -import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' +import { OrganizationMembershipLevel } from 'lib/constants' import { IconDelete } from 'lib/lemon-ui/icons' +import { Dispatch, SetStateAction, useState } from 'react' +import { teamLogic } from 'scenes/teamLogic' + import { TeamType } from '~/types' -import { OrganizationMembershipLevel } from 'lib/constants' export function DeleteProjectModal({ isOpen, diff --git a/frontend/src/scenes/settings/project/ProjectSettings.tsx b/frontend/src/scenes/settings/project/ProjectSettings.tsx index ac6d40c2849a5..9bb788431fa31 100644 --- a/frontend/src/scenes/settings/project/ProjectSettings.tsx +++ b/frontend/src/scenes/settings/project/ProjectSettings.tsx @@ -1,17 +1,18 @@ +import { urls } from '@posthog/apps-common' +import { LemonButton, LemonInput, LemonLabel, LemonSkeleton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' -import { JSSnippet } from 'lib/components/JSSnippet' -import { JSBookmarklet } from 'lib/components/JSBookmarklet' +import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' +import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' import { CodeSnippet } from 'lib/components/CodeSnippet' +import { JSBookmarklet } from 'lib/components/JSBookmarklet' +import { JSSnippet } from 'lib/components/JSSnippet' import { IconRefresh } from 'lib/lemon-ui/icons' import { Link } from 'lib/lemon-ui/Link' -import { LemonButton, LemonInput, LemonLabel, LemonSkeleton } from '@posthog/lemon-ui' import { useState } from 'react' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' + import { TimezoneConfig } from './TimezoneConfig' import { WeekStartConfig } from './WeekStartConfig' -import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' -import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { urls } from '@posthog/apps-common' export function ProjectDisplayName(): JSX.Element { const { currentTeam, currentTeamLoading } = useValues(teamLogic) @@ -58,7 +59,7 @@ export function WebSnippet(): JSX.Element {

    {currentTeamLoading && !currentTeam ? (
    - +
    ) : ( diff --git a/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx index e16f1167ceebd..44ff966db0c65 100644 --- a/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx +++ b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx @@ -1,14 +1,14 @@ -import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { LemonBanner, LemonButton, LemonSelect, LemonSwitch, Link } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' +import { useActions, useValues } from 'kea' import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FlagSelector } from 'lib/components/FlagSelector' import { FEATURE_FLAGS } from 'lib/constants' import { IconCancel } from 'lib/lemon-ui/icons' -import { FlagSelector } from 'lib/components/FlagSelector' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' export function ReplayGeneral(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) diff --git a/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx b/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx index 031def3241109..369f0803c1994 100644 --- a/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx +++ b/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx @@ -1,6 +1,6 @@ +import { LemonSwitch, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' -import { LemonSwitch, Link } from '@posthog/lemon-ui' export function SettingPersonsOnEvents(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) diff --git a/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx b/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx index aa21717e2cbf5..51a240d83f2a4 100644 --- a/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx +++ b/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx @@ -1,8 +1,10 @@ import { Meta } from '@storybook/react' -import { AvailableFeature } from '~/types' -import { useAvailableFeatures } from '~/mocks/features' + import { useStorybookMocks } from '~/mocks/browser' +import { useAvailableFeatures } from '~/mocks/features' import { mockIntegration } from '~/test/mocks' +import { AvailableFeature } from '~/types' + import { SlackIntegration } from './SlackIntegration' const meta: Meta = { diff --git a/frontend/src/scenes/settings/project/SlackIntegration.tsx b/frontend/src/scenes/settings/project/SlackIntegration.tsx index e74ef84a8fa01..1f40b80e25281 100644 --- a/frontend/src/scenes/settings/project/SlackIntegration.tsx +++ b/frontend/src/scenes/settings/project/SlackIntegration.tsx @@ -1,13 +1,14 @@ -import { useState } from 'react' +import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { getSlackAppManifest, integrationsLogic } from './integrationsLogic' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { LemonButton, Link } from '@posthog/lemon-ui' -import { IconDelete, IconSlack } from 'lib/lemon-ui/icons' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' +import { IconDelete, IconSlack } from 'lib/lemon-ui/icons' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { useState } from 'react' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' + +import { getSlackAppManifest, integrationsLogic } from './integrationsLogic' export function SlackIntegration(): JSX.Element { const { slackIntegration, addToSlackButtonUrl } = useValues(integrationsLogic) diff --git a/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx b/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx index 58cd23f6288e1..28ddaf24fad76 100644 --- a/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx +++ b/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx @@ -1,12 +1,14 @@ +import { LemonSwitch, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { teamLogic } from 'scenes/teamLogic' -import { AnyPropertyFilter } from '~/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' + import { groupsModel } from '~/models/groupsModel' -import { LemonSwitch, Link } from '@posthog/lemon-ui' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { AnyPropertyFilter } from '~/types' + import { filterTestAccountsDefaultsLogic } from './filterTestAccountDefaultsLogic' function TestAccountFiltersConfig(): JSX.Element { diff --git a/frontend/src/scenes/settings/project/TimezoneConfig.tsx b/frontend/src/scenes/settings/project/TimezoneConfig.tsx index 09d7c8a70c026..f596f162115df 100644 --- a/frontend/src/scenes/settings/project/TimezoneConfig.tsx +++ b/frontend/src/scenes/settings/project/TimezoneConfig.tsx @@ -1,10 +1,9 @@ import { useActions, useValues } from 'kea' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { teamLogic } from 'scenes/teamLogic' - -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { teamLogic } from 'scenes/teamLogic' const tzLabel = (tz: string, offset: number): string => `${tz.replace(/\//g, ' / ').replace(/_/g, ' ')} (UTC${offset === 0 ? '±' : offset > 0 ? '+' : '-'}${Math.abs( @@ -17,7 +16,7 @@ export function TimezoneConfig(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) if (!preflight?.available_timezones || !currentTeam) { - return + return } const options = Object.entries(preflight.available_timezones).map(([tz, offset]) => ({ key: tz, diff --git a/frontend/src/scenes/settings/project/WebhookIntegration.tsx b/frontend/src/scenes/settings/project/WebhookIntegration.tsx index f2fae688f7340..d17cfba9cd639 100644 --- a/frontend/src/scenes/settings/project/WebhookIntegration.tsx +++ b/frontend/src/scenes/settings/project/WebhookIntegration.tsx @@ -1,11 +1,12 @@ -import { useEffect, useState } from 'react' +import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { useEffect, useState } from 'react' import { teamLogic } from 'scenes/teamLogic' + import { webhookIntegrationLogic } from './webhookIntegrationLogic' -import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import { supportLogic } from 'lib/components/Support/supportLogic' export function WebhookIntegration(): JSX.Element { const [webhook, setWebhook] = useState('') diff --git a/frontend/src/scenes/settings/project/WeekStartConfig.tsx b/frontend/src/scenes/settings/project/WeekStartConfig.tsx index 4a00d35729c23..a4577b16a703d 100644 --- a/frontend/src/scenes/settings/project/WeekStartConfig.tsx +++ b/frontend/src/scenes/settings/project/WeekStartConfig.tsx @@ -1,7 +1,7 @@ -import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { LemonSelect } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { teamLogic } from 'scenes/teamLogic' export function WeekStartConfig(): JSX.Element { const { currentTeam, currentTeamLoading } = useValues(teamLogic) diff --git a/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts b/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts index 9aa7a465bdb0c..b9be34b208c50 100644 --- a/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts +++ b/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts @@ -1,4 +1,4 @@ -import { kea, reducers, actions, listeners, selectors, connect, path, afterMount } from 'kea' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { teamLogic } from 'scenes/teamLogic' import type { autocaptureExceptionsLogicType } from './autocaptureExceptionsLogicType' diff --git a/frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts b/frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts index ba3fee5cb8a17..0616394c5ef24 100644 --- a/frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts +++ b/frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts @@ -1,4 +1,4 @@ -import { kea, path, reducers, actions, listeners, events, selectors, connect } from 'kea' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' import { teamLogic } from 'scenes/teamLogic' import type { filterTestAccountsDefaultsLogicType } from './filterTestAccountDefaultsLogicType' diff --git a/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts b/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts index 89a3029b4d47e..3edfe58f5b7d1 100644 --- a/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts +++ b/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts @@ -1,5 +1,7 @@ -import { kea, path, connect, actions, reducers, selectors, listeners } from 'kea' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' + import { groupsModel } from '~/models/groupsModel' + import type { groupAnalyticsConfigLogicType } from './groupAnalyticsConfigLogicType' export const groupAnalyticsConfigLogic = kea([ @@ -40,7 +42,7 @@ export const groupAnalyticsConfigLogic = kea([ ], }), listeners(({ values, actions }) => ({ - save: async () => { + save: () => { const { groupTypes, singularChanges, pluralChanges } = values const payload = Array.from(groupTypes.values()).map((groupType) => { const result = { ...groupType } diff --git a/frontend/src/scenes/settings/project/integrationsLogic.ts b/frontend/src/scenes/settings/project/integrationsLogic.ts index be287440db304..55c90ac929b79 100644 --- a/frontend/src/scenes/settings/project/integrationsLogic.ts +++ b/frontend/src/scenes/settings/project/integrationsLogic.ts @@ -1,10 +1,11 @@ import { lemonToast } from '@posthog/lemon-ui' -import { kea, path, listeners, selectors, connect, afterMount, actions } from 'kea' +import { actions, afterMount, connect, kea, listeners, path, selectors } from 'kea' import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' import api from 'lib/api' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { urls } from 'scenes/urls' + import { IntegrationType, SlackChannelType } from '~/types' import type { integrationsLogicType } from './integrationsLogicType' diff --git a/frontend/src/scenes/settings/project/teamMembersLogic.tsx b/frontend/src/scenes/settings/project/teamMembersLogic.tsx index d6d137b48a2be..f9457bca55b00 100644 --- a/frontend/src/scenes/settings/project/teamMembersLogic.tsx +++ b/frontend/src/scenes/settings/project/teamMembersLogic.tsx @@ -1,8 +1,12 @@ -import { kea, path, actions, selectors, listeners, events } from 'kea' -import { loaders } from 'kea-loaders' +import { actions, events, kea, listeners, path, selectors } from 'kea' import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' import api from 'lib/api' import { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { membershipLevelToName } from 'lib/utils/permissioning' +import { membersLogic } from 'scenes/organization/membersLogic' + import { AvailableFeature, BaseMemberType, @@ -12,12 +16,10 @@ import { UserBasicType, UserType, } from '~/types' -import type { teamMembersLogicType } from './teamMembersLogicType' -import { membershipLevelToName } from 'lib/utils/permissioning' -import { userLogic } from '../../userLogic' + import { teamLogic } from '../../teamLogic' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { membersLogic } from 'scenes/organization/membersLogic' +import { userLogic } from '../../userLogic' +import type { teamMembersLogicType } from './teamMembersLogicType' export const MINIMUM_IMPLICIT_ACCESS_LEVEL = OrganizationMembershipLevel.Admin diff --git a/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts b/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts index 77cf7e1a69180..b9df2b8de3b72 100644 --- a/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts +++ b/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts @@ -1,9 +1,10 @@ +import { kea, listeners, path, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, selectors, listeners } from 'kea' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { capitalizeFirstLetter } from 'lib/utils' import { teamLogic } from 'scenes/teamLogic' + import type { webhookIntegrationLogicType } from './webhookIntegrationLogicType' function adjustDiscordWebhook(webhookUrl: string): string { @@ -55,7 +56,7 @@ export const webhookIntegrationLogic = kea([ ], }), listeners(() => ({ - testWebhookSuccess: async ({ testedWebhook }) => { + testWebhookSuccess: ({ testedWebhook }) => { if (testedWebhook) { teamLogic.actions.updateCurrentTeam({ slack_incoming_webhook: testedWebhook }) } diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index 665b7d27a4c7f..8af9bb55f8475 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -1,23 +1,12 @@ import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { SettingsMap } from './SettingsMap' - -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' -import { SettingSection, Setting, SettingSectionId, SettingLevelId, SettingId } from './types' - -import type { settingsLogicType } from './settingsLogicType' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { urls } from 'scenes/urls' -import { copyToClipboard } from 'lib/utils' -export type SettingsLogicProps = { - logicKey?: string - // Optional - if given, renders only the given level - settingLevelId?: SettingLevelId - // Optional - if given, renders only the given section - sectionId?: SettingSectionId - // Optional - if given, renders only the given setting - settingId?: SettingId -} +import type { settingsLogicType } from './settingsLogicType' +import { SettingsMap } from './SettingsMap' +import { Setting, SettingId, SettingLevelId, SettingSection, SettingSectionId, SettingsLogicProps } from './types' export const settingsLogic = kea([ props({} as SettingsLogicProps), @@ -94,10 +83,9 @@ export const settingsLogic = kea([ }), listeners(({ values }) => ({ - selectSetting({ setting }) { + async selectSetting({ setting }) { const url = urls.settings(values.selectedSectionId ?? values.selectedLevel, setting as SettingId) - - copyToClipboard(window.location.origin + url) + await copyToClipboard(window.location.origin + url) }, })), ]) diff --git a/frontend/src/scenes/settings/settingsSceneLogic.ts b/frontend/src/scenes/settings/settingsSceneLogic.ts index ecd2b85d06e3f..9339ee8b1274e 100644 --- a/frontend/src/scenes/settings/settingsSceneLogic.ts +++ b/frontend/src/scenes/settings/settingsSceneLogic.ts @@ -1,15 +1,16 @@ import { connect, kea, path, selectors } from 'kea' -import { SettingsMap } from './SettingsMap' - -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { actionToUrl, urlToAction } from 'kea-router' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { capitalizeFirstLetter } from 'lib/utils' +import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + import { Breadcrumb } from '~/types' -import { capitalizeFirstLetter } from 'lib/utils' -import { SettingSectionId, SettingLevelId, SettingLevelIds } from './types' -import type { settingsSceneLogicType } from './settingsSceneLogicType' import { settingsLogic } from './settingsLogic' +import { SettingsMap } from './SettingsMap' +import type { settingsSceneLogicType } from './settingsSceneLogicType' +import { SettingLevelId, SettingLevelIds, SettingSectionId } from './types' export const settingsSceneLogic = kea([ path(['scenes', 'settings', 'settingsSceneLogic']), @@ -28,10 +29,12 @@ export const settingsSceneLogic = kea([ (s) => [s.selectedLevel, s.selectedSectionId, s.sections], (selectedLevel, selectedSectionId): Breadcrumb[] => [ { + key: Scene.Settings, name: `Settings`, path: urls.settings('project'), }, { + key: selectedSectionId || selectedLevel, name: selectedSectionId ? SettingsMap.find((x) => x.id === selectedSectionId)?.title : capitalizeFirstLetter(selectedLevel), diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index 30ee8324d0ebe..038da8dd71126 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -1,5 +1,14 @@ -import { FEATURE_FLAGS } from 'lib/constants' -import { EitherMembershipLevel } from 'lib/utils/permissioning' +import { EitherMembershipLevel, FEATURE_FLAGS } from 'lib/constants' + +export type SettingsLogicProps = { + logicKey?: string + // Optional - if given, renders only the given level + settingLevelId?: SettingLevelId + // Optional - if given, renders only the given section + sectionId?: SettingSectionId + // Optional - if given, renders only the given setting + settingId?: SettingId +} export type SettingLevelId = 'user' | 'project' | 'organization' export const SettingLevelIds: SettingLevelId[] = ['project', 'organization', 'user'] diff --git a/frontend/src/scenes/settings/user/ChangePassword.tsx b/frontend/src/scenes/settings/user/ChangePassword.tsx index b7cb1139c40b4..89007c6ecdd5d 100644 --- a/frontend/src/scenes/settings/user/ChangePassword.tsx +++ b/frontend/src/scenes/settings/user/ChangePassword.tsx @@ -1,8 +1,9 @@ +import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { useValues } from 'kea' import { Form } from 'kea-forms' -import { Field } from 'lib/forms/Field' -import { LemonButton, LemonInput } from '@posthog/lemon-ui' import PasswordStrength from 'lib/components/PasswordStrength' +import { Field } from 'lib/forms/Field' + import { changePasswordLogic } from './changePasswordLogic' export function ChangePassword(): JSX.Element { diff --git a/frontend/src/scenes/settings/user/OptOutCapture.tsx b/frontend/src/scenes/settings/user/OptOutCapture.tsx index 4290928ebb5d1..da5bb75147877 100644 --- a/frontend/src/scenes/settings/user/OptOutCapture.tsx +++ b/frontend/src/scenes/settings/user/OptOutCapture.tsx @@ -1,6 +1,6 @@ +import { LemonSwitch } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { userLogic } from 'scenes/userLogic' -import { LemonSwitch } from '@posthog/lemon-ui' export function OptOutCapture(): JSX.Element { const { user, userLoading } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx index f9f79327e1ba4..a618450c8de94 100644 --- a/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx +++ b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx @@ -1,13 +1,15 @@ -import { useState, useCallback, Dispatch, SetStateAction, useEffect } from 'react' +import { LemonDialog, LemonInput, LemonModal, LemonTable, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { personalAPIKeysLogic } from './personalAPIKeysLogic' -import { PersonalAPIKeyType } from '~/types' +import { IconPlus } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { humanFriendlyDetailedTime } from 'lib/utils' +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' + +import { PersonalAPIKeyType } from '~/types' + import { CopyToClipboardInline } from '../../../lib/components/CopyToClipboard' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDialog, LemonInput, LemonModal, LemonTable, Link } from '@posthog/lemon-ui' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { IconPlus } from 'lib/lemon-ui/icons' +import { personalAPIKeysLogic } from './personalAPIKeysLogic' function CreateKeyModal({ isOpen, @@ -90,7 +92,9 @@ function PersonalAPIKeysTable(): JSX.Element { dataIndex: 'value', render: function RenderValue(value) { return value ? ( - {`${value}`} + + {String(value)} + ) : ( secret ) diff --git a/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx index a7d71f1bb3d74..4d6333b16fbfb 100644 --- a/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx +++ b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx @@ -1,10 +1,10 @@ -import { useValues, useActions } from 'kea' -import { userLogic } from 'scenes/userLogic' import { LemonButton, LemonModal } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { IconCheckmark, IconWarning } from 'lib/lemon-ui/icons' import { useState } from 'react' import { Setup2FA } from 'scenes/authentication/Setup2FA' import { membersLogic } from 'scenes/organization/membersLogic' +import { userLogic } from 'scenes/userLogic' export function TwoFactorAuthentication(): JSX.Element { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx index 5d1387d376324..d9d9abfc98f20 100644 --- a/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx +++ b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx @@ -1,6 +1,6 @@ -import { useValues, useActions } from 'kea' -import { userLogic } from 'scenes/userLogic' import { LemonSwitch } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { userLogic } from 'scenes/userLogic' export function UpdateEmailPreferences(): JSX.Element { const { user, userLoading } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/user/UserDetails.tsx b/frontend/src/scenes/settings/user/UserDetails.tsx index 324e70bc2818d..0ad4587948d61 100644 --- a/frontend/src/scenes/settings/user/UserDetails.tsx +++ b/frontend/src/scenes/settings/user/UserDetails.tsx @@ -1,10 +1,10 @@ +import { LemonTag } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { userLogic } from 'scenes/userLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Form } from 'kea-forms' import { Field } from 'lib/forms/Field' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { Form } from 'kea-forms' -import { LemonTag } from '@posthog/lemon-ui' +import { userLogic } from 'scenes/userLogic' export function UserDetails(): JSX.Element { const { userLoading, isUserDetailsSubmitting, userDetailsChanged, user } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/user/changePasswordLogic.ts b/frontend/src/scenes/settings/user/changePasswordLogic.ts index 99053bdf8da10..4b04c0b7452aa 100644 --- a/frontend/src/scenes/settings/user/changePasswordLogic.ts +++ b/frontend/src/scenes/settings/user/changePasswordLogic.ts @@ -1,5 +1,5 @@ import { lemonToast } from '@posthog/lemon-ui' -import { kea, path, connect } from 'kea' +import { connect, kea, path } from 'kea' import { forms } from 'kea-forms' import api from 'lib/api' import { userLogic } from 'scenes/userLogic' diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts index f500ebe5e0553..7b04396e5079d 100644 --- a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts @@ -1,10 +1,12 @@ +import { kea, listeners, path } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, listeners } from 'kea' import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { copyToClipboard } from 'lib/utils/copyToClipboard' + import { PersonalAPIKeyType } from '~/types' + import type { personalAPIKeysLogicType } from './personalAPIKeysLogicType' -import { copyToClipboard } from 'lib/utils' -import { lemonToast } from 'lib/lemon-ui/lemonToast' export const personalAPIKeysLogic = kea([ path(['lib', 'components', 'PersonalAPIKeys', 'personalAPIKeysLogic']), @@ -24,7 +26,7 @@ export const personalAPIKeysLogic = kea([ }, deleteKey: async (key: PersonalAPIKeyType) => { await api.delete(`api/personal_api_keys/${key.id}/`) - return (values.keys as PersonalAPIKeyType[]).filter((filteredKey) => filteredKey.id != key.id) + return values.keys.filter((filteredKey) => filteredKey.id != key.id) }, }, ], diff --git a/frontend/src/scenes/sites/Site.tsx b/frontend/src/scenes/sites/Site.tsx index ad487e77da371..4e97669011639 100644 --- a/frontend/src/scenes/sites/Site.tsx +++ b/frontend/src/scenes/sites/Site.tsx @@ -1,8 +1,10 @@ -import { SceneExport } from 'scenes/sceneTypes' import './Site.scss' -import { SiteLogicProps, siteLogic } from './siteLogic' -import { AuthorizedUrlListType, authorizedUrlListLogic } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' + import { useValues } from 'kea' +import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { SceneExport } from 'scenes/sceneTypes' + +import { siteLogic, SiteLogicProps } from './siteLogic' export const scene: SceneExport = { component: Site, diff --git a/frontend/src/scenes/sites/siteLogic.ts b/frontend/src/scenes/sites/siteLogic.ts index 7476f4eb51ab9..48a318edde6b1 100644 --- a/frontend/src/scenes/sites/siteLogic.ts +++ b/frontend/src/scenes/sites/siteLogic.ts @@ -1,4 +1,6 @@ -import { kea, props, selectors, path } from 'kea' +import { kea, path, props, selectors } from 'kea' +import { Scene } from 'scenes/sceneTypes' + import { Breadcrumb } from '~/types' import type { siteLogicType } from './siteLogicType' @@ -15,9 +17,11 @@ export const siteLogic = kea([ (_, p) => [p.url], (url): Breadcrumb[] => [ { + key: Scene.Site, name: `Site`, }, { + key: url, name: url, }, ], diff --git a/frontend/src/scenes/surveys/Survey.tsx b/frontend/src/scenes/surveys/Survey.tsx index 30ed3fe5e4e34..12bebeb2a9c4a 100644 --- a/frontend/src/scenes/surveys/Survey.tsx +++ b/frontend/src/scenes/surveys/Survey.tsx @@ -1,20 +1,22 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { surveyLogic } from './surveyLogic' +import { LemonButton, LemonDivider, Link } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' import { Form } from 'kea-forms' +import { router } from 'kea-router' +import { FlagSelector } from 'lib/components/FlagSelector' +import { NotFound } from 'lib/components/NotFound' import { PageHeader } from 'lib/components/PageHeader' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonButton, LemonDivider, Link } from '@posthog/lemon-ui' -import { router } from 'kea-router' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + import { Survey, SurveyUrlMatchType } from '~/types' -import { SurveyView } from './SurveyView' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' + import { NewSurvey, SurveyUrlMatchTypeLabels } from './constants' -import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' import SurveyEdit from './SurveyEdit' -import { NotFound } from 'lib/components/NotFound' -import { FlagSelector } from 'lib/components/FlagSelector' +import { surveyLogic } from './surveyLogic' +import { SurveyView } from './SurveyView' export const scene: SceneExport = { component: SurveyComponent, diff --git a/frontend/src/scenes/surveys/SurveyAPIEditor.tsx b/frontend/src/scenes/surveys/SurveyAPIEditor.tsx index c474d973db1a9..f0c2b9190f77b 100644 --- a/frontend/src/scenes/surveys/SurveyAPIEditor.tsx +++ b/frontend/src/scenes/surveys/SurveyAPIEditor.tsx @@ -1,6 +1,8 @@ +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + import { Survey } from '~/types' + import { NewSurvey } from './constants' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' export function SurveyAPIEditor({ survey }: { survey: Survey | NewSurvey }): JSX.Element { // Make sure this is synced to SurveyAPISerializer diff --git a/frontend/src/scenes/surveys/SurveyAppearance.scss b/frontend/src/scenes/surveys/SurveyAppearance.scss index 640e99ec703e0..32ee384fc1d83 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.scss +++ b/frontend/src/scenes/surveys/SurveyAppearance.scss @@ -1,12 +1,12 @@ .survey-form { - margin: 0px; + margin: 0; color: black; font-weight: normal; - font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', Helvetica, Arial, sans-serif, + font-family: -apple-system, BlinkMacSystemFont, Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; text-align: left; width: 320px; - border-bottom: 0px; + border-bottom: 0; flex-direction: column; box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); border-radius: 10px; @@ -14,15 +14,16 @@ position: relative; box-sizing: border-box; } + .form-submit[disabled] { opacity: 0.6; filter: grayscale(100%); cursor: not-allowed; } + .survey-form textarea { - color: #2d2d2d; font-size: 14px; - font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', Helvetica, Arial, sans-serif, + font-family: -apple-system, BlinkMacSystemFont, Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; background: white; color: black; @@ -33,6 +34,7 @@ border-radius: 6px; margin-top: 14px; } + .form-submit { box-sizing: border-box; margin: 0; @@ -52,16 +54,18 @@ font-size: 14px; border-radius: 6px; outline: 0; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); - box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); + text-shadow: 0 -1px 0 rgb(0 0 0 / 12%); + box-shadow: 0 2px 0 rgb(0 0 0 / 4.5%); width: 100%; } + .form-cancel { float: right; border: none; background: none; cursor: pointer; } + .cancel-btn-wrapper { position: absolute; width: 35px; @@ -75,13 +79,16 @@ justify-content: center; align-items: center; } + .bolded { font-weight: 600; } + .buttons { display: flex; justify-content: center; } + .footer-branding { font-size: 11px; margin-top: 10px; @@ -94,59 +101,73 @@ text-decoration: none; color: inherit !important; } + .survey-box { padding: 20px 25px 10px; display: flex; flex-direction: column; } + .survey-question { font-weight: 500; font-size: 14px; } + .question-textarea-wrapper { display: flex; flex-direction: column; } + .description { font-size: 13px; margin-top: 5px; opacity: 0.6; } + .ratings-number { font-size: 14px; - padding: 8px 0px; + padding: 8px 0; border: none; } + .ratings-number:hover { cursor: pointer; } + .rating-options { margin-top: 14px; } + .rating-options-buttons { display: grid; border-radius: 6px; overflow: hidden; } + .rating-options-buttons > .ratings-number { border-right: 1px solid; } + .rating-options-buttons > .ratings-number:last-of-type { - border-right: 0px !important; + border-right: 0 !important; } + .rating-options-emoji { display: flex; justify-content: space-between; } + .ratings-emoji { font-size: 16px; background-color: transparent; border: none; - padding: 0px; + padding: 0; } + .ratings-emoji:hover { cursor: pointer; } + .rating-text { display: flex; flex-direction: row; @@ -155,11 +176,13 @@ margin-top: 6px; opacity: 0.6; } + .multiple-choice-options { margin-top: 13px; font-size: 14px; color: black; } + .multiple-choice-options .choice-option { display: flex; align-items: center; @@ -169,8 +192,9 @@ margin-bottom: 5px; position: relative; } + .multiple-choice-options > .choice-option:last-of-type { - margin-bottom: 0px; + margin-bottom: 0; } .multiple-choice-options input { @@ -181,41 +205,49 @@ height: 100%; inset: 0; } + .choice-check { position: absolute; right: 10px; background: white; } + .choice-check svg { display: none; } + .multiple-choice-options .choice-option:hover .choice-check svg { display: inline-block; opacity: 0.25; } + .multiple-choice-options input:checked + label + .choice-check svg { display: inline-block; - opacity: 100% !important; + opacity: 1 !important; } + .multiple-choice-options input[type='checkbox']:checked + label { font-weight: bold; } + .multiple-choice-options input:checked + label { - border: 1.5px solid rgba(0, 0, 0); + border: 1.5px solid rgb(0 0 0); } + .multiple-choice-options label { width: 100%; cursor: pointer; padding: 10px; - border: 1.5px solid rgba(0, 0, 0, 0.25); + border: 1.5px solid rgb(0 0 0 / 25%); border-radius: 4px; background: white; } + .thank-you-message { position: relative; - bottom: 0px; + bottom: 0; box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); - font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', Helvetica, Arial, sans-serif, + font-family: -apple-system, BlinkMacSystemFont, Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; border-radius: 10px; padding: 20px 25px 10px; @@ -225,23 +257,28 @@ line-height: 1.4; box-sizing: border-box; } + .thank-you-message-body { margin-top: 6px; font-size: 14px; } + .thank-you-message-header { - margin: 10px 0px 0px; + margin: 10px 0 0; font-weight: bold; font-size: 19px; color: inherit; } + .thank-you-message-container .form-submit { margin-top: 20px; margin-bottom: 10px; } + .thank-you-message-countdown { margin-left: 6px; } + .bottom-section { margin-top: 14px; } diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index b5563821908a3..c98574af938d9 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -1,15 +1,21 @@ import './SurveyAppearance.scss' + import { LemonButton, LemonCheckbox, LemonInput, Link } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import React, { useEffect, useRef, useState } from 'react' + import { - SurveyAppearance as SurveyAppearanceType, - SurveyQuestion, - RatingSurveyQuestion, - SurveyQuestionType, - MultipleSurveyQuestion, AvailableFeature, BasicSurveyQuestion, LinkSurveyQuestion, + MultipleSurveyQuestion, + RatingSurveyQuestion, + SurveyAppearance as SurveyAppearanceType, + SurveyQuestion, + SurveyQuestionType, } from '~/types' + import { defaultSurveyAppearance } from './constants' import { cancel, @@ -23,9 +29,6 @@ import { verySatisfiedEmoji, } from './SurveyAppearanceUtils' import { surveysLogic } from './surveysLogic' -import { useValues } from 'kea' -import React, { useEffect, useRef, useState } from 'react' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { sanitizeHTML } from './utils' interface SurveyAppearanceProps { @@ -104,7 +107,7 @@ export function SurveyAppearance({ surveyQuestionItem.type === SurveyQuestionType.MultipleChoice) && ( undefined} /> diff --git a/frontend/src/scenes/surveys/SurveyEdit.tsx b/frontend/src/scenes/surveys/SurveyEdit.tsx index 79a3db10bdb4f..64d2e0aad64ae 100644 --- a/frontend/src/scenes/surveys/SurveyEdit.tsx +++ b/frontend/src/scenes/surveys/SurveyEdit.tsx @@ -1,7 +1,5 @@ import './EditSurvey.scss' -import { SurveyEditSection, surveyLogic } from './surveyLogic' -import { BindLogic, useActions, useValues } from 'kea' -import { Group } from 'kea-forms' + import { LemonBanner, LemonButton, @@ -14,17 +12,37 @@ import { LemonTextArea, Link, } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { BindLogic, useActions, useValues } from 'kea' +import { Group } from 'kea-forms' +import { CodeEditor } from 'lib/components/CodeEditors' +import { FlagSelector } from 'lib/components/FlagSelector' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { FEATURE_FLAGS } from 'lib/constants' import { Field, PureField } from 'lib/forms/Field' +import { IconCancel, IconDelete, IconLock, IconPlus, IconPlusMini } from 'lib/lemon-ui/icons' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' +import React from 'react' +import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' +import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' + import { + AvailableFeature, + LinkSurveyQuestion, + RatingSurveyQuestion, SurveyQuestion, SurveyQuestionType, SurveyType, - LinkSurveyQuestion, - RatingSurveyQuestion, SurveyUrlMatchType, - AvailableFeature, } from '~/types' -import { IconCancel, IconDelete, IconLock, IconPlus, IconPlusMini } from 'lib/lemon-ui/icons' + +import { + defaultSurveyAppearance, + defaultSurveyFieldValues, + SurveyQuestionLabel, + SurveyUrlMatchTypeLabels, +} from './constants' +import { SurveyAPIEditor } from './SurveyAPIEditor' import { BaseAppearance, Customization, @@ -32,24 +50,9 @@ import { SurveyMultipleChoiceAppearance, SurveyRatingAppearance, } from './SurveyAppearance' -import { SurveyAPIEditor } from './SurveyAPIEditor' -import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' -import { - defaultSurveyFieldValues, - defaultSurveyAppearance, - SurveyQuestionLabel, - SurveyUrlMatchTypeLabels, -} from './constants' -import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' -import React from 'react' -import { CodeEditor } from 'lib/components/CodeEditors' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' import { SurveyFormAppearance } from './SurveyFormAppearance' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { SurveyEditSection, surveyLogic } from './surveyLogic' import { surveysLogic } from './surveysLogic' -import { FlagSelector } from 'lib/components/FlagSelector' -import clsx from 'clsx' function PresentationTypeCard({ title, @@ -450,7 +453,13 @@ export default function SurveyEdit(): JSX.Element { SurveyQuestionType.MultipleChoice) && (
    - {({ value, onChange }) => ( + {({ + value, + onChange, + }: { + value: string[] + onChange: (newValue: string[]) => void + }) => (
    {(value || []).map( ( @@ -527,7 +536,10 @@ export default function SurveyEdit(): JSX.Element { 1 && + index !== survey.questions.length - 1 + ? 'Next' + : survey.appearance.submitButtonText : question.buttonText } /> @@ -602,6 +614,17 @@ export default function SurveyEdit(): JSX.Element { textPlaceholder="ex: We really appreciate it." /> + + + setSurveyValue('appearance', { + ...survey.appearance, + autoDisappear: checked, + }) + } + /> + ), }, @@ -830,7 +853,7 @@ export default function SurveyEdit(): JSX.Element { />
    - + diff --git a/frontend/src/scenes/surveys/SurveyFormAppearance.tsx b/frontend/src/scenes/surveys/SurveyFormAppearance.tsx index 2ca9b4d98dae5..1c6eb32739283 100644 --- a/frontend/src/scenes/surveys/SurveyFormAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyFormAppearance.tsx @@ -1,9 +1,11 @@ import { LemonSelect } from '@posthog/lemon-ui' -import { SurveyAppearance, SurveyThankYou } from './SurveyAppearance' -import { SurveyAPIEditor } from './SurveyAPIEditor' -import { NewSurvey, defaultSurveyAppearance } from './constants' + import { Survey, SurveyType } from '~/types' +import { defaultSurveyAppearance, NewSurvey } from './constants' +import { SurveyAPIEditor } from './SurveyAPIEditor' +import { SurveyAppearance, SurveyThankYou } from './SurveyAppearance' + interface SurveyFormAppearanceProps { activePreview: number survey: NewSurvey | Survey diff --git a/frontend/src/scenes/surveys/SurveySettings.tsx b/frontend/src/scenes/surveys/SurveySettings.tsx index 6278a1530ce52..ac5fd8c458be4 100644 --- a/frontend/src/scenes/surveys/SurveySettings.tsx +++ b/frontend/src/scenes/surveys/SurveySettings.tsx @@ -1,8 +1,8 @@ +import { LemonSwitch, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { teamLogic } from 'scenes/teamLogic' -import { LemonSwitch, Link } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' export type SurveySettingsProps = { inModal?: boolean diff --git a/frontend/src/scenes/surveys/SurveyTemplates.scss b/frontend/src/scenes/surveys/SurveyTemplates.scss index 9d950ef65b1c3..c9622e7624689 100644 --- a/frontend/src/scenes/surveys/SurveyTemplates.scss +++ b/frontend/src/scenes/surveys/SurveyTemplates.scss @@ -1,22 +1,20 @@ @import '../../styles/mixins'; .SurveyTemplateContainer { - flex: 1; - display: flex; align-items: center; + background: var(--bg-light); + border-radius: var(--radius); border: 1px solid var(--border); - border-radius: 6px; + display: flex; + flex: 1; min-height: 300px; - margin-top: 2px; &:hover { cursor: pointer; - border-color: var(--primary-light); + border-color: var(--primary-3000-hover); } .SurveyTemplate { - -ms-transform: scale(0.8, 0.8); /* IE 9 */ - -webkit-transform: scale(0.8, 0.8); /* Safari */ transform: scale(0.8, 0.8); } } diff --git a/frontend/src/scenes/surveys/SurveyTemplates.tsx b/frontend/src/scenes/surveys/SurveyTemplates.tsx index 5eee9ba520d9a..fb2bbf55290de 100644 --- a/frontend/src/scenes/surveys/SurveyTemplates.tsx +++ b/frontend/src/scenes/surveys/SurveyTemplates.tsx @@ -1,14 +1,17 @@ -import { SceneExport } from 'scenes/sceneTypes' -import { SurveyAppearance } from './SurveyAppearance' -import { defaultSurveyTemplates, defaultSurveyAppearance } from './constants' -import { SurveyQuestion } from '~/types' import './SurveyTemplates.scss' + +import { LemonButton } from '@posthog/lemon-ui' import { useActions } from 'kea' import { PageHeader } from 'lib/components/PageHeader' -import { LemonButton } from '@posthog/lemon-ui' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' + +import { SurveyQuestion } from '~/types' + +import { defaultSurveyAppearance, defaultSurveyTemplates } from './constants' +import { SurveyAppearance } from './SurveyAppearance' import { surveyLogic } from './surveyLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export const scene: SceneExport = { component: SurveyTemplates, @@ -28,7 +31,7 @@ export function SurveyTemplates(): JSX.Element { } /> -
    +
    {defaultSurveyTemplates.map((template, idx) => { return (
    { }, []) return } -SurveyView.parameters = { - testOptions: { - skip: true, // FIXME: Fix the mocked data so that survey results can actually load - }, -} +SurveyView.tags = ['test-skip'] // FIXME: Fix the mocked data so that survey results can actually load export const SurveyTemplates: StoryFn = () => { useEffect(() => { diff --git a/frontend/src/scenes/surveys/Surveys.tsx b/frontend/src/scenes/surveys/Surveys.tsx index 143f39fcf01e7..877895ccb38a2 100644 --- a/frontend/src/scenes/surveys/Surveys.tsx +++ b/frontend/src/scenes/surveys/Surveys.tsx @@ -1,36 +1,38 @@ import { LemonButton, + LemonButtonWithSideAction, LemonDivider, LemonInput, LemonSelect, LemonTable, - Link, LemonTag, LemonTagType, + Link, Spinner, - LemonButtonWithSideAction, } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { router } from 'kea-router' import { PageHeader } from 'lib/components/PageHeader' +import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' +import { dayjs } from 'lib/dayjs' +import { IconSettings } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { More } from 'lib/lemon-ui/LemonButton/More' -import stringWithWBR from 'lib/utils/stringWithWBR' -import { SceneExport } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' -import { getSurveyStatus, surveysLogic } from './surveysLogic' -import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' -import { ProductKey, ProgressStatus, Survey } from '~/types' import { LemonTableColumn } from 'lib/lemon-ui/LemonTable' -import { useActions, useValues } from 'kea' -import { router } from 'kea-router' +import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import stringWithWBR from 'lib/utils/stringWithWBR' import { useState } from 'react' -import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { dayjs } from 'lib/dayjs' -import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { IconSettings } from 'lib/lemon-ui/icons' -import { openSurveysSettingsDialog } from './SurveySettings' + +import { ProductKey, ProgressStatus, Survey } from '~/types' + import { SurveyQuestionLabel } from './constants' +import { openSurveysSettingsDialog } from './SurveySettings' +import { getSurveyStatus, surveysLogic } from './surveysLogic' export const scene: SceneExport = { component: Surveys, @@ -117,7 +119,7 @@ export function Surveys(): JSX.Element { ]} />
    - + {showSurveysDisabledBanner ? ( ([ actions.loadSurveys() actions.reportSurveyResumed(survey) }, - archiveSurvey: async () => { + archiveSurvey: () => { actions.updateSurvey({ archived: true }) }, loadSurveySuccess: () => { @@ -571,10 +575,11 @@ export const surveyLogic = kea([ (s) => [s.survey], (survey: Survey): Breadcrumb[] => [ { + key: Scene.Surveys, name: 'Surveys', path: urls.surveys(), }, - ...(survey?.name ? [{ name: survey.name }] : []), + { key: survey?.id || 'new', name: survey.name }, ], ], dataTableQuery: [ @@ -682,7 +687,7 @@ export const surveyLogic = kea([ // controlled using a PureField in the form urlMatchType: values.urlMatchTypeValidationError, }), - submit: async (surveyPayload) => { + submit: (surveyPayload) => { let surveyPayloadWithTargetingFlagFilters = surveyPayload const flagLogic = featureFlagLogic({ id: values.survey.targeting_flag?.id || 'new' }) if (values.hasTargetingFlag) { @@ -721,12 +726,12 @@ export const surveyLogic = kea([ return [urls.survey(values.survey.id), router.values.searchParams, hashParams] }, })), - afterMount(async ({ props, actions }) => { + afterMount(({ props, actions }) => { if (props.id !== 'new') { - await actions.loadSurvey() + actions.loadSurvey() } if (props.id === 'new') { - await actions.resetSurvey() + actions.resetSurvey() } }), ]) diff --git a/frontend/src/scenes/surveys/surveyViewViz.tsx b/frontend/src/scenes/surveys/surveyViewViz.tsx index 0b5786ab2c109..6ff1ecc331b97 100644 --- a/frontend/src/scenes/surveys/surveyViewViz.tsx +++ b/frontend/src/scenes/surveys/surveyViewViz.tsx @@ -1,24 +1,26 @@ import { LemonTable } from '@posthog/lemon-ui' -import { - surveyLogic, - SurveyRatingResults, - QuestionResultsReady, - SurveySingleChoiceResults, - SurveyMultipleChoiceResults, - SurveyOpenTextResults, - SurveyUserStats, -} from './surveyLogic' -import { useActions, useValues, BindLogic } from 'kea' -import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { BindLogic, useActions, useValues } from 'kea' import { IconInfo } from 'lib/lemon-ui/icons' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { GraphType } from '~/types' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useEffect } from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' import { LineGraph } from 'scenes/insights/views/LineGraph/LineGraph' import { PieChart } from 'scenes/insights/views/LineGraph/PieChart' import { PersonDisplay } from 'scenes/persons/PersonDisplay' -import { insightLogic } from 'scenes/insights/insightLogic' + +import { GraphType } from '~/types' import { InsightLogicProps, SurveyQuestionType } from '~/types' -import { useEffect } from 'react' + +import { + QuestionResultsReady, + surveyLogic, + SurveyMultipleChoiceResults, + SurveyOpenTextResults, + SurveyRatingResults, + SurveySingleChoiceResults, + SurveyUserStats, +} from './surveyLogic' const insightProps: InsightLogicProps = { dashboardItemId: `new-survey`, @@ -483,8 +485,10 @@ export function OpenTextViz({ return (
    -
    - {event.properties[surveyResponseField]} +
    + {typeof event.properties[surveyResponseField] !== 'string' + ? JSON.stringify(event.properties[surveyResponseField]) + : event.properties[surveyResponseField]}
    ([ () => [], (): Breadcrumb[] => [ { + key: Scene.Surveys, name: 'Surveys', path: urls.surveys(), }, diff --git a/frontend/src/scenes/teamLogic.tsx b/frontend/src/scenes/teamLogic.tsx index 26fadbed4ed66..0e7c63f67931a 100644 --- a/frontend/src/scenes/teamLogic.tsx +++ b/frontend/src/scenes/teamLogic.tsx @@ -1,17 +1,19 @@ import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' -import type { teamLogicType } from './teamLogicType' -import { CorrelationConfigType, PropertyOperator, TeamPublicType, TeamType } from '~/types' -import { userLogic } from './userLogic' -import { identifierToHuman, isUserLoggedIn, resolveWebhookService } from 'lib/utils' -import { organizationLogic } from './organizationLogic' -import { getAppContext } from 'lib/utils/getAppContext' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { IconSwapHoriz } from 'lib/lemon-ui/icons' import { loaders } from 'kea-loaders' +import api, { ApiConfig } from 'lib/api' import { OrganizationMembershipLevel } from 'lib/constants' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { IconSwapHoriz } from 'lib/lemon-ui/icons' +import { lemonToast } from 'lib/lemon-ui/lemonToast' import { getPropertyLabel } from 'lib/taxonomy' +import { identifierToHuman, isUserLoggedIn, resolveWebhookService } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { getAppContext } from 'lib/utils/getAppContext' + +import { CorrelationConfigType, PropertyOperator, TeamPublicType, TeamType } from '~/types' + +import { organizationLogic } from './organizationLogic' +import type { teamLogicType } from './teamLogicType' +import { userLogic } from './userLogic' const parseUpdatedAttributeName = (attr: string | null): string => { if (attr === 'slack_incoming_webhook') { @@ -206,6 +208,11 @@ export const teamLogic = kea([ ], })), listeners(({ actions }) => ({ + loadCurrentTeamSuccess: ({ currentTeam }) => { + if (currentTeam) { + ApiConfig.setCurrentTeamId(currentTeam.id) + } + }, createTeamSuccess: () => { organizationLogic.actions.loadCurrentOrganization() }, diff --git a/frontend/src/scenes/toolbar-launch/ToolbarLaunch.tsx b/frontend/src/scenes/toolbar-launch/ToolbarLaunch.tsx index e844b3d9ab9ec..a3551ee84d6d0 100644 --- a/frontend/src/scenes/toolbar-launch/ToolbarLaunch.tsx +++ b/frontend/src/scenes/toolbar-launch/ToolbarLaunch.tsx @@ -1,12 +1,13 @@ -import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' import './ToolbarLaunch.scss' -import { Link } from 'lib/lemon-ui/Link' -import { urls } from 'scenes/urls' -import { IconFlag, IconGroupedEvents, IconHeatmap, IconMagnifier } from 'lib/lemon-ui/icons' + import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { PageHeader } from 'lib/components/PageHeader' +import { IconFlag, IconGroupedEvents, IconHeatmap, IconMagnifier } from 'lib/lemon-ui/icons' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { Link } from 'lib/lemon-ui/Link' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' export const scene: SceneExport = { component: ToolbarLaunch, diff --git a/frontend/src/scenes/trends/Trends.tsx b/frontend/src/scenes/trends/Trends.tsx index 8938d477173f1..c6e7faffcf6b2 100644 --- a/frontend/src/scenes/trends/Trends.tsx +++ b/frontend/src/scenes/trends/Trends.tsx @@ -1,14 +1,16 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { ActionsPie, ActionsLineGraph, ActionsHorizontalBar } from './viz' -import { ChartDisplayType, InsightType, ItemMode } from '~/types' -import { InsightsTable } from 'scenes/insights/views/InsightsTable/InsightsTable' import { insightLogic } from 'scenes/insights/insightLogic' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' -import { WorldMap } from 'scenes/insights/views/WorldMap' import { BoldNumber } from 'scenes/insights/views/BoldNumber' -import { LemonButton } from '@posthog/lemon-ui' -import { trendsDataLogic } from './trendsDataLogic' +import { InsightsTable } from 'scenes/insights/views/InsightsTable/InsightsTable' +import { WorldMap } from 'scenes/insights/views/WorldMap' + import { QueryContext } from '~/queries/types' +import { ChartDisplayType, InsightType, ItemMode } from '~/types' + +import { trendsDataLogic } from './trendsDataLogic' +import { ActionsHorizontalBar, ActionsLineGraph, ActionsPie } from './viz' interface Props { view: InsightType @@ -61,19 +63,7 @@ export function TrendInsight({ view, context }: Props): JSX.Element { return ( <> - {series && ( -
    - {renderViz()} -
    - )} + {series &&
    {renderViz()}
    } {display !== ChartDisplayType.WorldMap && // the world map doesn't need this cta breakdown && loadMoreBreakdownUrl && ( diff --git a/frontend/src/scenes/trends/mathsLogic.tsx b/frontend/src/scenes/trends/mathsLogic.tsx index 4d107d9349d90..f8fe2e6fdd56b 100644 --- a/frontend/src/scenes/trends/mathsLogic.tsx +++ b/frontend/src/scenes/trends/mathsLogic.tsx @@ -1,8 +1,10 @@ -import { kea, path, connect, selectors } from 'kea' +import { connect, kea, path, selectors } from 'kea' +import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' + import { groupsModel } from '~/models/groupsModel' -import type { mathsLogicType } from './mathsLogicType' import { BaseMathType, CountPerActorMathType, HogQLMathType, PropertyMathType } from '~/types' -import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic' + +import type { mathsLogicType } from './mathsLogicType' export enum MathCategory { EventCount, diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx index 631648849327f..bbb3a85a68ac2 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.stories.tsx @@ -1,9 +1,11 @@ import { Meta, Story } from '@storybook/react' import { MOCK_TEAM_ID } from 'lib/api.mock' import { RawPropertiesTimelineResult } from 'lib/components/PropertiesTimeline/propertiesTimelineLogic' + import { useStorybookMocks } from '~/mocks/browser' -import { PersonsModal as PersonsModalComponent } from './PersonsModal' + import EXAMPLE_PERSONS_RESPONSE from './__mocks__/examplePersonsResponse.json' +import { PersonsModal as PersonsModalComponent } from './PersonsModal' const meta: Meta = { title: 'Scenes-App/Persons Modal', diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index a2a9aa764c090..894a03d70b6e0 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -1,5 +1,30 @@ -import { useState } from 'react' +import './PersonsModal.scss' + +import { LemonBadge, LemonButton, LemonDivider, LemonInput, LemonModal, LemonSelect, Link } from '@posthog/lemon-ui' +import { LemonModalProps } from '@posthog/lemon-ui' +import { Skeleton } from 'antd' import { useActions, useValues } from 'kea' +import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' +import { triggerExport } from 'lib/components/ExportButton/exporter' +import { PropertiesTable } from 'lib/components/PropertiesTable' +import { PropertiesTimeline } from 'lib/components/PropertiesTimeline' +import { IconPlayCircle, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { capitalizeFirstLetter, isGroupType, midEllipsis, pluralize } from 'lib/utils' +import { useState } from 'react' +import { createRoot } from 'react-dom/client' +import { GroupActorDisplay, groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { asDisplay } from 'scenes/persons/person-utils' +import { PersonDisplay } from 'scenes/persons/PersonDisplay' +import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' +import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' +import { teamLogic } from 'scenes/teamLogic' + +import { Noun } from '~/models/groupsModel' import { ActorType, ExporterFormat, @@ -7,32 +32,9 @@ import { PropertyDefinitionType, SessionRecordingType, } from '~/types' + import { personsModalLogic } from './personsModalLogic' -import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' -import { capitalizeFirstLetter, isGroupType, midEllipsis, pluralize } from 'lib/utils' -import { GroupActorDisplay, groupDisplayId } from 'scenes/persons/GroupActorDisplay' -import { IconPlayCircle, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { triggerExport } from 'lib/components/ExportButton/exporter' -import { LemonButton, LemonBadge, LemonDivider, LemonInput, LemonModal, LemonSelect, Link } from '@posthog/lemon-ui' -import { PersonDisplay } from 'scenes/persons/PersonDisplay' -import { createRoot } from 'react-dom/client' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { SaveCohortModal } from './SaveCohortModal' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { Skeleton } from 'antd' -import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' -import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { Noun } from '~/models/groupsModel' -import { LemonModalProps } from '@posthog/lemon-ui' -import { PropertiesTimeline } from 'lib/components/PropertiesTimeline' -import { PropertiesTable } from 'lib/components/PropertiesTable' -import { teamLogic } from 'scenes/teamLogic' -import { LemonTabs } from 'lib/lemon-ui/LemonTabs' - -import './PersonsModal.scss' -import { asDisplay } from 'scenes/persons/person-utils' export interface PersonsModalProps extends Pick { onAfterClose?: () => void @@ -183,7 +185,7 @@ export function PersonsModal({ { - triggerExport({ + void triggerExport({ export_format: ExporterFormat.CSV, export_context: { path: originalUrl, diff --git a/frontend/src/scenes/trends/persons-modal/SaveCohortModal.tsx b/frontend/src/scenes/trends/persons-modal/SaveCohortModal.tsx index f513e1e5e3a2f..7a8da3e4219fc 100644 --- a/frontend/src/scenes/trends/persons-modal/SaveCohortModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/SaveCohortModal.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' +import { useState } from 'react' interface Props { onSave: (title: string) => void diff --git a/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts b/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts index 50f3156f013d1..70958019ed94c 100644 --- a/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts +++ b/frontend/src/scenes/trends/persons-modal/peronsModalLogic.test.ts @@ -1,7 +1,9 @@ -import { personsModalLogic } from './personsModalLogic' -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' +import { initKeaTests } from '~/test/init' + +import { personsModalLogic } from './personsModalLogic' + describe('personsModalLogic', () => { let logic: ReturnType diff --git a/frontend/src/scenes/trends/persons-modal/persons-modal-utils.tsx b/frontend/src/scenes/trends/persons-modal/persons-modal-utils.tsx index 4956c5f36c002..3f44300480fe5 100644 --- a/frontend/src/scenes/trends/persons-modal/persons-modal-utils.tsx +++ b/frontend/src/scenes/trends/persons-modal/persons-modal-utils.tsx @@ -1,10 +1,13 @@ import * as Sentry from '@sentry/react' -import md5 from 'md5' +import { getBarColorFromStatus, getSeriesColor } from 'lib/colors' +import { InsightLabel } from 'lib/components/InsightLabel' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { dayjs } from 'lib/dayjs' import { capitalizeFirstLetter, pluralize, toParams } from 'lib/utils' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import md5 from 'md5' import { isFunnelsFilter, isPathsFilter } from 'scenes/insights/sharedUtils' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' + import { FunnelsFilterType, FunnelVizType, @@ -13,8 +16,6 @@ import { PathsFilterType, StepOrderValue, } from '~/types' -import { InsightLabel } from 'lib/components/InsightLabel' -import { getBarColorFromStatus, getSeriesColor } from 'lib/colors' export const funnelTitle = (props: { converted: boolean diff --git a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts index 1497b84e180fe..2cfc2112c8c80 100644 --- a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts +++ b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts @@ -1,17 +1,18 @@ -import { kea, connect, path, props, reducers, actions, selectors, listeners, afterMount } from 'kea' -import api from 'lib/api' -import { ActorType, BreakdownType, ChartDisplayType, IntervalType, PropertiesTimelineFilterType } from '~/types' -import { loaders } from 'kea-loaders' -import { cohortsModel } from '~/models/cohortsModel' import { lemonToast } from '@posthog/lemon-ui' +import { actions, afterMount, connect, kea, listeners, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' +import api from 'lib/api' +import { fromParamsGivenUrl, isGroupType } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { urls } from 'scenes/urls' -import type { personsModalLogicType } from './personsModalLogicType' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { fromParamsGivenUrl, isGroupType } from 'lib/utils' +import { cohortsModel } from '~/models/cohortsModel' import { groupsModel } from '~/models/groupsModel' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' +import { ActorType, BreakdownType, ChartDisplayType, IntervalType, PropertiesTimelineFilterType } from '~/types' + +import type { personsModalLogicType } from './personsModalLogicType' export interface PersonModalLogicProps { url: string diff --git a/frontend/src/scenes/trends/trendsDataLogic.test.ts b/frontend/src/scenes/trends/trendsDataLogic.test.ts index b4e78196b18cd..5a684c1ec1668 100644 --- a/frontend/src/scenes/trends/trendsDataLogic.test.ts +++ b/frontend/src/scenes/trends/trendsDataLogic.test.ts @@ -1,14 +1,14 @@ import { expectLogic } from 'kea-test-utils' -import { initKeaTests } from '~/test/init' - -import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { insightDataLogic } from 'scenes/insights/insightDataLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { trendsDataLogic } from './trendsDataLogic' -import { ChartDisplayType, InsightLogicProps, InsightModel } from '~/types' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { DataNode, LifecycleQuery, NodeKind, TrendsQuery } from '~/queries/schema' -import { trendResult, trendPieResult, lifecycleResult } from './__mocks__/trendsDataLogicMocks' +import { initKeaTests } from '~/test/init' +import { ChartDisplayType, InsightLogicProps, InsightModel } from '~/types' + +import { lifecycleResult, trendPieResult, trendResult } from './__mocks__/trendsDataLogicMocks' +import { trendsDataLogic } from './trendsDataLogic' let logic: ReturnType let builtDataNodeLogic: ReturnType diff --git a/frontend/src/scenes/trends/trendsDataLogic.ts b/frontend/src/scenes/trends/trendsDataLogic.ts index 5ee67ec453f4e..96cc366507357 100644 --- a/frontend/src/scenes/trends/trendsDataLogic.ts +++ b/frontend/src/scenes/trends/trendsDataLogic.ts @@ -1,12 +1,13 @@ -import { kea, props, key, path, connect, selectors, actions, reducers, listeners } from 'kea' -import { ChartDisplayType, InsightLogicProps, LifecycleToggle, TrendAPIResponse, TrendResult } from '~/types' -import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import api from 'lib/api' +import { dayjs } from 'lib/dayjs' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' + +import { ChartDisplayType, InsightLogicProps, LifecycleToggle, TrendAPIResponse, TrendResult } from '~/types' import type { trendsDataLogicType } from './trendsDataLogicType' import { IndexedTrendResult } from './types' -import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { dayjs } from 'lib/dayjs' export const trendsDataLogic = kea([ props({} as InsightLogicProps), @@ -26,6 +27,7 @@ export const trendsDataLogic = kea([ 'interval', 'breakdown', 'showValueOnSeries', + 'showLabelOnSeries', 'showPercentStackView', 'supportsPercentStackView', 'trendsFilter', @@ -36,6 +38,7 @@ export const trendsDataLogic = kea([ 'isNonTimeSeriesDisplay', 'isSingleSeries', 'hasLegend', + 'vizSpecificOptions', ], ], actions: [insightVizDataLogic(props), ['setInsightData', 'updateInsightFilter']], @@ -55,7 +58,7 @@ export const trendsDataLogic = kea([ ], }), - selectors({ + selectors(({ values }) => ({ results: [ (s) => [s.insightData], (insightData: TrendAPIResponse | null): TrendResult[] => { @@ -87,7 +90,6 @@ export const trendsDataLogic = kea([ } else if (lifecycleFilter) { if (lifecycleFilter.toggledLifecycles) { indexedResults = indexedResults.filter((result) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion lifecycleFilter.toggledLifecycles!.includes(String(result.status) as LifecycleToggle) ) } @@ -130,7 +132,12 @@ export const trendsDataLogic = kea([ } }, ], - }), + + pieChartVizOptions: [ + () => [() => values.vizSpecificOptions], + (vizSpecificOptions) => vizSpecificOptions?.[ChartDisplayType.ActionsPie], + ], + })), listeners(({ actions, values }) => ({ loadMoreBreakdownValues: async () => { diff --git a/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx b/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx index 017bde1423b7a..a1478f30142ce 100644 --- a/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx +++ b/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx @@ -1,21 +1,23 @@ -import { useState, useEffect } from 'react' -import { LineGraph } from '../../insights/views/LineGraph/LineGraph' -import { getSeriesColor } from 'lib/colors' import { useValues } from 'kea' -import { InsightEmptyState } from '../../insights/EmptyStates' -import { ChartParams, GraphType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { openPersonsModal } from '../persons-modal/PersonsModal' +import { getSeriesColor } from 'lib/colors' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { useEffect, useState } from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' +import { formatBreakdownLabel } from 'scenes/insights/utils' + import { cohortsModel } from '~/models/cohortsModel' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatBreakdownLabel } from 'scenes/insights/utils' +import { ChartParams, GraphType } from '~/types' + +import { InsightEmptyState } from '../../insights/EmptyStates' +import { LineGraph } from '../../insights/views/LineGraph/LineGraph' +import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { openPersonsModal } from '../persons-modal/PersonsModal' import { trendsDataLogic } from '../trendsDataLogic' type DataSet = any -export function ActionsHorizontalBar({ inCardView, showPersonsModal = true }: ChartParams): JSX.Element | null { +export function ActionsHorizontalBar({ showPersonsModal = true }: ChartParams): JSX.Element | null { const [data, setData] = useState(null) const [total, setTotal] = useState(0) @@ -80,7 +82,6 @@ export function ActionsHorizontalBar({ inCardView, showPersonsModal = true }: Ch trendsFilter={trendsFilter} formula={formula} showValueOnSeries={showValueOnSeries} - inCardView={inCardView} onClick={ !showPersonsModal || trendsFilter?.formula ? undefined diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx index 9fe2c882f8dd1..d973981d09a14 100644 --- a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx +++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx @@ -1,21 +1,23 @@ -import { LineGraph } from '../../insights/views/LineGraph/LineGraph' import { useValues } from 'kea' -import { InsightEmptyState } from '../../insights/EmptyStates' -import { ChartDisplayType, ChartParams, GraphType } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { capitalizeFirstLetter, isMultiSeriesFormula } from 'lib/utils' -import { openPersonsModal } from '../persons-modal/PersonsModal' -import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { combineUrl, router } from 'kea-router' import { DateDisplay } from 'lib/components/DateDisplay' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { trendsDataLogic } from '../trendsDataLogic' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { capitalizeFirstLetter, isMultiSeriesFormula } from 'lib/utils' import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { isInsightVizNode, isLifecycleQuery } from '~/queries/utils' -import { DataTableNode, NodeKind } from '~/queries/schema' -import { combineUrl, router } from 'kea-router' +import { insightLogic } from 'scenes/insights/insightLogic' import { urls } from 'scenes/urls' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' + +import { DataTableNode, NodeKind } from '~/queries/schema' +import { isInsightVizNode, isLifecycleQuery } from '~/queries/utils' +import { ChartDisplayType, ChartParams, GraphType } from '~/types' + +import { InsightEmptyState } from '../../insights/EmptyStates' +import { LineGraph } from '../../insights/views/LineGraph/LineGraph' +import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { openPersonsModal } from '../persons-modal/PersonsModal' +import { trendsDataLogic } from '../trendsDataLogic' export function ActionsLineGraph({ inSharedMode = false, diff --git a/frontend/src/scenes/trends/viz/ActionsPie.scss b/frontend/src/scenes/trends/viz/ActionsPie.scss index f333b07b09e89..47af0778a00bd 100644 --- a/frontend/src/scenes/trends/viz/ActionsPie.scss +++ b/frontend/src/scenes/trends/viz/ActionsPie.scss @@ -1,22 +1,25 @@ -.actions-pie-component { - min-width: 33%; +.ActionsPie { + flex: 1; + width: 100%; height: 100%; - // No padding at the top because usually there's whitespace between "Computed ..." and "Show legend" - // More padding at the bottom to give the number some breathing room - padding: 0 1rem 2rem; + display: flex; + flex-direction: row; + gap: 0.5rem; - .InsightCard & { - padding-top: 1rem; - } + .ActionsPie__component { + min-width: 33%; + flex: 1; + padding: 1rem; - .pie-chart { - position: relative; - width: 100%; - height: calc(100% - 4.5rem); // 4.5rem is the height of the number below the chart - transition: height 0.5s; - } + .ActionsPie__chart { + position: relative; + width: 100%; + height: calc(100% - 4.5rem); // 4.5rem is the height of the number below the chart + transition: height 0.5s; + } - h3 { - letter-spacing: -0.025em; + h3 { + letter-spacing: -0.025em; + } } } diff --git a/frontend/src/scenes/trends/viz/ActionsPie.tsx b/frontend/src/scenes/trends/viz/ActionsPie.tsx index 01a53d16dbc3a..d7cd6331c3e74 100644 --- a/frontend/src/scenes/trends/viz/ActionsPie.tsx +++ b/frontend/src/scenes/trends/viz/ActionsPie.tsx @@ -1,22 +1,29 @@ import './ActionsPie.scss' -import { useState, useEffect } from 'react' -import { getSeriesColor } from 'lib/colors' + import { useValues } from 'kea' -import { ChartParams, GraphType, GraphDataset } from '~/types' -import { insightLogic } from 'scenes/insights/insightLogic' -import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' -import { openPersonsModal } from '../persons-modal/PersonsModal' +import { getSeriesColor } from 'lib/colors' +import { InsightLegend } from 'lib/components/InsightLegend/InsightLegend' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' -import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { useEffect, useState } from 'react' +import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat' +import { insightLogic } from 'scenes/insights/insightLogic' +import { formatBreakdownLabel } from 'scenes/insights/utils' import { PieChart } from 'scenes/insights/views/LineGraph/PieChart' -import { InsightLegend } from 'lib/components/InsightLegend/InsightLegend' -import clsx from 'clsx' + import { cohortsModel } from '~/models/cohortsModel' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { formatBreakdownLabel } from 'scenes/insights/utils' +import { ChartDisplayType, ChartParams, GraphDataset, GraphType } from '~/types' + +import { urlsForDatasets } from '../persons-modal/persons-modal-utils' +import { openPersonsModal } from '../persons-modal/PersonsModal' import { trendsDataLogic } from '../trendsDataLogic' -export function ActionsPie({ inSharedMode, inCardView, showPersonsModal = true }: ChartParams): JSX.Element | null { +export function ActionsPie({ + inSharedMode, + inCardView, + showPersonsModal = true, + context, +}: ChartParams): JSX.Element | null { const [data, setData] = useState(null) const [total, setTotal] = useState(0) @@ -30,10 +37,16 @@ export function ActionsPie({ inSharedMode, inCardView, showPersonsModal = true } trendsFilter, formula, showValueOnSeries, + showLabelOnSeries, supportsPercentStackView, showPercentStackView, + pieChartVizOptions, } = useValues(trendsDataLogic(insightProps)) + const renderingMetadata = context?.chartRenderingMetadata?.[ChartDisplayType.ActionsPie] + + const showAggregation = !pieChartVizOptions?.hideAggregation + function updateData(): void { const _data = [...indexedResults].sort((a, b) => b.aggregated_value - a.aggregated_value) const days = _data.length > 0 ? _data[0].days : [] @@ -70,17 +83,32 @@ export function ActionsPie({ inSharedMode, inCardView, showPersonsModal = true } } }, [indexedResults, hiddenLegendKeys]) + const onClick = + renderingMetadata?.onSegmentClick || + (!showPersonsModal || formula + ? undefined + : (payload) => { + const { points, index, crossDataset } = payload + const dataset = points.referencePoint.dataset + const label = dataset.labels?.[index] + + const urls = urlsForDatasets(crossDataset, index) + const selectedUrl = urls[index]?.value + + if (selectedUrl) { + openPersonsModal({ + urls, + urlsIndex: index, + title: , + }) + } + }) + return data ? ( data[0] && data[0].labels ? ( -
    -
    -
    +
    +
    +
    { - const { points, index, crossDataset } = payload - const dataset = points.referencePoint.dataset - const label = dataset.labels?.[index] - - const urls = urlsForDatasets(crossDataset, index) - const selectedUrl = urls[index]?.value - - if (selectedUrl) { - openPersonsModal({ - urls, - urlsIndex: index, - title: , - }) - } - } - } + onClick={onClick} + disableHoverOffset={pieChartVizOptions?.disableHoverOffset} />
    -

    - {formatAggregationAxisValue(trendsFilter, total)} -

    + {showAggregation && ( +

    + {formatAggregationAxisValue(trendsFilter, total)} +

    + )}
    {inCardView && trendsFilter?.show_legend && }
    diff --git a/frontend/src/scenes/trends/viz/index.ts b/frontend/src/scenes/trends/viz/index.ts index 14965e37acde4..cd0636422f7c4 100644 --- a/frontend/src/scenes/trends/viz/index.ts +++ b/frontend/src/scenes/trends/viz/index.ts @@ -1,3 +1,3 @@ -export * from './ActionsPie' -export * from './ActionsLineGraph' export * from './ActionsHorizontalBar' +export * from './ActionsLineGraph' +export * from './ActionsPie' diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index 7f6fa0c5b03ea..9653e768dc3bc 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -1,18 +1,21 @@ +import { combineUrl } from 'kea-router' +import { toParams } from 'lib/utils' + +import { ExportOptions } from '~/exporter/types' import { ActionType, AnnotationType, AnyPartialFilterType, + AppMetricsUrlParams, DashboardType, FilterType, InsightShortId, - ReplayTabs, + PipelineAppTabs, PipelineTabs, + ReplayTabs, } from '~/types' -import { combineUrl } from 'kea-router' -import { ExportOptions } from '~/exporter/types' -import { AppMetricsUrlParams } from './apps/appMetricsSceneLogic' + import { PluginTab } from './plugins/types' -import { toParams } from 'lib/utils' import { SettingId, SettingLevelId, SettingSectionId } from './settings/types' /** @@ -95,8 +98,10 @@ export const urls = { personByUUID: (uuid: string, encode: boolean = true): string => encode ? `/persons/${encodeURIComponent(uuid)}` : `/persons/${uuid}`, persons: (): string => '/persons', - pipeline: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : 'destinations'}`, - pipelineNew: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : 'destinations'}/new`, + pipeline: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : PipelineTabs.Destinations}`, + pipelineApp: (id: string | number, tab?: PipelineAppTabs): string => + `/pipeline/${id}/${tab ? tab : PipelineAppTabs.Configuration}`, + pipelineNew: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : PipelineTabs.Destinations}/new`, groups: (groupTypeIndex: string | number): string => `/groups/${groupTypeIndex}`, // :TRICKY: Note that groupKey is provided by user. We need to override urlPatternOptions for kea-router. group: (groupTypeIndex: string | number, groupKey: string, encode: boolean = true, tab?: string | null): string => @@ -108,9 +113,11 @@ export const urls = { featureFlags: (tab?: string): string => `/feature_flags${tab ? `?tab=${tab}` : ''}`, featureFlag: (id: string | number): string => `/feature_flags/${id}`, earlyAccessFeatures: (): string => '/early_access_features', - earlyAccessFeature: (id: ':id' | 'new' | string): string => `/early_access_features/${id}`, + /** @param id A UUID or 'new'. ':id' for routing. */ + earlyAccessFeature: (id: string): string => `/early_access_features/${id}`, surveys: (): string => '/surveys', - survey: (id: ':id' | 'new' | string): string => `/surveys/${id}`, + /** @param id A UUID or 'new'. ':id' for routing. */ + survey: (id: string): string => `/surveys/${id}`, surveyTemplates: (): string => '/survey_templates', dataWarehouse: (): string => '/data-warehouse', dataWarehouseTable: (): string => `/data-warehouse/new`, @@ -152,7 +159,6 @@ export const urls = { verifyEmail: (userUuid: string = '', token: string = ''): string => `/verify_email${userUuid ? `/${userUuid}` : ''}${token ? `/${token}` : ''}`, inviteSignup: (id: string): string => `/signup/${id}`, - ingestion: (): string => '/ingestion', products: (): string => '/products', onboarding: (productKey: string): string => `/onboarding/${productKey}`, // Cloud only diff --git a/frontend/src/scenes/userLogic.ts b/frontend/src/scenes/userLogic.ts index 8964fc32ab291..d65757c264403 100644 --- a/frontend/src/scenes/userLogic.ts +++ b/frontend/src/scenes/userLogic.ts @@ -1,14 +1,15 @@ -import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' -import api from 'lib/api' -import type { userLogicType } from './userLogicType' -import { AvailableFeature, OrganizationBasicType, ProductKey, UserType } from '~/types' -import posthog from 'posthog-js' -import { getAppContext } from 'lib/utils/getAppContext' -import { preflightLogic } from './PreflightCheck/preflightLogic' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { loaders } from 'kea-loaders' +import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import api from 'lib/api' import { DashboardCompatibleScenes } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { getAppContext } from 'lib/utils/getAppContext' +import posthog from 'posthog-js' + +import { AvailableFeature, OrganizationBasicType, ProductKey, UserType } from '~/types' + +import type { userLogicType } from './userLogicType' export interface UserDetailsFormType { first_name: string @@ -17,9 +18,6 @@ export interface UserDetailsFormType { export const userLogic = kea([ path(['scenes', 'userLogic']), - connect({ - values: [preflightLogic, ['preflight']], - }), actions(() => ({ loadUser: (resetOnFailure?: boolean) => ({ resetOnFailure }), updateCurrentTeam: (teamId: number, destination?: string) => ({ teamId, destination }), diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsHealthCheck.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsHealthCheck.tsx index e87a1fc8b3c9d..34cad0e5dc31a 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsHealthCheck.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsHealthCheck.tsx @@ -21,9 +21,12 @@ export const WebAnalyticsHealthCheck = (): JSX.Element | null => { or $pageleave{' '} ) : null} - events have been received, please read{' '} - the documentation and - fix this before using Web Analytics. + events have been received. Web analytics won't work correctly (it'll be a little empty!) +

    +

    + Please see{' '} + documentation for how to set up posthog-js + .

    ) @@ -32,9 +35,12 @@ export const WebAnalyticsHealthCheck = (): JSX.Element | null => {

    No $pageleave events have been received, this means that Bounce rate and Session - Duration might be inaccurate. Please read{' '} - the documentation and - fix this before using Web Analytics. + duration might be inaccurate. +

    +

    + Please see{' '} + documentation for how to set up posthog-js + .

    ) diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx index a38e6b9164edf..62627d13eeac0 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx @@ -1,9 +1,9 @@ import { useActions, useValues } from 'kea' import { supportLogic } from 'lib/components/Support/supportLogic' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { IconBugReport, IconFeedback } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { Link } from 'lib/lemon-ui/Link' -import { IconBugReport, IconFeedback } from 'lib/lemon-ui/icons' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' export const WebAnalyticsNotice = (): JSX.Element => { const { openSupportForm } = useActions(supportLogic) diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index 815e57d583d58..82465684496be 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -1,12 +1,13 @@ -import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' -import { DataTableNode, InsightVizNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' +import { useActions, useValues } from 'kea' import { UnexpectedNeverError } from 'lib/utils' -import { useActions } from 'kea' -import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { useCallback, useMemo } from 'react' -import { Query } from '~/queries/Query/Query' import { countryCodeToFlag, countryCodeToName } from 'scenes/insights/views/WorldMap' -import { PropertyFilterType } from '~/types' +import { DeviceTab, GeographyTab, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' + +import { Query } from '~/queries/Query/Query' +import { DataTableNode, InsightVizNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' +import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' +import { GraphPointPayload, PropertyFilterType } from '~/types' import { ChartDisplayType } from '~/types' const PercentageCell: QueryContextColumnComponent = ({ value }) => { @@ -173,23 +174,66 @@ export const webAnalyticsDataTableQueryContext: QueryContext = { } export const WebStatsTrendTile = ({ query }: { query: InsightVizNode }): JSX.Element => { - const { togglePropertyFilter } = useActions(webAnalyticsLogic) + const { togglePropertyFilter, setGeographyTab, setDeviceTab } = useActions(webAnalyticsLogic) + const { hasCountryFilter, deviceTab, hasDeviceTypeFilter, hasBrowserFilter, hasOSFilter } = + useValues(webAnalyticsLogic) const { key: worldMapPropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.Country) + const { key: deviceTypePropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.DeviceType) + const onWorldMapClick = useCallback( (breakdownValue: string) => { togglePropertyFilter(PropertyFilterType.Event, worldMapPropertyName, breakdownValue) + if (!hasCountryFilter) { + // if we just added a country filter, switch to the region tab, as the world map will not be useful + setGeographyTab(GeographyTab.REGIONS) + } }, [togglePropertyFilter, worldMapPropertyName] ) + const onDeviceTilePieChartClick = useCallback( + (graphPoint: GraphPointPayload) => { + if (graphPoint.seriesId == null) { + return + } + const dataset = graphPoint.crossDataset?.[graphPoint.seriesId] + if (!dataset) { + return + } + const breakdownValue = dataset.breakdownValues?.[graphPoint.index] + if (!breakdownValue) { + return + } + togglePropertyFilter(PropertyFilterType.Event, deviceTypePropertyName, breakdownValue) + + // switch to a different tab if we can, try them in this order: DeviceType Browser OS + if (deviceTab !== DeviceTab.DEVICE_TYPE && !hasDeviceTypeFilter) { + setDeviceTab(DeviceTab.DEVICE_TYPE) + } else if (deviceTab !== DeviceTab.BROWSER && !hasBrowserFilter) { + setDeviceTab(DeviceTab.BROWSER) + } else if (deviceTab !== DeviceTab.OS && !hasOSFilter) { + setDeviceTab(DeviceTab.OS) + } + }, + [togglePropertyFilter, deviceTypePropertyName, deviceTab, hasDeviceTypeFilter, hasBrowserFilter, hasOSFilter] + ) + const context = useMemo((): QueryContext => { return { ...webAnalyticsDataTableQueryContext, chartRenderingMetadata: { [ChartDisplayType.WorldMap]: { - countryProps: (countryCode, values) => ({ - onClick: values && values.count > 0 ? () => onWorldMapClick(countryCode) : undefined, - }), + countryProps: (countryCode, values) => { + return { + onClick: + values && (values.count > 0 || values.aggregated_value > 0) + ? () => onWorldMapClick(countryCode) + : undefined, + } + }, + }, + [ChartDisplayType.ActionsPie]: { + onSegmentClick: onDeviceTilePieChartClick, }, }, } diff --git a/frontend/src/scenes/web-analytics/WebDashboard.tsx b/frontend/src/scenes/web-analytics/WebDashboard.tsx index 9f4b9f42336a0..131832445ef55 100644 --- a/frontend/src/scenes/web-analytics/WebDashboard.tsx +++ b/frontend/src/scenes/web-analytics/WebDashboard.tsx @@ -1,10 +1,13 @@ -import { Query } from '~/queries/Query/Query' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { TabsTile, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { isEventPropertyOrPersonPropertyFilter } from 'lib/components/PropertyFilters/utils' -import { NodeKind, QuerySchema } from '~/queries/schema' -import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { WebAnalyticsHealthCheck } from 'scenes/web-analytics/WebAnalyticsHealthCheck' +import { TabsTile, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { WebAnalyticsNotice } from 'scenes/web-analytics/WebAnalyticsNotice' import { webAnalyticsDataTableQueryContext, @@ -12,11 +15,9 @@ import { WebStatsTrendTile, } from 'scenes/web-analytics/WebAnalyticsTile' import { WebTabs } from 'scenes/web-analytics/WebTabs' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' -import clsx from 'clsx' -import { WebAnalyticsHealthCheck } from 'scenes/web-analytics/WebAnalyticsHealthCheck' + +import { Query } from '~/queries/Query/Query' +import { NodeKind, QuerySchema } from '~/queries/schema' const Filters = (): JSX.Element => { const { webAnalyticsFilters, dateTo, dateFrom } = useValues(webAnalyticsLogic) @@ -97,9 +98,11 @@ const Tiles = (): JSX.Element => { return (
    {title &&

    {title}

    } @@ -120,7 +123,11 @@ const TabsTileItem = ({ tile }: { tile: TabsTile }): JSX.Element => { return ( ({ @@ -146,7 +153,7 @@ const WebQuery = ({ query }: { query: QuerySchema }): JSX.Element => { export const WebAnalyticsDashboard = (): JSX.Element => { return ( -
    +
    diff --git a/frontend/src/scenes/web-analytics/WebTabs.tsx b/frontend/src/scenes/web-analytics/WebTabs.tsx index 96d1d89d2001e..64cf389373d20 100644 --- a/frontend/src/scenes/web-analytics/WebTabs.tsx +++ b/frontend/src/scenes/web-analytics/WebTabs.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx' -import React from 'react' import { useSliderPositioning } from 'lib/lemon-ui/hooks' +import React from 'react' const TRANSITION_MS = 200 export const WebTabs = ({ @@ -24,7 +24,7 @@ export const WebTabs = ({
    {

    {activeTab?.title}

    } -
    +
    {tabs.length > 1 && ( // TODO switch to a select if more than 3
      @@ -59,7 +59,7 @@ export const WebTabs = ({
    -
    {activeTab?.content}
    +
    {activeTab?.content}
    ) } diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index d3aaac330e695..845629e3d7037 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -1,6 +1,10 @@ import { actions, afterMount, connect, kea, path, reducers, selectors } from 'kea' - -import type { webAnalyticsLogicType } from './webAnalyticsLogicType' +import { loaders } from 'kea-loaders' +import { windowValues } from 'kea-window-values' +import api from 'lib/api' +import { RETENTION_FIRST_TIME, STALE_EVENT_SECONDS } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { isNotNil } from 'lib/utils' import { NodeKind, @@ -9,14 +13,24 @@ import { WebAnalyticsPropertyFilters, WebStatsBreakdown, } from '~/queries/schema' -import { BaseMathType, ChartDisplayType, EventDefinitionType, PropertyFilterType, PropertyOperator } from '~/types' -import { isNotNil } from 'lib/utils' -import { loaders } from 'kea-loaders' -import api from 'lib/api' +import { + BaseMathType, + ChartDisplayType, + EventDefinition, + EventDefinitionType, + InsightType, + PropertyDefinition, + PropertyFilterType, + PropertyOperator, + RetentionPeriod, +} from '~/types' + +import type { webAnalyticsLogicType } from './webAnalyticsLogicType' export interface WebTileLayout { colSpan?: number rowSpan?: number + className?: string } interface BaseTile { @@ -170,7 +184,7 @@ export const webAnalyticsLogic = kea([ }, ], deviceTab: [ - DeviceTab.BROWSER as string, + DeviceTab.DEVICE_TYPE as string, { setDeviceTab: (_, { tab }) => tab, }, @@ -200,7 +214,7 @@ export const webAnalyticsLogic = kea([ }, ], }), - selectors(({ actions }) => ({ + selectors(({ actions, values }) => ({ tiles: [ (s) => [ s.webAnalyticsFilters, @@ -211,6 +225,8 @@ export const webAnalyticsLogic = kea([ s.geographyTab, s.dateFrom, s.dateTo, + () => values.isGreaterThanMd, + () => values.shouldShowGeographyTile, ], ( webAnalyticsFilters, @@ -220,13 +236,15 @@ export const webAnalyticsLogic = kea([ sourceTab, geographyTab, dateFrom, - dateTo + dateTo, + isGreaterThanMd: boolean, + shouldShowGeographyTile ): WebDashboardTile[] => { const dateRange = { date_from: dateFrom, date_to: dateTo, } - return [ + const tiles: (WebDashboardTile | null)[] = [ { layout: { colSpan: 12, @@ -431,72 +449,15 @@ export const webAnalyticsLogic = kea([ activeTabId: deviceTab, setTabId: actions.setDeviceTab, tabs: [ - { - id: DeviceTab.BROWSER, - title: 'Top browsers', - linkText: 'Browser', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.Browser, - dateRange, - }, - }, - }, - { - id: DeviceTab.OS, - title: 'Top OSs', - linkText: 'OS', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.OS, - dateRange, - }, - }, - }, { id: DeviceTab.DEVICE_TYPE, - title: 'Top device types', - linkText: 'Device type', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.DeviceType, - dateRange, - }, - }, - }, - ], - }, - { - layout: { - colSpan: 6, - }, - activeTabId: geographyTab, - setTabId: actions.setGeographyTab, - tabs: [ - { - id: GeographyTab.MAP, - title: 'World map', - linkText: 'Map', + title: 'Top Device Types', + linkText: 'Device Type', query: { kind: NodeKind.InsightVizNode, source: { kind: NodeKind.TrendsQuery, - breakdown: { - breakdown: '$geoip_country_code', - breakdown_type: 'person', - }, + breakdown: { breakdown: '$device_type', breakdown_type: 'event' }, dateRange, series: [ { @@ -506,62 +467,192 @@ export const webAnalyticsLogic = kea([ }, ], trendsFilter: { - display: ChartDisplayType.WorldMap, + display: ChartDisplayType.ActionsPie, + show_labels_on_series: true, }, filterTestAccounts: true, properties: webAnalyticsFilters, }, hidePersonsModal: true, - }, - }, - { - id: GeographyTab.COUNTRIES, - title: 'Top countries', - linkText: 'Countries', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.Country, - dateRange, + vizSpecificOptions: { + [ChartDisplayType.ActionsPie]: { + disableHoverOffset: true, + hideAggregation: true, + }, }, }, }, { - id: GeographyTab.REGIONS, - title: 'Top regions', - linkText: 'Regions', + id: DeviceTab.BROWSER, + title: 'Top browsers', + linkText: 'Browser', query: { full: true, kind: NodeKind.DataTableNode, source: { kind: NodeKind.WebStatsTableQuery, properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.Region, + breakdownBy: WebStatsBreakdown.Browser, dateRange, }, }, }, { - id: GeographyTab.CITIES, - title: 'Top cities', - linkText: 'Cities', + id: DeviceTab.OS, + title: 'Top OSs', + linkText: 'OS', query: { full: true, kind: NodeKind.DataTableNode, source: { kind: NodeKind.WebStatsTableQuery, properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.City, + breakdownBy: WebStatsBreakdown.OS, dateRange, }, }, }, ], }, + { + title: 'Retention', + layout: { + colSpan: 12, + }, + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.RetentionQuery, + properties: webAnalyticsFilters, + dateRange, + filterTestAccounts: true, + retentionFilter: { + retention_type: RETENTION_FIRST_TIME, + retention_reference: 'total', + total_intervals: isGreaterThanMd ? 8 : 5, + period: RetentionPeriod.Week, + }, + }, + vizSpecificOptions: { + [InsightType.RETENTION]: { + hideLineGraph: true, + hideSizeColumn: !isGreaterThanMd, + useSmallLayout: !isGreaterThanMd, + }, + }, + }, + }, + shouldShowGeographyTile + ? { + layout: { + colSpan: 12, + }, + activeTabId: geographyTab, + setTabId: actions.setGeographyTab, + tabs: [ + { + id: GeographyTab.MAP, + title: 'World map', + linkText: 'Map', + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + breakdown: { + breakdown: '$geoip_country_code', + breakdown_type: 'person', + }, + dateRange, + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueUsers, + }, + ], + trendsFilter: { + display: ChartDisplayType.WorldMap, + }, + filterTestAccounts: true, + properties: webAnalyticsFilters, + }, + hidePersonsModal: true, + }, + }, + { + id: GeographyTab.COUNTRIES, + title: 'Top countries', + linkText: 'Countries', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.Country, + dateRange, + }, + }, + }, + { + id: GeographyTab.REGIONS, + title: 'Top regions', + linkText: 'Regions', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.Region, + dateRange, + }, + }, + }, + { + id: GeographyTab.CITIES, + title: 'Top cities', + linkText: 'Cities', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.City, + dateRange, + }, + }, + }, + ], + } + : null, ] + return tiles.filter(isNotNil) + }, + ], + hasCountryFilter: [ + (s) => [s.webAnalyticsFilters], + (webAnalyticsFilters: WebAnalyticsPropertyFilters) => { + return webAnalyticsFilters.some((filter) => filter.key === '$geoip_country_code') + }, + ], + hasDeviceTypeFilter: [ + (s) => [s.webAnalyticsFilters], + (webAnalyticsFilters: WebAnalyticsPropertyFilters) => { + return webAnalyticsFilters.some((filter) => filter.key === '$device_type') + }, + ], + hasBrowserFilter: [ + (s) => [s.webAnalyticsFilters], + (webAnalyticsFilters: WebAnalyticsPropertyFilters) => { + return webAnalyticsFilters.some((filter) => filter.key === '$browser') + }, + ], + hasOSFilter: [ + (s) => [s.webAnalyticsFilters], + (webAnalyticsFilters: WebAnalyticsPropertyFilters) => { + return webAnalyticsFilters.some((filter) => filter.key === '$os') }, ], })), @@ -584,16 +675,18 @@ export const webAnalyticsLogic = kea([ // no need to worry about pagination here, event names beginning with $ are reserved, and we're not // going to add enough reserved event names that match this search term to cause problems - const shouldWarnAboutNoPageviews = - pageviewResult.status === 'fulfilled' && - !pageviewResult.value.next && - (pageviewResult.value.count === 0 || - !pageviewResult.value.results.some((r) => r.name === '$pageview')) - const shouldWarnAboutNoPageleaves = - pageleaveResult.status === 'fulfilled' && - !pageleaveResult.value.next && - (pageleaveResult.value.count === 0 || - !pageleaveResult.value.results.some((r) => r.name === '$pageleave')) + const pageviewEntry = + pageviewResult.status === 'fulfilled' + ? pageviewResult.value.results.find((r) => r.name === '$pageview') + : undefined + + const pageleaveEntry = + pageleaveResult.status === 'fulfilled' + ? pageleaveResult.value.results.find((r) => r.name === '$pageleave') + : undefined + + const shouldWarnAboutNoPageviews = !pageviewEntry || isDefinitionStale(pageviewEntry) + const shouldWarnAboutNoPageleaves = !pageleaveEntry || isDefinitionStale(pageleaveEntry) return { shouldWarnAboutNoPageviews, @@ -601,10 +694,56 @@ export const webAnalyticsLogic = kea([ } }, }, + shouldShowGeographyTile: { + _default: null as boolean | null, + loadShouldShowGeographyTile: async (): Promise => { + const [propertiesResponse, pluginsResponse, pluginsConfigResponse] = await Promise.allSettled([ + api.propertyDefinitions.list({ + event_names: ['$pageview'], + properties: ['$geoip_country_code'], + }), + api.loadPaginatedResults('api/organizations/@current/plugins'), + api.loadPaginatedResults('api/plugin_config'), + ]) + + const hasNonStaleCountryCodeDefinition = + propertiesResponse.status === 'fulfilled' && + propertiesResponse.value.results.some( + (property) => property.name === '$geoip_country_code' && !isDefinitionStale(property) + ) + + if (!hasNonStaleCountryCodeDefinition) { + return false + } + + const geoIpPlugin = + pluginsResponse.status === 'fulfilled' && + pluginsResponse.value.find( + (plugin) => plugin.url === 'https://www.npmjs.com/package/@posthog/geoip-plugin' + ) + const geoIpPluginId = geoIpPlugin ? geoIpPlugin.id : undefined + + const geoIpPluginConfig = + isNotNil(geoIpPluginId) && + pluginsConfigResponse.status === 'fulfilled' && + pluginsConfigResponse.value.find((plugin) => plugin.id === geoIpPluginId) + + return !!geoIpPluginConfig && geoIpPluginConfig.enabled + }, + }, })), // start the loaders after mounting the logic afterMount(({ actions }) => { actions.loadStatusCheck() + actions.loadShouldShowGeographyTile() + }), + windowValues({ + isGreaterThanMd: (window: Window) => window.innerWidth > 768, }), ]) + +const isDefinitionStale = (definition: EventDefinition | PropertyDefinition): boolean => { + const parsedLastSeen = definition.last_seen_at ? dayjs(definition.last_seen_at) : null + return !!parsedLastSeen && dayjs().diff(parsedLastSeen, 'seconds') > STALE_EVENT_SECONDS +} diff --git a/frontend/src/styles/fonts.scss b/frontend/src/styles/fonts.scss index acda559f9d01b..60443636e9b6b 100644 --- a/frontend/src/styles/fonts.scss +++ b/frontend/src/styles/fonts.scss @@ -1,6 +1,6 @@ /* Inter; bold (700); latin */ @font-face { - font-family: 'Inter'; + font-family: Inter; font-style: normal; font-weight: 700; font-display: swap; @@ -10,7 +10,7 @@ } @font-face { - font-family: 'Inter'; + font-family: Inter; font-style: normal; font-weight: 500; font-display: swap; @@ -21,7 +21,7 @@ /* Inter; normal (400); latin */ @font-face { - font-family: 'Inter'; + font-family: Inter; font-style: normal; font-weight: 400; font-display: swap; @@ -29,3 +29,39 @@ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } + +/* Matter; bold (800); latin */ +@font-face { + font-family: MatterSQ; + font-style: normal; + font-weight: 800; + font-display: swap; + + // src: url('../../public/MatterSQ-Bold.woff2') format('woff2'), url('../../public/MatterSQ-Bold.woff') format('woff'); + src: url('https://d1sdjtjk6xzm7.cloudfront.net/MatterSQ-Bold.woff2') format('woff2'), + url('https://d1sdjtjk6xzm7.cloudfront.net/MatterSQ-Bold.woff') format('woff'); +} + +/* Matter; bold (700); latin */ +@font-face { + font-family: MatterSQ; + font-style: normal; + font-weight: 700; + font-display: swap; + + // src: url('../../public/MatterSQ-SemiBold.woff2') format('woff2'), url('../../public/MatterSQ-SemiBold.woff') format('woff'); + src: url('https://d1sdjtjk6xzm7.cloudfront.net/MatterSQ-SemiBold.woff2') format('woff2'), + url('https://d1sdjtjk6xzm7.cloudfront.net/MatterSQ-SemiBold.woff') format('woff'); +} + +/* Matter; medium (500); latin */ +@font-face { + font-family: MatterSQ; + font-style: normal; + font-weight: 500; + font-display: swap; + + // src: url('../../public/MatterSQ-Medium.woff2') format('woff2'), url('../../public/MatterSQ-Medium.woff') format('woff'); + src: url('https://d1sdjtjk6xzm7.cloudfront.net/public/MatterSQ-Medium.woff2') format('woff2'), + url('https://d1sdjtjk6xzm7.cloudfront.net/public/MatterSQ-Medium.woff') format('woff'); +} diff --git a/frontend/src/styles/functions.scss b/frontend/src/styles/functions.scss index d3b9636f804ff..4fdd8c8d0b7dc 100644 --- a/frontend/src/styles/functions.scss +++ b/frontend/src/styles/functions.scss @@ -1,11 +1,16 @@ +@use 'sass:math'; + @function escape-number($value) { - $int: floor($value); + $int: math.floor($value); $fract: $value - $int; - @if ($fract == 0) { + + @if $fract == 0 { @return $int; } - @while ($fract != floor($fract)) { + + @while $fract != math.floor($fract) { $fract: $fract * 10; } - @return $int + '\\.'+ $fract; + + @return $int + '\\.'+ $fract; /* stylelint-disable-line scss/operator-no-unspaced */ } diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index f7e1274cc469d..69d3d66238b01 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -7,6 +7,8 @@ Only 400 (`normal`), 500 (`var(--font-medium)`), 600 (`var(--font-semibold)`), o */ +@use 'sass:map'; + // Global components @import '../../../node_modules/react-toastify/dist/ReactToastify'; @import 'fonts'; @@ -26,10 +28,6 @@ body strong { font-weight: 600; } -.main-app-content { - background-color: var(--bg-light); -} - // Disable default styling for ul ul { margin-bottom: 0; @@ -75,8 +73,10 @@ ul { .page-caption { @extend .text-sm; + max-width: 48rem; margin-bottom: 1rem; + &.tabbed { margin-bottom: 0.5rem; } @@ -144,6 +144,7 @@ input::-ms-clear { // Highlight background blink +/* stylelint-disable-next-line keyframes-name-pattern */ @keyframes highlight { 0% { background-color: var(--mark); @@ -156,6 +157,7 @@ input::-ms-clear { // Generic 360 spin +/* stylelint-disable-next-line keyframes-name-pattern */ @keyframes spin { 0% { transform: rotateZ(0deg); @@ -188,6 +190,7 @@ input::-ms-clear { .Toastify__toast-body { @extend .text-sm; @extend .text-default; + font-weight: 500; padding: 0; @@ -243,11 +246,11 @@ input::-ms-clear { } &::before { - box-shadow: 16px 0 16px -16px rgba(0, 0, 0, 0.25) inset; + box-shadow: 16px 0 16px -16px rgb(0 0 0 / 25%) inset; } &::after { - box-shadow: -16px 0 16px -16px rgba(0, 0, 0, 0.25) inset; + box-shadow: -16px 0 16px -16px rgb(0 0 0 / 25%) inset; } &.scrollable--right::after, @@ -270,11 +273,13 @@ input::-ms-clear { label { font-weight: bold; + @extend .text-sm; } .caption { color: var(--muted); + @extend .text-sm; } @@ -390,7 +395,7 @@ input::-ms-clear { cursor: pointer; div:nth-child(1) { - background: var(--primary); + background: var(--primary-3000); } div:nth-child(2) { @@ -466,8 +471,8 @@ input::-ms-clear { .overlay--danger & { background: repeating-linear-gradient( -45deg, - rgba(255, 255, 255, 0.5), - rgba(255, 255, 255, 0.5) 0.75rem, + rgb(255 255 255 / 50%), + rgb(255 255 255 / 50%) 0.75rem, var(--danger) 0.5rem, var(--danger) 20px ); @@ -497,6 +502,7 @@ input::-ms-clear { width: 100%; padding: 0 1rem 1rem; flex: 1; + background-color: var(--bg-light); @include screen($sm) { padding: 0 1rem 2rem; @@ -520,10 +526,13 @@ input::-ms-clear { body { // Until we have 3000 rolled out we fallback to standard colors --text-3000: var(--default); + --text-secondary-3000: var(--text-secondary); --muted-3000: var(--muted); --trace-3000: var(--trace-3000-light); --primary-3000: var(--primary); + --primary-3000-highlight: var(--primary-highlight); --primary-3000-hover: var(--primary-light); + --primary-3000-active: var(--primary-dark); --secondary-3000: var(--secondary); --secondary-3000-hover: var(--secondary-light); --accent-3000: var(--side); @@ -533,16 +542,20 @@ body { --glass-bg-3000: var(--bg-light); --glass-border-3000: var(--border); --bg-light: #fff; - --link: var(--primary); + --bg-table: var(--bg-light); + --link: var(--primary-3000); touch-action: manipulation; // Disable double-tap-to-zoom on mobile, making taps slightly snappier &.posthog-3000[theme='light'] { --text-3000: var(--text-3000-light); + --text-secondary-3000: var(--text-secondary-3000-light); --muted-3000: var(--muted-3000-light); --trace-3000: var(--trace-3000-light); --primary-3000: var(--primary-3000-light); + --primary-3000-highlight: var(--primary-3000-highlight-light); --primary-3000-hover: var(--primary-3000-hover-light); + --primary-3000-active: var(--primary-3000-active-light); --secondary-3000: var(--secondary-3000-light); --secondary-3000-hover: var(--secondary-3000-hover-light); --accent-3000: var(--accent-3000-light); @@ -552,15 +565,31 @@ body { --glass-bg-3000: var(--glass-bg-3000-light); --glass-border-3000: var(--glass-border-3000-light); --bg-light: #fff; + --bg-table: #f9faf7; --link: var(--link-3000-light); + --shadow-elevation-3000: var(--shadow-elevation-3000-light); + --primary-3000-frame-bg: var(--primary-3000-frame-bg-light); + --primary-3000-frame-border: var(--primary-3000-frame-border-light); + --primary-3000-button-bg: var(--primary-3000-button-bg-light); + --primary-3000-button-border: var(--primary-3000-button-border-light); + --primary-3000-button-border-hover: var(--primary-3000-button-border-hover-light); + --primary-alt-highlight-3000: var(--primary-alt-highlight-3000-light); + --secondary-3000-frame-bg: var(--secondary-3000-frame-bg-light); + --secondary-3000-frame-border: var(--secondary-3000-frame-border-light); + --secondary-3000-button-bg: var(--secondary-3000-button-bg-light); + --secondary-3000-button-border: var(--secondary-3000-button-border-light); + --secondary-3000-button-border-hover: var(--secondary-3000-button-border-hover-light); } &.posthog-3000[theme='dark'] { --text-3000: var(--text-3000-dark); + --text-secondary-3000: var(--text-secondary-3000-dark); --muted-3000: var(--muted-3000-dark); --trace-3000: var(--trace-3000-dark); --primary-3000: var(--primary-3000-dark); + --primary-3000-highlight: var(--primary-3000-highlight-dark); --primary-3000-hover: var(--primary-3000-hover-dark); + --primary-3000-active: var(--primary-3000-active-dark); --secondary-3000: var(--secondary-3000-dark); --secondary-3000-hover: var(--secondary-3000-hover-dark); --accent-3000: var(--accent-3000-dark); @@ -570,12 +599,32 @@ body { --glass-bg-3000: var(--glass-bg-3000-dark); --glass-border-3000: var(--glass-border-3000-dark); --bg-light: var(--accent-3000); + --bg-table: #232429; --brand-key: #fff; // In dark mode the black in PostHog's logo is replaced with white for proper contrast --link: var(--link-3000-dark); + --shadow-elevation-3000: var(--shadow-elevation-3000-dark); + --primary-3000-frame-bg: var(--primary-3000-frame-bg-dark); + --primary-3000-frame-border: var(--primary-3000-frame-border-dark); + --primary-3000-button-bg: var(--primary-3000-button-bg-dark); + --primary-3000-button-border: var(--primary-3000-button-border-dark); + --primary-3000-button-border-hover: var(--primary-3000-button-border-hover-dark); + --primary-alt-highlight-3000: var(--primary-alt-highlight-3000-dark); + --secondary-3000-frame-bg: var(--secondary-3000-frame-bg-dark); + --secondary-3000-frame-border: var(--secondary-3000-frame-border-dark); + --secondary-3000-button-bg: var(--secondary-3000-button-bg-dark); + --secondary-3000-button-border: var(--secondary-3000-button-border-dark); + --secondary-3000-button-border-hover: var(--secondary-3000-button-border-hover-dark); + --data-color-2: #7f26d9; + --data-color-3: #3e7a76; + --data-color-4: #bf0d6c; + --data-color-5: #f0474f; + --data-color-6: #b36114; + --data-color-10: #6576b3; } &.posthog-3000 { --default: var(--text-3000); + --text-secondary: var(--text-secondary-3000); --muted: var(--muted-3000); --muted-alt: var(--muted-3000); --primary-alt: var(--text-3000); @@ -583,25 +632,54 @@ body { --border-bold: var(--border-bold-3000); --mid: var(--bg-3000); --side: var(--bg-3000); + --primary-alt-highlight: var(--primary-alt-highlight-3000); + --data-color-1: #1d4aff; + --data-color-10: #35416b; + background: var(--bg-3000); overflow: hidden; // Each area handles scrolling individually (e.g. navbar, scene, side panel) + .LemonButton, + .Link { + .text-link { + color: var(--text-3000); + } + + &:hover { + .text-link { + color: var(--primary-3000); + } + } + } + + --shadow-elevation: var(--shadow-elevation-3000); + * > { ::-webkit-scrollbar { width: 0.5rem; height: 0.5rem; } + ::-webkit-scrollbar-track { background: var(--accent-3000); } + ::-webkit-scrollbar-thumb { border-radius: 0.25rem; background: var(--trace-3000); + &:hover { background: var(--muted-3000); } } } + + h1, + h2, + h3, + h4 { + font-family: var(--font-title); + } } h1, @@ -659,12 +737,15 @@ body { .ant-modal-mask { z-index: var(--z-ant-modal-mask); } + .ant-modal-wrap { z-index: var(--z-ant-modal-wrap); } + .ant-message { z-index: var(--z-ant-message); } + .ant-select-dropdown { z-index: var(--z-ant-select-dropdown); background-color: var(--bg-3000); @@ -730,6 +811,7 @@ body { .ant-table-small .ant-table-thead > tr > th { background: var(--mid); } + .ant-table-tbody > tr > td { border-bottom-color: var(--border); } @@ -749,6 +831,7 @@ body { animation-duration: 0ms !important; animation-iteration-count: 1 !important; } + // Hide some parts of the UI that were causing flakiness ::-webkit-scrollbar, // Scrollbar in WebKit/Blink browsers .LemonTabs__bar::after, // Active tab slider diff --git a/frontend/src/styles/mixins.scss b/frontend/src/styles/mixins.scss index b55a6e3feb337..6fb6db7db668a 100644 --- a/frontend/src/styles/mixins.scss +++ b/frontend/src/styles/mixins.scss @@ -28,7 +28,7 @@ } @mixin screen($breakpoint) { - @if $breakpoint == null { + @if not $breakpoint { @content; } @else { @media screen and (min-width: $breakpoint) { diff --git a/frontend/src/styles/utilities.scss b/frontend/src/styles/utilities.scss index 0f26bf65d4b11..d01caa82c1d77 100644 --- a/frontend/src/styles/utilities.scss +++ b/frontend/src/styles/utilities.scss @@ -1,5 +1,4 @@ @use 'sass:string'; - @import 'vars'; @import 'mixins'; @@ -83,10 +82,7 @@ } .inset-#{escape-number($space)} { - top: #{$space * 0.25}rem; - right: #{$space * 0.25}rem; - bottom: #{$space * 0.25}rem; - left: #{$space * 0.25}rem; + inset: #{$space * 0.25}rem #{$space * 0.25}rem #{$space * 0.25}rem #{$space * 0.25}rem; } .inset-x-#{escape-number($space)} { @@ -115,15 +111,15 @@ } @each $side in $sides { - .m#{str-slice($side, 0, 1)}-#{escape-number($space)} { + .m#{string.slice($side, 0, 1)}-#{escape-number($space)} { margin-#{$side}: #{$space * 0.25}rem; } - .-m#{str-slice($side, 0, 1)}-#{escape-number($space)} { + .-m#{string.slice($side, 0, 1)}-#{escape-number($space)} { margin-#{$side}: #{-$space * 0.25}rem; } - .p#{str-slice($side, 0, 1)}-#{escape-number($space)} { + .p#{string.slice($side, 0, 1)}-#{escape-number($space)} { padding-#{$side}: #{$space * 0.25}rem; } } @@ -132,18 +128,23 @@ .w-px { width: 1px; } + .h-px { height: 1px; } + .min-w-px { min-width: 1px; } + .max-w-px { max-width: 1px; } + .min-h-px { min-height: 1px; } + .max-h-px { max-height: 1px; } @@ -212,7 +213,7 @@ // Margins/padding @each $kind in ('margin', 'padding') { - $char: str-slice($kind, 0, 1); + $char: string.slice($kind, 0, 1); .#{$char}-auto { #{$kind}: auto; @@ -241,11 +242,11 @@ } @each $side in $sides { - .#{$char}#{str-slice($side, 0, 1)}-auto { + .#{$char}#{string.slice($side, 0, 1)}-auto { #{$kind}-#{$side}: auto; } - .#{$char}#{str-slice($side, 0, 1)}-px { + .#{$char}#{string.slice($side, 0, 1)}-px { #{$kind}-#{$side}: 1px; } } @@ -257,89 +258,117 @@ // Border .border-0 { - border-width: 0px; + border-width: 0; } + .border-2 { border-width: 2px; } + .border-4 { border-width: 4px; } + .border-6 { border-width: 6px; } + .border-8 { border-width: 8px; } + .border-t-0 { - border-top-width: 0px; + border-top-width: 0; } + .border-t-2 { border-top-width: 2px; } + .border-t-4 { border-top-width: 4px; } + .border-t-6 { border-top-width: 6px; } + .border-t-8 { border-top-width: 8px; } + .border-t { border-top-width: 1px; } + .border-r-0 { - border-right-width: 0px; + border-right-width: 0; } + .border-r-2 { border-right-width: 2px; } + .border-r-4 { border-right-width: 4px; } + .border-r-6 { border-right-width: 6px; } + .border-r-8 { border-right-width: 8px; } + .border-r { border-right-width: 1px; } + .border-b-0 { - border-bottom-width: 0px; + border-bottom-width: 0; } + .border-b-2 { border-bottom-width: 2px; } + .border-b-4 { border-bottom-width: 4px; } + .border-b-6 { border-bottom-width: 6px; } + .border-b-8 { border-bottom-width: 8px; } + .border-b { border-bottom-width: 1px; } + .border-l-0 { - border-left-width: 0px; + border-left-width: 0; } + .border-l-2 { border-left-width: 2px; } + .border-l-4 { border-left-width: 4px; } + .border-l-6 { border-left-width: 6px; } + .border-l-8 { border-left-width: 8px; } + .border-l { border-left-width: 1px; } @@ -347,23 +376,29 @@ .border-solid { border-style: solid; } + .border-dashed { border-style: dashed; } + .border-dotted { border-style: dotted; } + .border-double { border-style: double; } + .border-hidden { border-style: hidden; } + .border-none { border-style: none; } $decorations: underline, overline, line-through, no-underline; + @each $decoration in $decorations { .#{$decoration} { text-decoration-line: $decoration; @@ -376,9 +411,11 @@ $decorations: underline, overline, line-through, no-underline; .decoration-inherit { text-decoration-color: inherit; } + .decoration-current { text-decoration-color: currentColor; } + .decoration-transparent { text-decoration-color: transparent; } @@ -429,7 +466,7 @@ $decorations: underline, overline, line-through, no-underline; // Widths / heights @each $kind in ('width', 'height') { - $char: str-slice($kind, 1, 1); + $char: string.slice($kind, 1, 1); .#{$char}-auto { #{$kind}: auto; } @@ -437,7 +474,7 @@ $decorations: underline, overline, line-through, no-underline; #{$kind}: 100%; } .#{$char}-screen { - #{$kind}: unquote('100v' + $char); + #{$kind}: string.unquote('100v' + $char); } .#{$char}-min { #{$kind}: min-content; @@ -453,14 +490,14 @@ $decorations: underline, overline, line-through, no-underline; min-#{$kind}: 100%; } .min-#{$char}-screen { - min-#{$kind}: unquote('100v' + $char); + min-#{$kind}: string.unquote('100v' + $char); } .max-#{$char}-full { max-#{$kind}: 100%; } .max-#{$char}-screen { - max-#{$kind}: unquote('100v' + $char); + max-#{$kind}: string.unquote('100v' + $char); } @each $variant in ('', 'max-', 'min-') { @@ -469,11 +506,11 @@ $decorations: underline, overline, line-through, no-underline; } .#{$variant}#{$char}-1\/3 { - #{$variant}#{$kind}: 33.333333%; + #{$variant}#{$kind}: 33.3333%; } .#{$variant}#{$char}-2\/3 { - #{$variant}#{$kind}: 66.666667%; + #{$variant}#{$kind}: 66.6667%; } .#{$variant}#{$char}-1\/4 { @@ -640,18 +677,23 @@ $decorations: underline, overline, line-through, no-underline; .justify-start { justify-content: flex-start; } + .justify-end { justify-content: flex-end; } + .justify-center { justify-content: center; } + .justify-between { justify-content: space-between; } + .justify-around { justify-content: space-around; } + .justify-evenly { justify-content: space-evenly; } @@ -659,15 +701,19 @@ $decorations: underline, overline, line-through, no-underline; .items-start { align-items: flex-start; } + .items-end { align-items: flex-end; } + .items-center { align-items: center; } + .items-baseline { align-items: baseline; } + .items-stretch { align-items: stretch; } @@ -675,18 +721,23 @@ $decorations: underline, overline, line-through, no-underline; .self-auto { align-self: auto; } + .self-start { align-self: flex-start; } + .self-end { align-self: flex-end; } + .self-center { align-self: center; } + .self-stretch { align-self: stretch; } + .self-baseline { align-self: baseline; } @@ -805,33 +856,43 @@ $decorations: underline, overline, line-through, no-underline; .font-thin { font-weight: 100; } + .font-extralight { font-weight: 200; } + .font-light { font-weight: 300; } + .font-normal { font-weight: 400; } + .font-medium { font-weight: 500; } + .font-semibold { font-weight: 600; } + .font-bold { font-weight: 700; } + .font-extrabold { font-weight: 800; } + .font-black { font-weight: 900; } + .italic { font-style: italic; } + .not-italic { font-style: normal; } @@ -852,15 +913,19 @@ $decorations: underline, overline, line-through, no-underline; .whitespace-normal { white-space: normal; } + .whitespace-nowrap { white-space: nowrap; } + .whitespace-pre { white-space: pre; } + .whitespace-pre-line { white-space: pre-line; } + .whitespace-pre-wrap { white-space: pre-wrap; } @@ -869,54 +934,67 @@ $decorations: underline, overline, line-through, no-underline; font-size: 0.625rem; /* 10px */ line-height: 0.75rem; /* 12px */ } + .text-xs { font-size: 0.75rem; /* 12px */ line-height: 1rem; /* 16px */ } + .text-sm { font-size: 0.875rem; /* 14px */ line-height: 1.25rem; /* 20px */ } + .text-base { font-size: 1rem; /* 16px */ line-height: 1.5rem; /* 24px */ } + .text-lg { font-size: 1.125rem; /* 18px */ line-height: 1.75rem; /* 28px */ } + .text-xl { font-size: 1.25rem; /* 20px */ line-height: 1.75rem; /* 28px */ } + .text-2xl { font-size: 1.5rem; /* 24px */ line-height: 2rem; /* 32px */ } + .text-3xl { font-size: 1.875rem; /* 30px */ line-height: 2.25rem; /* 36px */ } + .text-4xl { font-size: 2.25rem; /* 36px */ line-height: 2.5rem; /* 40px */ } + .text-5xl { font-size: 3rem; /* 48px */ line-height: 1; } + .text-6xl { font-size: 3.75rem; /* 60px */ line-height: 1; } + .text-7xl { font-size: 4.5rem; /* 72px */ line-height: 1; } + .text-8xl { font-size: 6rem; /* 96px */ line-height: 1; } + .text-9xl { font-size: 8rem; /* 128px */ line-height: 1; @@ -931,18 +1009,22 @@ $decorations: underline, overline, line-through, no-underline; .rounded { border-radius: var(--radius); /* 4px */ } + .rounded-l { border-top-left-radius: var(--radius); /* 4px */ border-bottom-left-radius: var(--radius); /* 4px */ } + .rounded-r { border-top-right-radius: var(--radius); /* 4px */ border-bottom-right-radius: var(--radius); /* 4px */ } + .rounded-t { border-top-left-radius: var(--radius); /* 4px */ border-top-right-radius: var(--radius); /* 4px */ } + .rounded-b { border-bottom-left-radius: var(--radius); /* 4px */ border-bottom-right-radius: var(--radius); /* 4px */ @@ -951,10 +1033,12 @@ $decorations: underline, overline, line-through, no-underline; .rounded-none { border-radius: 0; } + .rounded-r-none { border-top-right-radius: 0; border-bottom-right-radius: 0; } + .rounded-l-none { border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -967,21 +1051,27 @@ $decorations: underline, overline, line-through, no-underline; .rounded-md { border-radius: calc(var(--radius) * 1.5); /* 6px */ } + .rounded-lg { border-radius: calc(var(--radius) * 2); /* 8px */ } + .rounded-xl { border-radius: calc(var(--radius) * 3); /* 12px */ } + .rounded-2xl { border-radius: calc(var(--radius) * 4); /* 16px */ } + .rounded-3xl { border-radius: calc(var(--radius) * 6); /* 24px */ } + .rounded-4xl { border-radius: calc(var(--radius) * 8); /* 32px */ } + .rounded-full { border-radius: 1000rem; } @@ -989,30 +1079,39 @@ $decorations: underline, overline, line-through, no-underline; .overflow-auto { overflow: auto; } + .overflow-hidden { overflow: hidden; } + .overflow-clip { overflow: clip; } + .overflow-visible { overflow: visible; } + .overflow-scroll { overflow: scroll; } + .overflow-x-scroll { overflow-x: scroll; } + .overflow-y-scroll { overflow-y: scroll; } + .overflow-x-auto { overflow-x: auto; } + .overflow-y-auto { overflow-y: auto; } + .overflow-x-hidden { overflow-x: hidden; } @@ -1028,45 +1127,59 @@ $decorations: underline, overline, line-through, no-underline; .opacity-0 { opacity: 0; } + .opacity-5 { opacity: 0.05; } + .opacity-10 { opacity: 0.1; } + .opacity-20 { opacity: 0.2; } + .opacity-25 { opacity: 0.25; } + .opacity-30 { opacity: 0.3; } + .opacity-40 { opacity: 0.4; } + .opacity-50 { opacity: 0.5; } + .opacity-60 { opacity: 0.6; } + .opacity-70 { opacity: 0.7; } + .opacity-75 { opacity: 0.75; } + .opacity-80 { opacity: 0.8; } + .opacity-90 { opacity: 0.9; } + .opacity-95 { opacity: 0.95; } + .opacity-100 { opacity: 1; } @@ -1074,6 +1187,7 @@ $decorations: underline, overline, line-through, no-underline; .pointer-events-none { pointer-events: none; } + .pointer-events-auto { pointer-events: auto; } @@ -1083,9 +1197,11 @@ $decorations: underline, overline, line-through, no-underline; text-overflow: ellipsis; white-space: nowrap; } + .text-ellipsis { text-overflow: ellipsis; } + .text-clip { text-overflow: clip; } @@ -1105,6 +1221,7 @@ $decorations: underline, overline, line-through, no-underline; .list-inside { list-style-position: inside; } + .list-outside { list-style-position: outside; } @@ -1125,18 +1242,23 @@ $decorations: underline, overline, line-through, no-underline; .tracking-tighter { letter-spacing: -0.05em; } + .tracking-tight { letter-spacing: -0.025em; } + .tracking-normal { - letter-spacing: 0em; + letter-spacing: 0; } + .tracking-wide { letter-spacing: 0.025em; } + .tracking-wider { letter-spacing: 0.05em; } + .tracking-widest { letter-spacing: 0.1em; } @@ -1144,12 +1266,15 @@ $decorations: underline, overline, line-through, no-underline; .select-none { user-select: none; } + .select-text { user-select: text; } + .select-all { user-select: all; } + .select-auto { user-select: auto; } @@ -1157,6 +1282,7 @@ $decorations: underline, overline, line-through, no-underline; .rotate-90 { transform: rotate(90deg); } + .rotate-180 { transform: rotate(180deg); } @@ -1197,6 +1323,14 @@ $decorations: underline, overline, line-through, no-underline; visibility: hidden; } +.resize { + resize: both; +} + +.resize-x { + resize: horizontal; +} + .resize-y { resize: vertical; } diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss index db90cf9e7ce60..76bef66e65647 100644 --- a/frontend/src/styles/vars.scss +++ b/frontend/src/styles/vars.scss @@ -1,11 +1,12 @@ @use 'sass:list'; +@use 'sass:map'; +@use 'sass:color'; $sm: 576px; $md: 768px; $lg: 992px; $xl: 1200px; $xxl: 1600px; - $screens: ( 'sm': $sm, 'md': $md, @@ -13,13 +14,13 @@ $screens: ( 'xl': $xl, 'xxl': $xxl, ); - $tiny_spaces: 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20; $humongous_spaces: 24, 30, 32, 40, 50, 60, 80, 100, 120, 140, 160, 180, 200; $all_spaces: list.join($tiny_spaces, $humongous_spaces); $flex_sizes: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; $leadings: 3, 4, 5, 6, 7, 8, 9, 10; $sides: 'top', 'right', 'bottom', 'left'; + // CSS cursors from https://tailwindcss.com/docs/cursor $cursors: ( 'auto', @@ -59,6 +60,7 @@ $cursors: ( 'zoom-in', 'zoom-out' ); + // CSS list style types from https://tailwindcss.com/docs/list-style-type $list_style_types: 'none', 'disc', 'decimal'; @@ -96,12 +98,12 @@ $colors: ( 'bg-light': #fff, 'side': #fafaf9, 'mid': #f2f2f2, - 'border': rgba(0, 0, 0, 0.15), - 'border-light': rgba(0, 0, 0, 0.08), - 'border-bold': rgba(0, 0, 0, 0.24), - 'border-active': rgba(0, 0, 0, 0.36), + 'border': rgb(0 0 0 / 15%), + 'border-light': rgb(0 0 0 / 8%), + 'border-bold': rgb(0 0 0 / 24%), + 'border-active': rgb(0 0 0 / 36%), 'transparent': transparent, - 'link': var(--link), + 'link': var(--primary-3000), // Colors of the PostHog logo 'brand-blue': #1d4aff, 'brand-red': #f54e00, @@ -111,35 +113,76 @@ $colors: ( // This becomes white in dark mode // PostHog 3000 'text-3000-light': #111, + 'text-secondary-3000-light': rgba(#111, 0.7), 'muted-3000-light': rgba(#111, 0.5), 'trace-3000-light': rgba(#111, 0.25), - 'primary-3000-light': rgba(#000, 0.75), - 'primary-3000-hover-light': #000, + 'primary-3000-light': #f54e01, + 'primary-3000-highlight-light': rgba(#f54e01, 0.1), + 'primary-3000-hover-light': #f54e01, + 'primary-3000-active-light': #f54e01, + 'secondary-3000-light': rgba(#cfd1c2, 0.6), 'secondary-3000-hover-light': #cfd1c2, 'accent-3000-light': #eeefe9, 'bg-3000-light': #f3f4ef, + 'bg-hover-3000-light': #f3f4ef, 'border-3000-light': #dadbd2, 'border-bold-3000-light': #c1c2b9, 'glass-bg-3000-light': #e4e5deb3, 'glass-border-3000-light': #e4e5de, - 'link-3000-light': var(--primary), + + 'link-3000-light': #f54e00, + 'primary-3000-frame-bg-light': #eb9d2a, + 'primary-3000-frame-border-light': #c28926, + 'primary-3000-button-bg-light': #fff, + 'primary-3000-button-border-light': #b17816, + 'primary-3000-button-border-hover-light': #8e5b03, + + 'secondary-3000-frame-bg-light': #e1dddd, + 'secondary-3000-frame-border-light': #d7d7d7, + 'secondary-3000-button-bg-light': #f3f4ef, + 'secondary-3000-button-border-light': #ccc, + 'secondary-3000-button-border-hover-light': #aaa, + + 'shadow-elevation-3000-light': 0 2px 0 var(--border-3000-light), + 'shadow-elevation-3000-dark': 0 2px 0 var(--border-3000-dark), 'text-3000-dark': #fff, + 'text-secondary-3000-dark': rgba(#fff, 0.7), 'muted-3000-dark': rgba(#fff, 0.5), 'trace-3000-dark': rgba(#fff, 0.25), - 'primary-3000-dark': var(--primary), - 'primary-3000-hover-dark': var(--primary-light), - 'secondary-3000-dark': #3b4159, + 'primary-3000-dark': #f7a503, + 'primary-3000-highlight-dark': rgba(#f7a503, 0.1), + 'primary-3000-hover-dark': #f7a503, + 'primary-3000-active-dark': #f7a503, + 'primary-alt-highlight-3000-light': #e5e7e0, + + 'secondary-3000-dark': #1d1f27, 'secondary-3000-hover-dark': #575d77, - 'accent-3000-dark': #1d1f27, - 'bg-3000-dark': #151619, - 'border-3000-dark': #2b2c32, + 'accent-3000-dark': #21242b, + 'bg-3000-dark': #1d1f27, + 'bg-hover-3000-dark': #292b36, + 'border-3000-dark': #35373e, 'border-bold-3000-dark': #3f4046, - 'glass-bg-3000-dark': #1d1f27b3, + 'glass-bg-3000-dark': #21242bb3, 'glass-border-3000-dark': var(--border-3000-dark), - 'link-3000-dark': rgb(47, 129, 247), + 'link-3000-dark': #f1a82c, + + 'primary-3000-frame-bg-dark': #926826, + 'primary-3000-frame-border-dark': #a97a2f, + 'primary-3000-button-bg-dark': #e0a045, + 'primary-3000-button-border-dark': #b17816, + 'primary-3000-button-border-hover-dark': #8e5b03, + 'primary-alt-highlight-3000-dark': #232429, + + 'secondary-3000-frame-bg-dark': #323232, + 'secondary-3000-frame-border-dark': #383838, + 'secondary-3000-button-bg-dark': #1d1f27, + 'secondary-3000-button-border-dark': #4a4c52, + 'secondary-3000-button-border-hover-dark': #5e6064, + // The derived colors 'text-3000': var(--text-3000), + 'text-secondary-3000': var(--text-secondary-3000), 'muted-3000': var(--muted-3000), 'trace-3000': var(--trace-3000), 'primary-3000': var(--primary-3000), @@ -148,18 +191,29 @@ $colors: ( 'secondary-3000-hover': var(--secondary-3000-hover), 'accent-3000': var(--accent-3000), 'bg-3000': var(--bg-3000), + 'bg-hover-3000': var(--bg-hover-3000), 'border-3000': var(--border-3000), 'border-bold-3000': var(--border-bold-3000), 'glass-bg-3000': var(--glass-bg-3000), - 'glass-border-3000': var(--glass-border-3000), + 'glass-border-3000': var(--border-3000), 'link-3000': var(--link-3000), // 'bg-light': var(--accent-3000), + 'primary-3000-frame-bg': var(--primary-3000-frame-bg), + 'primary-3000-frame-border': var(--primary-3000-frame-border), + 'primary-3000-button-bg': var(--primary-3000-button-bg), + 'primary-3000-button-border': var(--primary-3000-button-border), + 'primary-3000-button-border-hover': var(--primary-3000-button-border-hover), + 'secondary-3000-frame-bg': var(--secondary-3000-frame-bg), + 'secondary-3000-frame-border': var(--secondary-3000-frame-border), + 'secondary-3000-button-bg': var(--secondary-3000-button-bg), + 'secondary-3000-button-border': var(--secondary-3000-button-border), + 'secondary-3000-button-border-hover': var(--secondary-3000-button-border-hover), ); // These vars are modified via SCSS for legacy reasons (e.g. darken/lighten), so keeping as SCSS vars for now. -$_primary: map-get($colors, 'primary'); -$_success: map-get($colors, 'success'); -$_danger: map-get($colors, 'danger'); +$_primary: map.get($colors, 'primary'); +$_success: map.get($colors, 'success'); +$_danger: map.get($colors, 'danger'); $_primary_bg_hover: rgba($_primary, 0.1); $_primary_bg_active: rgba($_primary, 0.2); $_lifecycle_new: $_primary; @@ -175,22 +229,22 @@ $_lifecycle_dormant: $_danger; --#{$name}: #{$hex}; } - //TODO: Remove the primary-bg... + // TODO: Remove the primary-bg... --primary-bg-hover: var(--primary-highlight); --primary-bg-active: #{$_primary_bg_active}; - --bg-charcoal: #2d2d2d; --bg-bridge: #ebece8; // Non-color vars --radius: 4px; - --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - --shadow-elevation: 0px 16px 16px -16px rgba(0, 0, 0, 0.35); + --shadow-elevation: 0px 16px 16px -16px rgb(0 0 0 / 35%); --opacity-disabled: 0.6; --font-medium: 500; --font-semibold: 600; - --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', 'Helvetica Neue', Helvetica, Arial, + --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', 'Helvetica Neue', helvetica, arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --font-title: 'MatterSQ', -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', 'Helvetica Neue', + helvetica, arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; --font-mono: ui-monospace, 'SFMono-Regular', 'SF Mono', 'Menlo', 'Consolas', 'Liberation Mono', monospace; // Dashboard item colors @@ -202,35 +256,35 @@ $_lifecycle_dormant: $_danger; // Tag colors --purple-light: #dcb1e3; - // Data colors (e.g. insight series). Note: colors.ts relies on these values being hexadecimal - --data-brand-blue: var(--primary); - --data-purple: #621da6; - --data-viridian: #42827e; - --data-magenta: #ce0e74; - --data-vermilion: #f14f58; - --data-brown: #7c440e; - --data-green: #529a0a; - --data-blue: #0476fb; - --data-pink: #fe729e; - --data-navy: var(--primary-alt); - --data-turquoise: #41cbc4; - --data-brick: #b64b02; - --data-yellow: #e4a604; - --data-lilac: #a56eff; + //// Data colors (e.g. insight series). Note: colors.ts relies on these values being hexadecimal + --data-color-1: #1d4aff; + --data-color-2: #621da6; + --data-color-3: #42827e; + --data-color-4: #ce0e74; + --data-color-5: #f14f58; + --data-color-6: #7c440e; + --data-color-7: #529a0a; + --data-color-8: #0476fb; + --data-color-9: #fe729e; + --data-color-10: #35416b; + --data-color-11: #41cbc4; + --data-color-12: #b64b02; + --data-color-13: #e4a604; + --data-color-14: #a56eff; + --data-color-15: #30d5c8; // Lifecycle series --lifecycle-new: #{$_lifecycle_new}; --lifecycle-returning: #{$_lifecycle_returning}; --lifecycle-resurrecting: #{$_lifecycle_resurrecting}; --lifecycle-dormant: #{$_lifecycle_dormant}; - --lifecycle-new-hover: #{darken($_lifecycle_new, 20%)}; - --lifecycle-returning-hover: #{darken($_lifecycle_returning, 20%)}; - --lifecycle-resurrecting-hover: #{darken($_lifecycle_resurrecting, 20%)}; - --lifecycle-dormant-hover: #{darken($_lifecycle_dormant, 20%)}; + --lifecycle-new-hover: #{color.adjust($_lifecycle_new, $lightness: -20%)}; + --lifecycle-returning-hover: #{color.adjust($_lifecycle_returning, $lightness: -20%)}; + --lifecycle-resurrecting-hover: #{color.adjust($_lifecycle_resurrecting, $lightness: -20%)}; + --lifecycle-dormant-hover: #{color.adjust($_lifecycle_dormant, $lightness: -20%)}; // Funnels // TODO: unify with lib/colors.ts, getGraphColors() - --funnel-default: var(--primary); --funnel-background: var(--border-light); --funnel-axis: var(--border); --funnel-grid: #ddd; @@ -243,7 +297,7 @@ $_lifecycle_dormant: $_danger; --recording-seekbar-red: var(--brand-red); --recording-hover-event: var(--primary-bg-hover); --recording-hover-event-mid: var(--primary-bg-active); - --recording-hover-event-dark: var(--primary); + --recording-hover-event-dark: var(--primary-3000); --recording-current-event: #eef2ff; --recording-current-event-dark: var(--primary-alt); --recording-failure-event: #fee9e2; @@ -281,7 +335,7 @@ $_lifecycle_dormant: $_danger; // which means they aren't available in the toolbar --toastify-color-dark: var(--accent-3000-dark); --toastify-color-light: var(--bg-light); - --toastify-color-info: var(--primary); + --toastify-color-info: var(--primary-3000); --toastify-color-success: var(--success); --toastify-color-warning: var(--warning); --toastify-color-error: var(--danger); @@ -294,9 +348,8 @@ $_lifecycle_dormant: $_danger; --toastify-color-progress-warning: var(--toastify-color-warning); --toastify-color-progress-error: var(--toastify-color-error); - //In App Prompts + // In-app prompts --in-app-prompts-width: 26rem; - --lettermark-1-bg: #dcb1e3; --lettermark-1-text: #572e5e; --lettermark-2-bg: #ffc4b2; @@ -313,12 +366,10 @@ $_lifecycle_dormant: $_danger; --lettermark-7-text: #35416b; --lettermark-8-bg: #ff906e; --lettermark-8-text: #2a3d65; - --lettermark-8-bg: #e8edff; - --lettermark-8-text: #35416b; // Modals --modal-backdrop-blur: 5px; // Half the value in Figma as blur is calculated differently there it seems - --modal-backdrop-color: rgba(0, 0, 0, 0.2); + --modal-backdrop-color: rgb(0 0 0 / 20%); --modal-transition-time: 200ms; // Notebooks diff --git a/frontend/src/test/init.ts b/frontend/src/test/init.ts index dc896f740e8c9..13597c8eebb25 100644 --- a/frontend/src/test/init.ts +++ b/frontend/src/test/init.ts @@ -1,13 +1,15 @@ -import { initKea } from '~/initKea' -import { testUtilsPlugin } from 'kea-test-utils' import { createMemoryHistory } from 'history' -import posthog from 'posthog-js' -import { AppContext, TeamType } from '~/types' +import { testUtilsPlugin } from 'kea-test-utils' import { MOCK_DEFAULT_TEAM } from 'lib/api.mock' import { dayjs } from 'lib/dayjs' +import posthog from 'posthog-js' import { organizationLogic } from 'scenes/organizationLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { teamLogic } from 'scenes/teamLogic' +import { initKea } from '~/initKea' +import { AppContext, TeamType } from '~/types' + process.on('unhandledRejection', (err) => { console.warn(err) }) @@ -33,6 +35,7 @@ export function initKeaTests(mountCommonLogic = true, teamForWindowContext: Team ;(history as any).replaceState = history.replace initKea({ beforePlugins: [testUtilsPlugin], routerLocation: history.location, routerHistory: history }) if (mountCommonLogic) { + preflightLogic.mount() teamLogic.mount() organizationLogic.mount() } diff --git a/frontend/src/test/mocks.ts b/frontend/src/test/mocks.ts index c58aa57b47f29..dcd926d4f1e7a 100644 --- a/frontend/src/test/mocks.ts +++ b/frontend/src/test/mocks.ts @@ -1,3 +1,7 @@ +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { PROPERTY_MATCH_TYPE } from 'lib/constants' +import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' + import { BehavioralEventType, CohortType, @@ -13,9 +17,6 @@ import { TimeUnitType, UserBasicType, } from '~/types' -import { PROPERTY_MATCH_TYPE } from 'lib/constants' -import { BehavioralFilterKey } from 'scenes/cohorts/CohortFilters/types' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' export const mockBasicUser: UserBasicType = { id: 0, diff --git a/frontend/src/toolbar/Toolbar.stories.tsx b/frontend/src/toolbar/Toolbar.stories.tsx index 35ea9e6dd2e01..a0c98724f3230 100644 --- a/frontend/src/toolbar/Toolbar.stories.tsx +++ b/frontend/src/toolbar/Toolbar.stories.tsx @@ -1,12 +1,12 @@ import '~/styles' import '~/toolbar/styles.scss' -import { useEffect } from 'react' import { Meta } from '@storybook/react' +import { useEffect } from 'react' +import { useStorybookMocks } from '~/mocks/browser' import { ToolbarApp } from '~/toolbar/ToolbarApp' import { ToolbarParams } from '~/types' -import { useStorybookMocks } from '~/mocks/browser' const toolbarParams: ToolbarParams = { temporaryToken: 'UExb1dCsoqBtrhrZYxzmxXQ7XdjVH5Ea_zbQjTFuJqk', @@ -20,10 +20,10 @@ const toolbarParams: ToolbarParams = { const meta: Meta = { title: 'Scenes-Other/Toolbar', + tags: ['test-skip'], // This story is not valuable to snapshot as is parameters: { layout: 'fullscreen', viewMode: 'story', - testOptions: { skip: true }, // This story is not valuable to snapshot as is }, } export default meta diff --git a/frontend/src/toolbar/ToolbarApp.tsx b/frontend/src/toolbar/ToolbarApp.tsx index e50a73ac8301e..0b8e6c1790d79 100644 --- a/frontend/src/toolbar/ToolbarApp.tsx +++ b/frontend/src/toolbar/ToolbarApp.tsx @@ -1,11 +1,12 @@ -import { useRef, useState } from 'react' +import { useValues } from 'kea' import { useSecondRender } from 'lib/hooks/useSecondRender' +import { useRef, useState } from 'react' import root from 'react-shadow' +import { Slide, ToastContainer } from 'react-toastify' + import { ToolbarContainer } from '~/toolbar/ToolbarContainer' -import { useValues } from 'kea' import { toolbarLogic } from '~/toolbar/toolbarLogic' import { ToolbarProps } from '~/types' -import { Slide, ToastContainer } from 'react-toastify' type HTMLElementWithShadowRoot = HTMLElement & { shadowRoot: ShadowRoot } diff --git a/frontend/src/toolbar/ToolbarContainer.tsx b/frontend/src/toolbar/ToolbarContainer.tsx index b184ccfdde469..e6aebff4342e9 100644 --- a/frontend/src/toolbar/ToolbarContainer.tsx +++ b/frontend/src/toolbar/ToolbarContainer.tsx @@ -1,8 +1,9 @@ import { useValues } from 'kea' -import { Elements } from '~/toolbar/elements/Elements' +import { Fade } from 'lib/components/Fade/Fade' + import { DraggableButton } from '~/toolbar/button/DraggableButton' +import { Elements } from '~/toolbar/elements/Elements' import { toolbarLogic } from '~/toolbar/toolbarLogic' -import { Fade } from 'lib/components/Fade/Fade' export function ToolbarContainer(): JSX.Element { const { buttonVisible } = useValues(toolbarLogic) diff --git a/frontend/src/toolbar/actions/ActionsList.tsx b/frontend/src/toolbar/actions/ActionsList.tsx index ab6ef76c72dd0..d8cdfb7c3ffb8 100644 --- a/frontend/src/toolbar/actions/ActionsList.tsx +++ b/frontend/src/toolbar/actions/ActionsList.tsx @@ -1,11 +1,12 @@ import { useActions, useValues } from 'kea' +import { IconPlus } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' + +import { ActionsListView } from '~/toolbar/actions/ActionsListView' import { actionsLogic } from '~/toolbar/actions/actionsLogic' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' -import { ActionsListView } from '~/toolbar/actions/ActionsListView' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconPlus } from 'lib/lemon-ui/icons' export function ActionsList(): JSX.Element { const { allActions, sortedActions, allActionsLoading, searchTerm } = useValues(actionsLogic) diff --git a/frontend/src/toolbar/actions/ActionsListView.tsx b/frontend/src/toolbar/actions/ActionsListView.tsx index 7f2a25c0db10f..2293a594c5d31 100644 --- a/frontend/src/toolbar/actions/ActionsListView.tsx +++ b/frontend/src/toolbar/actions/ActionsListView.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' +import { Spinner } from 'lib/lemon-ui/Spinner' + +import { actionsLogic } from '~/toolbar/actions/actionsLogic' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { ActionType } from '~/types' -import { actionsLogic } from '~/toolbar/actions/actionsLogic' -import { Spinner } from 'lib/lemon-ui/Spinner' interface ActionsListViewProps { actions: ActionType[] diff --git a/frontend/src/toolbar/actions/ActionsTab.scss b/frontend/src/toolbar/actions/ActionsTab.scss index 1ab083acc90a9..6a6a0f8866ec5 100644 --- a/frontend/src/toolbar/actions/ActionsTab.scss +++ b/frontend/src/toolbar/actions/ActionsTab.scss @@ -8,9 +8,11 @@ .action-section { &.highlight { - background: hsla(228, 14%, 96%, 1); + background: hsl(228deg 14% 96% / 100%); + &:nth-child(even) { background: white; + &:last-child { padding-bottom: 10px; } @@ -28,7 +30,7 @@ .action-field-caption { font-size: 0.8em; - color: rgba(0, 0, 0, 0.5); + color: rgb(0 0 0 / 50%); padding: 8px 0; } } diff --git a/frontend/src/toolbar/actions/ActionsTab.tsx b/frontend/src/toolbar/actions/ActionsTab.tsx index 981eb79a15877..71d06c73c87de 100644 --- a/frontend/src/toolbar/actions/ActionsTab.tsx +++ b/frontend/src/toolbar/actions/ActionsTab.tsx @@ -1,12 +1,13 @@ import './ActionsTab.scss' +import { Link } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { toolbarLogic } from '~/toolbar/toolbarLogic' +import { urls } from 'scenes/urls' + import { ActionsList } from '~/toolbar/actions/ActionsList' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { EditAction } from '~/toolbar/actions/EditAction' -import { urls } from 'scenes/urls' -import { Link } from '@posthog/lemon-ui' +import { toolbarLogic } from '~/toolbar/toolbarLogic' export function ActionsTab(): JSX.Element { const { selectedAction } = useValues(actionsTabLogic) diff --git a/frontend/src/toolbar/actions/ButtonWindow.stories.tsx b/frontend/src/toolbar/actions/ButtonWindow.stories.tsx index 3b98177f17f05..b31e1a3e9f13c 100644 --- a/frontend/src/toolbar/actions/ButtonWindow.stories.tsx +++ b/frontend/src/toolbar/actions/ButtonWindow.stories.tsx @@ -1,14 +1,15 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' import { useMountedLogic } from 'kea' + import { useStorybookMocks } from '~/mocks/browser' -import { useToolbarStyles } from '~/toolbar/Toolbar.stories' -import { toolbarLogic } from '~/toolbar/toolbarLogic' -import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' +import { actionsLogic } from '~/toolbar/actions/actionsLogic' import { ActionsTab } from '~/toolbar/actions/ActionsTab' +import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { ButtonWindow } from '~/toolbar/button/ButtonWindow' import { FeatureFlags } from '~/toolbar/flags/FeatureFlags' import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic' -import { actionsLogic } from '~/toolbar/actions/actionsLogic' +import { useToolbarStyles } from '~/toolbar/Toolbar.stories' +import { toolbarLogic } from '~/toolbar/toolbarLogic' interface StoryProps { contents: JSX.Element name: string diff --git a/frontend/src/toolbar/actions/EditAction.tsx b/frontend/src/toolbar/actions/EditAction.tsx index 409f439565704..782c2c10b42c7 100644 --- a/frontend/src/toolbar/actions/EditAction.tsx +++ b/frontend/src/toolbar/actions/EditAction.tsx @@ -1,13 +1,14 @@ import { useActions, useValues } from 'kea' +import { Field, Form, Group } from 'kea-forms' +import { IconClose, IconDelete, IconEdit, IconMagnifier, IconMinusOutlined, IconPlus } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' + import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { StepField } from '~/toolbar/actions/StepField' import { SelectorEditingModal } from '~/toolbar/elements/SelectorEditingModal' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { IconClose, IconDelete, IconEdit, IconMagnifier, IconMinusOutlined, IconPlus } from 'lib/lemon-ui/icons' import { posthog } from '~/toolbar/posthog' import { getShadowRootPopoverContainer } from '~/toolbar/utils' -import { Field, Form, Group } from 'kea-forms' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' export function EditAction(): JSX.Element { const { diff --git a/frontend/src/toolbar/actions/SelectorCount.tsx b/frontend/src/toolbar/actions/SelectorCount.tsx index 20a2b764d894f..205620c655d5c 100644 --- a/frontend/src/toolbar/actions/SelectorCount.tsx +++ b/frontend/src/toolbar/actions/SelectorCount.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react' import { querySelectorAllDeep } from 'query-selector-shadow-dom' +import { useMemo } from 'react' interface SelectorCountProps { selector: string diff --git a/frontend/src/toolbar/actions/StepField.tsx b/frontend/src/toolbar/actions/StepField.tsx index b066bae78d239..42729674f99c9 100644 --- a/frontend/src/toolbar/actions/StepField.tsx +++ b/frontend/src/toolbar/actions/StepField.tsx @@ -1,14 +1,15 @@ -import { SelectorCount } from '~/toolbar/actions/SelectorCount' -import { cssEscape } from 'lib/utils/cssEscape' -import { ActionStepForm } from '~/toolbar/types' -import { URL_MATCHING_HINTS } from 'scenes/actions/hints' +import clsx from 'clsx' import { Field } from 'kea-forms' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { StringMatching } from '~/types' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonSegmentedButton } from 'lib/lemon-ui/LemonSegmentedButton' import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import clsx from 'clsx' +import { cssEscape } from 'lib/utils/cssEscape' +import { URL_MATCHING_HINTS } from 'scenes/actions/hints' + +import { SelectorCount } from '~/toolbar/actions/SelectorCount' +import { ActionStepForm } from '~/toolbar/types' +import { StringMatching } from '~/types' interface StepFieldProps { item: 'href' | 'text' | 'selector' | 'url' diff --git a/frontend/src/toolbar/actions/actionsLogic.test.ts b/frontend/src/toolbar/actions/actionsLogic.test.ts index a832eaf32e536..7c52916927a73 100644 --- a/frontend/src/toolbar/actions/actionsLogic.test.ts +++ b/frontend/src/toolbar/actions/actionsLogic.test.ts @@ -1,4 +1,5 @@ import { expectLogic } from 'kea-test-utils' + import { initKeaTests } from '~/test/init' import { actionsLogic } from '~/toolbar/actions/actionsLogic' import { toolbarLogic } from '~/toolbar/toolbarLogic' diff --git a/frontend/src/toolbar/actions/actionsLogic.ts b/frontend/src/toolbar/actions/actionsLogic.ts index 63057d6330b29..68c101a7fb532 100644 --- a/frontend/src/toolbar/actions/actionsLogic.ts +++ b/frontend/src/toolbar/actions/actionsLogic.ts @@ -1,10 +1,12 @@ +import Fuse from 'fuse.js' +import { actions, kea, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' -import { kea, path, actions, reducers, selectors } from 'kea' + import { toolbarLogic } from '~/toolbar/toolbarLogic' -import type { actionsLogicType } from './actionsLogicType' -import { ActionType } from '~/types' -import Fuse from 'fuse.js' import { toolbarFetch } from '~/toolbar/utils' +import { ActionType } from '~/types' + +import type { actionsLogicType } from './actionsLogicType' export const actionsLogic = kea([ path(['toolbar', 'actions', 'actionsLogic']), @@ -62,9 +64,7 @@ export const actionsLogic = kea([ .search(searchTerm) .map(({ item }) => item) : allActions - return [...filteredActions].sort((a, b) => - (a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled') - ) as ActionType[] + return [...filteredActions].sort((a, b) => (a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled')) }, ], actionCount: [(s) => [s.allActions], (allActions) => allActions.length], diff --git a/frontend/src/toolbar/actions/actionsTabLogic.tsx b/frontend/src/toolbar/actions/actionsTabLogic.tsx index c19cecf532c63..0a0ab5235c938 100644 --- a/frontend/src/toolbar/actions/actionsTabLogic.tsx +++ b/frontend/src/toolbar/actions/actionsTabLogic.tsx @@ -1,17 +1,19 @@ -import { kea, path, actions, connect, reducers, selectors, listeners } from 'kea' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { urls } from 'scenes/urls' + import { actionsLogic } from '~/toolbar/actions/actionsLogic' -import { actionStepToActionStepFormItem, elementToActionStep, stepToDatabaseFormat } from '~/toolbar/utils' -import { toolbarLogic } from '~/toolbar/toolbarLogic' import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' -import type { actionsTabLogicType } from './actionsTabLogicType' -import { ActionType, ElementType } from '~/types' -import { ActionDraftType, ActionForm } from '~/toolbar/types' import { posthog } from '~/toolbar/posthog' -import { lemonToast } from 'lib/lemon-ui/lemonToast' -import { urls } from 'scenes/urls' -import { forms } from 'kea-forms' -import { subscriptions } from 'kea-subscriptions' +import { toolbarLogic } from '~/toolbar/toolbarLogic' +import { ActionDraftType, ActionForm } from '~/toolbar/types' +import { actionStepToActionStepFormItem, elementToActionStep, stepToDatabaseFormat } from '~/toolbar/utils' +import { ActionType, ElementType } from '~/types' + +import type { actionsTabLogicType } from './actionsTabLogicType' function newAction(element: HTMLElement | null, dataAttributes: string[] = []): ActionDraftType { return { diff --git a/frontend/src/toolbar/button/ButtonWindow.tsx b/frontend/src/toolbar/button/ButtonWindow.tsx index 5e93376c75f47..bba87b81030e8 100644 --- a/frontend/src/toolbar/button/ButtonWindow.tsx +++ b/frontend/src/toolbar/button/ButtonWindow.tsx @@ -1,7 +1,7 @@ +import { LemonButton } from '@posthog/lemon-ui' import { Fade } from 'lib/components/Fade/Fade' -import Draggable from 'react-draggable' import { IconClose } from 'lib/lemon-ui/icons' -import { LemonButton } from '@posthog/lemon-ui' +import Draggable from 'react-draggable' interface ButtonWindowProps { name: string diff --git a/frontend/src/toolbar/button/DraggableButton.tsx b/frontend/src/toolbar/button/DraggableButton.tsx index 392dc31b27b44..08f66d767ed71 100644 --- a/frontend/src/toolbar/button/DraggableButton.tsx +++ b/frontend/src/toolbar/button/DraggableButton.tsx @@ -1,13 +1,15 @@ -import { ToolbarButton } from '~/toolbar/button/ToolbarButton' -import Draggable from 'react-draggable' -import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' import { useActions, useValues } from 'kea' -import { HeatmapStats } from '~/toolbar/stats/HeatmapStats' +import Draggable from 'react-draggable' + import { ActionsTab } from '~/toolbar/actions/ActionsTab' import { ButtonWindow } from '~/toolbar/button/ButtonWindow' -import { posthog } from '~/toolbar/posthog' +import { ToolbarButton } from '~/toolbar/button/ToolbarButton' +import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' import { FeatureFlags } from '~/toolbar/flags/FeatureFlags' import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic' +import { posthog } from '~/toolbar/posthog' +import { HeatmapStats } from '~/toolbar/stats/HeatmapStats' + import { HedgehogButton } from './HedgehogButton' export function DraggableButton(): JSX.Element { diff --git a/frontend/src/toolbar/button/HedgehogButton.tsx b/frontend/src/toolbar/button/HedgehogButton.tsx index 44f1b20ae2d0b..026b6d6c30717 100644 --- a/frontend/src/toolbar/button/HedgehogButton.tsx +++ b/frontend/src/toolbar/button/HedgehogButton.tsx @@ -1,10 +1,12 @@ -import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' import { useActions, useValues } from 'kea' import { HedgehogActor, HedgehogBuddy } from 'lib/components/HedgehogBuddy/HedgehogBuddy' import { SPRITE_SIZE } from 'lib/components/HedgehogBuddy/sprites/sprites' -import { toolbarLogic } from '../toolbarLogic' import { useEffect, useRef } from 'react' + +import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' + import { heatmapLogic } from '../elements/heatmapLogic' +import { toolbarLogic } from '../toolbarLogic' export function HedgehogButton(): JSX.Element { const { hedgehogMode, extensionPercentage } = useValues(toolbarButtonLogic) diff --git a/frontend/src/toolbar/button/ToolbarButton.scss b/frontend/src/toolbar/button/ToolbarButton.scss index a082973bbdfda..2ea811c6abdaf 100644 --- a/frontend/src/toolbar/button/ToolbarButton.scss +++ b/frontend/src/toolbar/button/ToolbarButton.scss @@ -8,22 +8,20 @@ .circle-button { transition: box-shadow 0.2s ease; - box-shadow: 0 0.3px 0.7px rgba(0, 0, 0, 0.25), 0 0.8px 1.7px rgba(0, 0, 0, 0.18), - 0 1.5px 3.1px rgba(0, 0, 0, 0.149), 0 2.7px 5.4px rgba(0, 0, 0, 0.125), 0 5px 10px rgba(0, 0, 0, 0.101), - 0 12px 24px rgba(0, 0, 0, 0.07); + box-shadow: 0 0.3px 0.7px rgb(0 0 0 / 25%), 0 0.8px 1.7px rgb(0 0 0 / 18%), 0 1.5px 3.1px rgb(0 0 0 / 14.9%), + 0 2.7px 5.4px rgb(0 0 0 / 12.5%), 0 5px 10px rgb(0 0 0 / 10.1%), 0 12px 24px rgb(0 0 0 / 7%); &:hover { - box-shadow: 0 1.3px 2.7px rgba(0, 0, 0, 0.25), 0 3.2px 6.4px rgba(0, 0, 0, 0.18), - 0 6px 12px rgba(0, 0, 0, 0.149), 0 10.7px 21.4px rgba(0, 0, 0, 0.125), - 0 20.1px 40.1px rgba(0, 0, 0, 0.101), 0 48px 96px rgba(0, 0, 0, 0.07); + box-shadow: 0 1.3px 2.7px rgb(0 0 0 / 25%), 0 3.2px 6.4px rgb(0 0 0 / 18%), 0 6px 12px rgb(0 0 0 / 14.9%), + 0 10.7px 21.4px rgb(0 0 0 / 12.5%), 0 20.1px 40.1px rgb(0 0 0 / 10.1%), 0 48px 96px rgb(0 0 0 / 7%); } } .circle-label { font-weight: bold; - text-shadow: black 0 0 1px, black 0 0 2px, rgba(0, 0, 0, 0.25) 0 0.3px 0.7px, rgba(0, 0, 0, 0.18) 0 0.8px 1.6px, - rgba(0, 0, 0, 0.15) 0 1.5px 3px, rgba(0, 0, 0, 0.125) 0 2.7px 5.4px, rgba(0, 0, 0, 0.1) 0 5px 10px, - rgba(0, 0, 0, 0.07) 0 12px 24px; + text-shadow: black 0 0 1px, black 0 0 2px, rgb(0 0 0 / 25%) 0 0.3px 0.7px, rgb(0 0 0 / 18%) 0 0.8px 1.6px, + rgb(0 0 0 / 15%) 0 1.5px 3px, rgb(0 0 0 / 12.5%) 0 2.7px 5.4px, rgb(0 0 0 / 10%) 0 5px 10px, + rgb(0 0 0 / 7%) 0 12px 24px; } & + .HedgehogBuddy { @@ -37,11 +35,11 @@ left: 0; top: 0; z-index: 2147483020; - box-shadow: 0 0.3px 0.7px rgba(0, 0, 0, 0.25), 0 0.8px 1.7px rgba(0, 0, 0, 0.18), 0 1.5px 3.1px rgba(0, 0, 0, 0.149), - 0 2.7px 5.4px rgba(0, 0, 0, 0.125), 0 5px 10px rgba(0, 0, 0, 0.101), 0 12px 24px rgba(0, 0, 0, 0.07); + box-shadow: 0 0.3px 0.7px rgb(0 0 0 / 25%), 0 0.8px 1.7px rgb(0 0 0 / 18%), 0 1.5px 3.1px rgb(0 0 0 / 14.9%), + 0 2.7px 5.4px rgb(0 0 0 / 12.5%), 0 5px 10px rgb(0 0 0 / 10.1%), 0 12px 24px rgb(0 0 0 / 7%); opacity: 1; transition: opacity ease 0.5s; - background-color: rgba(255, 255, 255); + background-color: rgb(255 255 255); border-radius: 4px; border: 1px solid var(--border); @@ -54,17 +52,16 @@ line-height: 26px; display: flex; align-items: center; - margin: 0 8px 0 8px; + margin: 0 8px; border-bottom: 1px solid var(--border); .toolbar-info-window-draggable { - cursor: move; flex: 1; display: flex; width: 100%; align-items: center; cursor: move; - padding: 15px 0 15px 0; + padding: 15px 0; .window-label { font-size: 14px; @@ -77,11 +74,11 @@ .close-button { cursor: pointer; padding: 8px; - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); transition: color 0.1s ease-in-out; &:hover { - color: rgb(0, 0, 0); + color: rgb(0 0 0); } } } diff --git a/frontend/src/toolbar/button/ToolbarButton.tsx b/frontend/src/toolbar/button/ToolbarButton.tsx index b4d3e9bc38a6d..564a6689e6224 100644 --- a/frontend/src/toolbar/button/ToolbarButton.tsx +++ b/frontend/src/toolbar/button/ToolbarButton.tsx @@ -1,23 +1,24 @@ import './ToolbarButton.scss' -import { useRef, useEffect } from 'react' import { useActions, useValues } from 'kea' +import { useLongPress } from 'lib/hooks/useLongPress' +import { IconHelpOutline, IconTarget } from 'lib/lemon-ui/icons' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { useEffect, useRef } from 'react' + +import { actionsLogic } from '~/toolbar/actions/actionsLogic' +import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { Logomark } from '~/toolbar/assets/Logomark' import { Circle } from '~/toolbar/button/Circle' +import { Close } from '~/toolbar/button/icons/Close' +import { Fire } from '~/toolbar/button/icons/Fire' +import { Flag } from '~/toolbar/button/icons/Flag' +import { Magnifier } from '~/toolbar/button/icons/Magnifier' import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' +import { elementsLogic } from '~/toolbar/elements/elementsLogic' import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' import { toolbarLogic } from '~/toolbar/toolbarLogic' import { getShadowRoot, getShadowRootPopoverContainer } from '~/toolbar/utils' -import { elementsLogic } from '~/toolbar/elements/elementsLogic' -import { useLongPress } from 'lib/hooks/useLongPress' -import { Flag } from '~/toolbar/button/icons/Flag' -import { Fire } from '~/toolbar/button/icons/Fire' -import { Magnifier } from '~/toolbar/button/icons/Magnifier' -import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' -import { actionsLogic } from '~/toolbar/actions/actionsLogic' -import { Close } from '~/toolbar/button/icons/Close' -import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { IconHelpOutline, IconTarget } from 'lib/lemon-ui/icons' const HELP_URL = 'https://posthog.com/docs/user-guides/toolbar?utm_medium=in-product&utm_campaign=toolbar-help-button' diff --git a/frontend/src/toolbar/button/icons/Fire.scss b/frontend/src/toolbar/button/icons/Fire.scss index 46596648187a6..4b13e407b311a 100644 --- a/frontend/src/toolbar/button/icons/Fire.scss +++ b/frontend/src/toolbar/button/icons/Fire.scss @@ -1,55 +1,66 @@ svg.posthog-toolbar-icon-fire.animated { path:nth-child(1) { - animation: posthHogToolbarIconOnFire1 0.5s infinite; + animation: Toolbar__fire-1 0.5s infinite; } + path:nth-child(2) { - animation: posthHogToolbarIconOnFire2 0.5s infinite; + animation: Toolbar__fire-2 0.5s infinite; } + path:nth-child(3) { - animation: posthHogToolbarIconOnFire3 0.5s infinite; + animation: Toolbar__fire-3 0.5s infinite; } } -@keyframes posthHogToolbarIconOnFire1 { +@keyframes Toolbar__fire-1 { 0% { fill: #fb4f0e; } + 33.3% { fill: #fe6d37; } + 66.6% { fill: #fcb811; } + 100% { fill: #fb4f0e; } } -@keyframes posthHogToolbarIconOnFire2 { +@keyframes Toolbar__fire-2 { 0% { fill: #fe6d37; } + 33.3% { fill: #fcb811; } + 66.6% { fill: #fb4f0e; } + 100% { fill: #fe6d37; } } -@keyframes posthHogToolbarIconOnFire3 { +@keyframes Toolbar__fire-3 { 0% { fill: #fcb811; } + 33.3% { fill: #fb4f0e; } + 66.6% { fill: #fe6d37; } + 100% { fill: #fcb811; } diff --git a/frontend/src/toolbar/button/icons/Flag.scss b/frontend/src/toolbar/button/icons/Flag.scss index 0ef3a9549e7d7..92a0749cf20b0 100644 --- a/frontend/src/toolbar/button/icons/Flag.scss +++ b/frontend/src/toolbar/button/icons/Flag.scss @@ -1,55 +1,66 @@ svg.posthog-toolbar-icon-flag.animated { path.color1 { - animation: posthHogToolbarIconOnFlag1 0.5s infinite; + animation: Toolbar__flag-1 0.5s infinite; } + path.color2 { - animation: posthHogToolbarIconOnFlag2 0.5s infinite; + animation: Toolbar__flag-2 0.5s infinite; } + path.color3 { - animation: posthHogToolbarIconOnFlag3 0.5s infinite; + animation: Toolbar__flag-3 0.5s infinite; } } -@keyframes posthHogToolbarIconOnFlag1 { +@keyframes Toolbar__flag-1 { 0% { fill: #70aa54; } + 33.3% { fill: #527940; } + 66.6% { fill: #415e32; } + 100% { fill: #70aa54; } } -@keyframes posthHogToolbarIconOnFlag2 { +@keyframes Toolbar__flag-2 { 0% { fill: #527940; } + 33.3% { fill: #415e32; } + 66.6% { fill: #70aa54; } + 100% { fill: #527940; } } -@keyframes posthHogToolbarIconOnFlag3 { +@keyframes Toolbar__flag-3 { 0% { fill: #415e32; } + 33.3% { fill: #70aa54; } + 66.6% { fill: #527940; } + 100% { fill: #415e32; } diff --git a/frontend/src/toolbar/button/toolbarButtonLogic.ts b/frontend/src/toolbar/button/toolbarButtonLogic.ts index f8d34f07ba4aa..68a0f3b7609d6 100644 --- a/frontend/src/toolbar/button/toolbarButtonLogic.ts +++ b/frontend/src/toolbar/button/toolbarButtonLogic.ts @@ -1,12 +1,14 @@ +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { windowValues } from 'kea-window-values' -import { kea, path, connect, actions, reducers, selectors, listeners } from 'kea' -import { inBounds } from '~/toolbar/utils' -import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' -import { elementsLogic } from '~/toolbar/elements/elementsLogic' +import { HedgehogActor } from 'lib/components/HedgehogBuddy/HedgehogBuddy' + import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' -import type { toolbarButtonLogicType } from './toolbarButtonLogicType' +import { elementsLogic } from '~/toolbar/elements/elementsLogic' +import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' import { posthog } from '~/toolbar/posthog' -import { HedgehogActor } from 'lib/components/HedgehogBuddy/HedgehogBuddy' +import { inBounds } from '~/toolbar/utils' + +import type { toolbarButtonLogicType } from './toolbarButtonLogicType' export const toolbarButtonLogic = kea([ path(['toolbar', 'button', 'toolbarButtonLogic']), diff --git a/frontend/src/toolbar/elements/ElementInfo.tsx b/frontend/src/toolbar/elements/ElementInfo.tsx index ffa34a43788d0..1c8e59b7b5809 100644 --- a/frontend/src/toolbar/elements/ElementInfo.tsx +++ b/frontend/src/toolbar/elements/ElementInfo.tsx @@ -1,10 +1,11 @@ +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { IconCalendar, IconPlus } from 'lib/lemon-ui/icons' + +import { ActionsListView } from '~/toolbar/actions/ActionsListView' import { ActionStep } from '~/toolbar/elements/ActionStep' -import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' import { elementsLogic } from '~/toolbar/elements/elementsLogic' -import { ActionsListView } from '~/toolbar/actions/ActionsListView' -import { LemonButton } from '@posthog/lemon-ui' -import { IconCalendar, IconPlus } from 'lib/lemon-ui/icons' +import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' function ElementStatistic({ prefix, diff --git a/frontend/src/toolbar/elements/Elements.scss b/frontend/src/toolbar/elements/Elements.scss index a3972935ddee7..a558e6b375ec1 100644 --- a/frontend/src/toolbar/elements/Elements.scss +++ b/frontend/src/toolbar/elements/Elements.scss @@ -1,5 +1,5 @@ #posthog-toolbar-elements, #posthog-infowindow-container { - color: #333333; + color: #333; pointer-events: none; } diff --git a/frontend/src/toolbar/elements/Elements.tsx b/frontend/src/toolbar/elements/Elements.tsx index b65605cad3706..f78e50445ecbd 100644 --- a/frontend/src/toolbar/elements/Elements.tsx +++ b/frontend/src/toolbar/elements/Elements.tsx @@ -1,15 +1,16 @@ import './Elements.scss' -import React from 'react' import { useActions, useValues } from 'kea' -import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' +import { compactNumber } from 'lib/utils' +import React from 'react' + +import { elementsLogic } from '~/toolbar/elements/elementsLogic' import { FocusRect } from '~/toolbar/elements/FocusRect' -import { InfoWindow } from '~/toolbar/elements/InfoWindow' import { HeatmapElement } from '~/toolbar/elements/HeatmapElement' import { HeatmapLabel } from '~/toolbar/elements/HeatmapLabel' -import { elementsLogic } from '~/toolbar/elements/elementsLogic' +import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' +import { InfoWindow } from '~/toolbar/elements/InfoWindow' import { getBoxColors, getHeatMapHue } from '~/toolbar/utils' -import { compactNumber } from 'lib/utils' export function Elements(): JSX.Element { const { diff --git a/frontend/src/toolbar/elements/HeatmapLabel.tsx b/frontend/src/toolbar/elements/HeatmapLabel.tsx index c8bac3793a2d1..5b1a77d301afb 100644 --- a/frontend/src/toolbar/elements/HeatmapLabel.tsx +++ b/frontend/src/toolbar/elements/HeatmapLabel.tsx @@ -1,5 +1,5 @@ -import { inBounds } from '~/toolbar/utils' import { ElementRect } from '~/toolbar/types' +import { inBounds } from '~/toolbar/utils' const heatmapLabelStyle = { lineHeight: '14px', diff --git a/frontend/src/toolbar/elements/InfoWindow.tsx b/frontend/src/toolbar/elements/InfoWindow.tsx index 7a5c39515e87a..a8ba8876e36f8 100644 --- a/frontend/src/toolbar/elements/InfoWindow.tsx +++ b/frontend/src/toolbar/elements/InfoWindow.tsx @@ -1,8 +1,9 @@ import { useActions, useValues } from 'kea' -import { elementsLogic } from '~/toolbar/elements/elementsLogic' -import { ElementInfo } from '~/toolbar/elements/ElementInfo' import { IconClose } from 'lib/lemon-ui/icons' +import { ElementInfo } from '~/toolbar/elements/ElementInfo' +import { elementsLogic } from '~/toolbar/elements/elementsLogic' + export function InfoWindow(): JSX.Element | null { const { hoverElement, diff --git a/frontend/src/toolbar/elements/SelectorEditingModal.tsx b/frontend/src/toolbar/elements/SelectorEditingModal.tsx index ee574eed6b7ac..3f3966915cfa5 100644 --- a/frontend/src/toolbar/elements/SelectorEditingModal.tsx +++ b/frontend/src/toolbar/elements/SelectorEditingModal.tsx @@ -1,8 +1,9 @@ -import { getShadowRootPopoverContainer } from '~/toolbar/utils' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { HTMLElementsDisplay } from 'lib/components/HTMLElementsDisplay/HTMLElementsDisplay' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { useState } from 'react' + +import { getShadowRootPopoverContainer } from '~/toolbar/utils' import { ElementType } from '~/types' export const SelectorEditingModal = ({ diff --git a/frontend/src/toolbar/elements/elementsLogic.ts b/frontend/src/toolbar/elements/elementsLogic.ts index ede7fd0a99ae2..9aaa7a665a8b7 100644 --- a/frontend/src/toolbar/elements/elementsLogic.ts +++ b/frontend/src/toolbar/elements/elementsLogic.ts @@ -1,16 +1,17 @@ -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { collectAllElementsDeep } from 'query-selector-shadow-dom' import { actionsLogic } from '~/toolbar/actions/actionsLogic' -import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' -import { elementToActionStep, getAllClickTargets, getElementForStep, getRectForElement } from '~/toolbar/utils' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' -import type { elementsLogicType } from './elementsLogicType' -import { ActionElementWithMetadata, ElementWithMetadata } from '~/toolbar/types' +import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' +import { posthog } from '~/toolbar/posthog' import { currentPageLogic } from '~/toolbar/stats/currentPageLogic' import { toolbarLogic } from '~/toolbar/toolbarLogic' -import { posthog } from '~/toolbar/posthog' -import { collectAllElementsDeep } from 'query-selector-shadow-dom' +import { ActionElementWithMetadata, ElementWithMetadata } from '~/toolbar/types' +import { elementToActionStep, getAllClickTargets, getElementForStep, getRectForElement } from '~/toolbar/utils' + +import type { elementsLogicType } from './elementsLogicType' export type ActionElementMap = Map export type ElementMap = Map diff --git a/frontend/src/toolbar/elements/heatmapLogic.ts b/frontend/src/toolbar/elements/heatmapLogic.ts index 84d7348a42b01..516717c8b701d 100644 --- a/frontend/src/toolbar/elements/heatmapLogic.ts +++ b/frontend/src/toolbar/elements/heatmapLogic.ts @@ -1,17 +1,19 @@ import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' import { encodeParams } from 'kea-router' +import { elementToSelector, escapeRegex } from 'lib/actionUtils' +import { PaginatedResponse } from 'lib/api' +import { dateFilterToText } from 'lib/utils' +import { collectAllElementsDeep, querySelectorAllDeep } from 'query-selector-shadow-dom' + +import { posthog } from '~/toolbar/posthog' import { currentPageLogic } from '~/toolbar/stats/currentPageLogic' -import { elementToActionStep, toolbarFetch, trimElement } from '~/toolbar/utils' import { toolbarLogic } from '~/toolbar/toolbarLogic' -import type { heatmapLogicType } from './heatmapLogicType' import { CountedHTMLElement, ElementsEventType } from '~/toolbar/types' -import { posthog } from '~/toolbar/posthog' -import { collectAllElementsDeep, querySelectorAllDeep } from 'query-selector-shadow-dom' -import { elementToSelector, escapeRegex } from 'lib/actionUtils' +import { elementToActionStep, toolbarFetch, trimElement } from '~/toolbar/utils' import { FilterType, PropertyFilterType, PropertyOperator } from '~/types' -import { PaginatedResponse } from 'lib/api' -import { loaders } from 'kea-loaders' -import { dateFilterToText } from 'lib/utils' + +import type { heatmapLogicType } from './heatmapLogicType' const emptyElementsStatsPages: PaginatedResponse = { next: undefined, @@ -21,9 +23,9 @@ const emptyElementsStatsPages: PaginatedResponse = { export const heatmapLogic = kea([ path(['toolbar', 'elements', 'heatmapLogic']), - connect({ + connect(() => ({ values: [toolbarLogic, ['apiURL']], - }), + })), actions({ getElementStats: (url?: string | null) => ({ url, @@ -184,7 +186,7 @@ export const heatmapLogic = kea([ if (domElements === undefined) { domElements = Array.from( querySelectorAllDeep(combinedSelector, document, cache.pageElements) - ) as HTMLElement[] + ) cache.selectorToElements[combinedSelector] = domElements } diff --git a/frontend/src/toolbar/flags/FeatureFlags.tsx b/frontend/src/toolbar/flags/FeatureFlags.tsx index 05698a0df7082..2da4f27b918a7 100644 --- a/frontend/src/toolbar/flags/FeatureFlags.tsx +++ b/frontend/src/toolbar/flags/FeatureFlags.tsx @@ -1,16 +1,17 @@ import './featureFlags.scss' +import { Link } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic' import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' -import { toolbarLogic } from '~/toolbar/toolbarLogic' -import { urls } from 'scenes/urls' +import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import clsx from 'clsx' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' import { Spinner } from 'lib/lemon-ui/Spinner' -import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' -import { Link } from '@posthog/lemon-ui' +import { urls } from 'scenes/urls' + +import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic' +import { toolbarLogic } from '~/toolbar/toolbarLogic' export function FeatureFlags(): JSX.Element { const { searchTerm, filteredFlags, userFlagsLoading } = useValues(featureFlagsLogic) diff --git a/frontend/src/toolbar/flags/featureFlags.scss b/frontend/src/toolbar/flags/featureFlags.scss index efe87c03b0595..e4f8b80d567a9 100644 --- a/frontend/src/toolbar/flags/featureFlags.scss +++ b/frontend/src/toolbar/flags/featureFlags.scss @@ -1,25 +1,31 @@ .flags-button-window { width: min(350px, 100vw); } + .toolbar-block { .feature-flag-row { .feature-flag-row-header { background-color: #fafafa; min-height: 38px; + > * { margin: 0 5px; } + .feature-flag-title { font-size: 13px; } + .feature-flag-external-link { - color: rgba(0, 0, 0, 0.5); + color: rgb(0 0 0 / 50%); line-height: normal; + &:hover { - color: rgba(0, 0, 0, 0.8); + color: rgb(0 0 0 / 80%); } } } + .feature-flag-row-header.overridden { background-color: var(--mark); } @@ -27,10 +33,12 @@ .variant-radio-group { border-left: 2px solid #fafafa; } + .variant-radio-group.overridden { border-left-color: var(--mark); } } + .local-feature-flag-override-note { color: var(--default); background-color: #fafafa; diff --git a/frontend/src/toolbar/flags/featureFlagsLogic.test.ts b/frontend/src/toolbar/flags/featureFlagsLogic.test.ts index 42bd952951a0c..f0f04d87060dd 100644 --- a/frontend/src/toolbar/flags/featureFlagsLogic.test.ts +++ b/frontend/src/toolbar/flags/featureFlagsLogic.test.ts @@ -1,4 +1,5 @@ import { expectLogic } from 'kea-test-utils' + import { initKeaTests } from '~/test/init' import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic' import { toolbarLogic } from '~/toolbar/toolbarLogic' diff --git a/frontend/src/toolbar/flags/featureFlagsLogic.ts b/frontend/src/toolbar/flags/featureFlagsLogic.ts index c6a31e6b62bc9..a6e21b5458aab 100644 --- a/frontend/src/toolbar/flags/featureFlagsLogic.ts +++ b/frontend/src/toolbar/flags/featureFlagsLogic.ts @@ -1,13 +1,15 @@ -import { loaders } from 'kea-loaders' -import { kea, path, connect, actions, reducers, selectors, listeners, events } from 'kea' -import { CombinedFeatureFlagAndValueType } from '~/types' -import type { featureFlagsLogicType } from './featureFlagsLogicType' -import { toolbarFetch } from '~/toolbar/utils' -import { toolbarLogic } from '~/toolbar/toolbarLogic' import Fuse from 'fuse.js' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { encodeParams } from 'kea-router' import type { PostHog } from 'posthog-js' + import { posthog } from '~/toolbar/posthog' -import { encodeParams } from 'kea-router' +import { toolbarLogic } from '~/toolbar/toolbarLogic' +import { toolbarFetch } from '~/toolbar/utils' +import { CombinedFeatureFlagAndValueType } from '~/types' + +import type { featureFlagsLogicType } from './featureFlagsLogicType' export const featureFlagsLogic = kea([ path(['toolbar', 'flags', 'featureFlagsLogic']), @@ -111,7 +113,7 @@ export const featureFlagsLogic = kea([ toolbarLogic.values.posthog?.featureFlags.reloadFeatureFlags() } }, - deleteOverriddenUserFlag: async ({ flagKey }) => { + deleteOverriddenUserFlag: ({ flagKey }) => { const { posthog: clientPostHog } = toolbarLogic.values if (clientPostHog) { const updatedFlags = { ...values.localOverrides } @@ -128,8 +130,8 @@ export const featureFlagsLogic = kea([ }, })), events(({ actions }) => ({ - afterMount: async () => { - await actions.getUserFlags() + afterMount: () => { + actions.getUserFlags() actions.checkLocalOverrides() }, })), diff --git a/frontend/src/toolbar/index.tsx b/frontend/src/toolbar/index.tsx index b88fb22c6f88e..66f36bd4f45ad 100644 --- a/frontend/src/toolbar/index.tsx +++ b/frontend/src/toolbar/index.tsx @@ -1,11 +1,12 @@ import '~/styles' import './styles.scss' +import { PostHog } from 'posthog-js' import { createRoot } from 'react-dom/client' + import { initKea } from '~/initKea' import { ToolbarApp } from '~/toolbar/ToolbarApp' import { ToolbarParams } from '~/types' -import { PostHog } from 'posthog-js' ;(window as any)['ph_load_toolbar'] = function (toolbarParams: ToolbarParams, posthog: PostHog) { initKea() const container = document.createElement('div') diff --git a/frontend/src/toolbar/stats/HeatmapStats.tsx b/frontend/src/toolbar/stats/HeatmapStats.tsx index ab557cf416774..556c0850ecd62 100644 --- a/frontend/src/toolbar/stats/HeatmapStats.tsx +++ b/frontend/src/toolbar/stats/HeatmapStats.tsx @@ -1,16 +1,17 @@ import { useActions, useValues } from 'kea' -import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' -import { elementsLogic } from '~/toolbar/elements/elementsLogic' -import { getShadowRootPopoverContainer } from '~/toolbar/utils' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' -import { currentPageLogic } from '~/toolbar/stats/currentPageLogic' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconSync } from 'lib/lemon-ui/icons' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch/LemonSwitch' +import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { elementsLogic } from '~/toolbar/elements/elementsLogic' +import { heatmapLogic } from '~/toolbar/elements/heatmapLogic' +import { currentPageLogic } from '~/toolbar/stats/currentPageLogic' +import { getShadowRootPopoverContainer } from '~/toolbar/utils' + export function HeatmapStats(): JSX.Element { const { matchLinksByHref, diff --git a/frontend/src/toolbar/stats/currentPageLogic.ts b/frontend/src/toolbar/stats/currentPageLogic.ts index 97f372f96f899..d637a6a488af3 100644 --- a/frontend/src/toolbar/stats/currentPageLogic.ts +++ b/frontend/src/toolbar/stats/currentPageLogic.ts @@ -1,4 +1,5 @@ -import { kea, path, actions, reducers, events } from 'kea' +import { actions, events, kea, path, reducers } from 'kea' + import type { currentPageLogicType } from './currentPageLogicType' export const currentPageLogic = kea([ diff --git a/frontend/src/toolbar/styles.scss b/frontend/src/toolbar/styles.scss index 11dbbc6e5b881..9e23aa050b7d8 100644 --- a/frontend/src/toolbar/styles.scss +++ b/frontend/src/toolbar/styles.scss @@ -2,8 +2,8 @@ // was inherited from antd.less before removing that *, -*:before, -*:after { +*::before, +*::after { box-sizing: border-box; } @@ -35,7 +35,7 @@ font-size: 12px; text-transform: uppercase; font-weight: bold; - color: hsla(220, 15%, 49%, 1); + color: hsl(220deg 15% 49% / 100%); margin-bottom: 10px; } @@ -50,10 +50,7 @@ .toolbar-global-fade-container { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; pointer-events: none; > * { diff --git a/frontend/src/toolbar/toolbarLogic.test.ts b/frontend/src/toolbar/toolbarLogic.test.ts index 49e36cd19127a..0d4354c3441f6 100644 --- a/frontend/src/toolbar/toolbarLogic.test.ts +++ b/frontend/src/toolbar/toolbarLogic.test.ts @@ -1,7 +1,8 @@ -import { toolbarLogic } from '~/toolbar/toolbarLogic' -import { initKeaTests } from '~/test/init' import { expectLogic } from 'kea-test-utils' +import { initKeaTests } from '~/test/init' +import { toolbarLogic } from '~/toolbar/toolbarLogic' + global.fetch = jest.fn(() => Promise.resolve({ ok: true, diff --git a/frontend/src/toolbar/toolbarLogic.ts b/frontend/src/toolbar/toolbarLogic.ts index 1703bdd4ce3a2..1394e1d1d05f2 100644 --- a/frontend/src/toolbar/toolbarLogic.ts +++ b/frontend/src/toolbar/toolbarLogic.ts @@ -1,12 +1,13 @@ import { actions, afterMount, kea, listeners, path, props, reducers, selectors } from 'kea' -import type { toolbarLogicType } from './toolbarLogicType' -import { ToolbarProps } from '~/types' -import { clearSessionToolbarToken } from '~/toolbar/utils' -import { posthog } from '~/toolbar/posthog' +import { lemonToast } from 'lib/lemon-ui/lemonToast' + import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' -import type { PostHog } from 'posthog-js' -import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { posthog } from '~/toolbar/posthog' +import { clearSessionToolbarToken } from '~/toolbar/utils' +import { ToolbarProps } from '~/types' + +import type { toolbarLogicType } from './toolbarLogicType' export const toolbarLogic = kea([ path(['toolbar', 'toolbarLogic']), @@ -30,8 +31,8 @@ export const toolbarLogic = kea([ userIntent: [props.userIntent || null, { logout: () => null, clearUserIntent: () => null }], source: [props.source || null, { logout: () => null }], buttonVisible: [true, { showButton: () => true, hideButton: () => false, logout: () => false }], - dataAttributes: [(props.dataAttributes || []) as string[]], - posthog: [(props.posthog ?? null) as PostHog | null], + dataAttributes: [props.dataAttributes || []], + posthog: [props.posthog ?? null], })), selectors({ @@ -63,7 +64,7 @@ export const toolbarLogic = kea([ } clearSessionToolbarToken() }, - processUserIntent: async () => { + processUserIntent: () => { if (props.userIntent === 'add-action' || props.userIntent === 'edit-action') { actionsTabLogic.actions.showButtonActions() toolbarButtonLogic.actions.showActionsInfo() diff --git a/frontend/src/toolbar/utils.ts b/frontend/src/toolbar/utils.ts index dc5cf9a9353a9..90b37dde6431f 100644 --- a/frontend/src/toolbar/utils.ts +++ b/frontend/src/toolbar/utils.ts @@ -1,13 +1,14 @@ -import { cssEscape } from 'lib/utils/cssEscape' -import { ActionStepType, StringMatching } from '~/types' -import { ActionStepForm, BoxColor, ElementRect } from '~/toolbar/types' -import { querySelectorAllDeep } from 'query-selector-shadow-dom' -import { toolbarLogic } from '~/toolbar/toolbarLogic' +import { finder } from '@medv/finder' import { combineUrl, encodeParams } from 'kea-router' import { CLICK_TARGET_SELECTOR, CLICK_TARGETS, escapeRegex, TAGS_TO_IGNORE } from 'lib/actionUtils' -import { finder } from '@medv/finder' +import { cssEscape } from 'lib/utils/cssEscape' +import { querySelectorAllDeep } from 'query-selector-shadow-dom' import wildcardMatch from 'wildcard-match' +import { toolbarLogic } from '~/toolbar/toolbarLogic' +import { ActionStepForm, BoxColor, ElementRect } from '~/toolbar/types' +import { ActionStepType, StringMatching } from '~/types' + export function getSafeText(el: HTMLElement): string { if (!el.childNodes || !el.childNodes.length) { return '' diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b66c5c590ec0e..bf9bba96c3194 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,3 +1,10 @@ +import { PluginConfigSchema } from '@posthog/plugin-scaffold' +import { eventWithTime } from '@rrweb/types' +import { UploadFile } from 'antd/lib/upload/interface' +import { ChartDataset, ChartType, InteractionItem } from 'chart.js' +import { LogicWrapper } from 'kea' +import { DashboardCompatibleScenes } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { BIN_COUNT_AUTO, DashboardPrivilegeLevel, @@ -12,20 +19,18 @@ import { ShownAsValue, TeamMembershipLevel, } from 'lib/constants' -import { PluginConfigSchema } from '@posthog/plugin-scaffold' -import { PluginInstallationType } from 'scenes/plugins/types' -import { UploadFile } from 'antd/lib/upload/interface' -import { eventWithTime } from '@rrweb/types' -import { PostHog } from 'posthog-js' -import { PopoverProps } from 'lib/lemon-ui/Popover/Popover' import { Dayjs, dayjs } from 'lib/dayjs' -import { ChartDataset, ChartType, InteractionItem } from 'chart.js' +import { PopoverProps } from 'lib/lemon-ui/Popover/Popover' +import { PostHog } from 'posthog-js' +import { Layout } from 'react-grid-layout' import { LogLevel } from 'rrweb' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { BehavioralFilterKey, BehavioralFilterType } from 'scenes/cohorts/CohortFilters/types' -import { LogicWrapper } from 'kea' import { AggregationAxisFormat } from 'scenes/insights/aggregationAxisFormat' -import { Layout } from 'react-grid-layout' +import { JSONContent } from 'scenes/notebooks/Notebook/utils' +import { PluginInstallationType } from 'scenes/plugins/types' + +import { QueryContext } from '~/queries/types' + import type { DashboardFilter, DatabaseSchemaQueryResponseField, @@ -33,10 +38,6 @@ import type { InsightVizNode, Node, } from './queries/schema' -import { QueryContext } from '~/queries/types' - -import { JSONContent } from 'scenes/notebooks/Notebook/utils' -import { DashboardCompatibleScenes } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' export type Optional = Omit & { [K in keyof T]?: T[K] } @@ -279,6 +280,8 @@ export interface ExplicitTeamMemberType extends BaseMemberType { effective_level: OrganizationMembershipLevel } +export type EitherMemberType = OrganizationMemberType | ExplicitTeamMemberType + /** * While OrganizationMemberType and ExplicitTeamMemberType refer to actual Django models, * this interface is only used in the frontend for fusing the data from these models together. @@ -526,6 +529,12 @@ export enum PipelineTabs { Destinations = 'destinations', } +export enum PipelineAppTabs { + Configuration = 'configuration', + Logs = 'logs', + Metrics = 'metrics', +} + export enum ProgressStatus { Draft = 'draft', Running = 'running', @@ -1142,6 +1151,11 @@ export interface PerformanceEvent { response_headers?: Record request_body?: Body response_body?: Body + method?: string + + //rrweb/network@1 - i.e. not in ClickHouse table + is_initial?: boolean + raw?: Record } export interface CurrentBillCycleType { @@ -1271,6 +1285,7 @@ export interface BillingV2PlanType { current_plan?: any tiers?: BillingV2TierType[] included_if?: 'no_active_subscription' | 'has_subscription' | null + initial_billing_limit?: number } export interface PlanInterface { @@ -1340,7 +1355,7 @@ export interface InsightModel extends Cacheable { description?: string favorited?: boolean order: number | null - result: any | null + result: any deleted: boolean saved: boolean created_at: string @@ -1744,6 +1759,7 @@ export interface TrendsFilterType extends FilterType { aggregation_axis_prefix?: string // a prefix to add to the aggregation axis e.g. £ aggregation_axis_postfix?: string // a postfix to add to the aggregation axis e.g. % show_values_on_series?: boolean + show_labels_on_series?: boolean show_percent_stack_view?: boolean } @@ -2195,6 +2211,7 @@ export interface SurveyAppearance { displayThankYouMessage?: boolean thankYouMessageHeader?: string thankYouMessageDescription?: string + autoDisappear?: boolean position?: string } @@ -2681,6 +2698,11 @@ export interface KeyMapping { system?: boolean } +export interface KeyMappingInterface { + event: Record + element: Record +} + export interface TileParams { title: string targetPath: string @@ -2744,16 +2766,30 @@ export interface DateMappingOption { defaultInterval?: IntervalType } -export interface Breadcrumb { +interface BreadcrumbBase { + /** E.g. scene identifier or item ID. Particularly important if `onRename` is used. */ + key: string | number /** Name to display. */ name: string | null | undefined /** Symbol, e.g. a lettermark or a profile picture. */ symbol?: React.ReactNode - /** Path to link to. */ - path?: string /** Whether to show a custom popover */ popover?: Pick } +interface LinkBreadcrumb extends BreadcrumbBase { + /** Path to link to. */ + path?: string + onRename?: never +} +interface RenamableBreadcrumb extends BreadcrumbBase { + path?: never + /** When this is set, an "Edit" button shows up next to the title */ + onRename?: (newName: string) => Promise +} +export type Breadcrumb = LinkBreadcrumb | RenamableBreadcrumb +export type FinalizedBreadcrumb = + | (LinkBreadcrumb & { globalKey: string }) + | (RenamableBreadcrumb & { globalKey: string }) export enum GraphType { Bar = 'bar', @@ -3387,6 +3423,23 @@ export enum SDKTag { export type SDKInstructionsMap = Partial> +export interface AppMetricsUrlParams { + tab?: AppMetricsTab + from?: string + error?: [string, string] +} + +export enum AppMetricsTab { + Logs = 'logs', + ProcessEvent = 'processEvent', + OnEvent = 'onEvent', + ComposeWebhook = 'composeWebhook', + ExportEvents = 'exportEvents', + ScheduledTask = 'scheduledTask', + HistoricalExports = 'historical_exports', + History = 'history', +} + export enum SidePanelTab { Notebooks = 'notebook', Support = 'support', diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 83decc1fa7bd1..27497f398a6fa 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0015_add_verified_properties otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0363_add_replay_payload_capture_config +posthog: 0366_alter_action_created_by sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/package.json b/package.json index 68051ba75de02..194e4190efbee 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test:visual-regression:legacy:ci:verify": "playwright test", "test:visual-regression:stories": "rm -rf frontend/__snapshots__/__failures__/ && docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:stories:docker", "test:visual-regression:stories:docker": "NODE_OPTIONS=--max-old-space-size=6144 test-storybook -u --no-index-json --browsers chromium webkit --url http://host.docker.internal:6006", + "test:visual-regression:stories:local": "NODE_OPTIONS=--max-old-space-size=6144 test-storybook -u --no-index-json --browsers chromium webkit --url http://localhost:6006", "test:visual-regression:stories:ci:update": "test-storybook -u --no-index-json --maxWorkers=2", "test:visual-regression:stories:ci:verify": "test-storybook --ci --no-index-json --maxWorkers=2", "start": "concurrently -n ESBUILD,TYPEGEN -c yellow,green \"pnpm start-http\" \"pnpm run typegen:watch\"", @@ -38,7 +39,7 @@ "build:esbuild": "node frontend/build.mjs", "schema:build": "pnpm run schema:build:json && pnpm run schema:build:python", "schema:build:json": "ts-json-schema-generator -f tsconfig.json --path 'frontend/src/queries/schema.ts' --no-type-check > frontend/src/queries/schema.json && prettier --write frontend/src/queries/schema.json", - "schema:build:python": "datamodel-codegen --class-name='SchemaRoot' --collapse-root-models --disable-timestamp --use-one-literal-as-default --use-default-kwarg --use-subclass-enum --input frontend/src/queries/schema.json --input-file-type jsonschema --output posthog/schema.py --output-model-type pydantic_v2.BaseModel && ruff format posthog/schema.py", + "schema:build:python": "datamodel-codegen --class-name='SchemaRoot' --collapse-root-models --disable-timestamp --use-one-literal-as-default --use-default --use-default-kwarg --use-subclass-enum --input frontend/src/queries/schema.json --input-file-type jsonschema --output posthog/schema.py --output-model-type pydantic_v2.BaseModel && ruff format posthog/schema.py", "grammar:build": "npm run grammar:build:python && npm run grammar:build:cpp", "grammar:build:python": "cd posthog/hogql/grammar && antlr -Dlanguage=Python3 HogQLLexer.g4 && antlr -visitor -no-listener -Dlanguage=Python3 HogQLParser.g4", "grammar:build:cpp": "cd posthog/hogql/grammar && antlr -o ../../../hogql_parser -Dlanguage=Cpp HogQLLexer.g4 && antlr -o ../../../hogql_parser -visitor -no-listener -Dlanguage=Cpp HogQLParser.g4", @@ -49,14 +50,15 @@ "prettier": "prettier --write \"./**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"", "prettier:check": "prettier --check \"frontend/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"", "typescript:check": "tsc --noEmit && echo \"No errors reported by tsc.\"", - "eslint": "eslint frontend/src", + "lint:js": "eslint frontend/src", + "lint:css": "stylelint \"frontend/**/*.{css,scss}\"", + "format:backend": "ruff --exclude posthog/hogql/grammar .", + "format:frontend": "pnpm lint:js --fix && pnpm lint:css --fix && pnpm prettier", + "format": "pnpm format:backend && pnpm format:frontend", "typegen:write": "kea-typegen write --delete --show-ts-errors", "typegen:check": "kea-typegen check", "typegen:watch": "kea-typegen watch --delete --show-ts-errors", "typegen:clean": "find frontend/src -type f -name '*Type.ts' -delete", - "format:python": "ruff --exclude posthog/hogql/grammar .", - "format:js": "pnpm prettier && pnpm eslint --fix", - "format": "pnpm format:python && pnpm format:js", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "dev:migrate:postgres": "export DEBUG=1 && source env/bin/activate && python manage.py migrate", @@ -74,7 +76,7 @@ "@medv/finder": "^2.1.0", "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.4.6", - "@posthog/icons": "0.2.0", + "@posthog/icons": "0.4.11", "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", "@rrweb/types": "^2.0.0-alpha.11", @@ -134,7 +136,7 @@ "monaco-editor": "^0.39.0", "papaparse": "^5.4.1", "pmtiles": "^2.11.0", - "posthog-js": "1.91.0", + "posthog-js": "1.92.1", "posthog-js-lite": "2.0.0-alpha5", "prettier": "^2.8.8", "prop-types": "^15.7.2", @@ -149,7 +151,7 @@ "react-dom": "^18.2.0", "react-draggable": "^4.2.0", "react-grid-layout": "^1.3.0", - "react-intersection-observer": "^9.4.3", + "react-intersection-observer": "^9.5.3", "react-markdown": "^5.0.3", "react-modal": "^3.15.1", "react-resizable": "^3.0.5", @@ -195,7 +197,7 @@ "@storybook/csf": "^0.1.1", "@storybook/react": "^7.5.1", "@storybook/react-webpack5": "^7.5.1", - "@storybook/test-runner": "^0.13.0", + "@storybook/test-runner": "^0.15.2", "@storybook/theming": "^7.5.1", "@storybook/types": "^7.5.1", "@sucrase/jest-plugin": "^3.0.0", @@ -249,6 +251,7 @@ "eslint-plugin-posthog": "link:./eslint-rules", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react": "^7.33.2", + "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-storybook": "^0.6.15", "file-loader": "^6.1.0", "givens": "^1.3.6", @@ -278,6 +281,9 @@ "storybook": "^7.5.1", "storybook-addon-pseudo-states": "2.1.2", "style-loader": "^2.0.0", + "stylelint": "^15.11.0", + "stylelint-config-standard-scss": "^11.1.0", + "stylelint-order": "^6.0.3", "sucrase": "^3.29.0", "timekeeper": "^2.2.0", "ts-json-schema-generator": "^1.2.0", @@ -291,7 +297,11 @@ "fsevents": "^2.3.2" }, "lint-staged": { - "*.{json,yaml,yml,css,scss}": "prettier --write", + "*.{json,yaml,yml}": "prettier --write", + "*.{css,scss}": [ + "stylelint --fix", + "prettier --write" + ], "(!(plugin-server)/**).{js,jsx,mjs,ts,tsx}": [ "eslint -c .eslintrc.js --fix", "prettier --write" @@ -302,7 +312,7 @@ ], "!(posthog/hogql/grammar/*)*.{py,pyi}": [ "ruff format", - "ruff check" + "ruff check --fix" ] }, "browserslist": { diff --git a/plugin-server/src/utils/status.ts b/plugin-server/src/utils/status.ts index d620bd01b92c6..385b97739685e 100644 --- a/plugin-server/src/utils/status.ts +++ b/plugin-server/src/utils/status.ts @@ -1,5 +1,6 @@ import pino from 'pino' +import { defaultConfig } from '../config/config' import { LogLevel, PluginsServerConfig } from '../types' import { isProdEnv } from './env-utils' @@ -14,7 +15,6 @@ export interface StatusBlueprint { export class Status implements StatusBlueprint { mode?: string - explicitLogLevel?: LogLevel logger: pino.Logger prompt: string transport: any @@ -22,7 +22,7 @@ export class Status implements StatusBlueprint { constructor(mode?: string) { this.mode = mode - const logLevel: LogLevel = this.explicitLogLevel || LogLevel.Info + const logLevel: LogLevel = defaultConfig.LOG_LEVEL if (isProdEnv()) { this.logger = pino({ // By default pino will log the level number. So we can easily unify diff --git a/plugin-server/src/worker/plugins/run.ts b/plugin-server/src/worker/plugins/run.ts index 9775e9c1ef860..e957132313168 100644 --- a/plugin-server/src/worker/plugins/run.ts +++ b/plugin-server/src/worker/plugins/run.ts @@ -111,7 +111,7 @@ async function runSingleTeamPluginComposeWebhook( const request = await trackedFetch(webhook.url, { method: webhook.method || 'POST', body: JSON.stringify(webhook.body, undefined, 4), - headers: { 'Content-Type': 'application/json' }, + headers: webhook.headers || { 'Content-Type': 'application/json' }, timeout: hub.EXTERNAL_REQUEST_TIMEOUT_MS, }) if (request.ok) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbba46296db6e..88e2b0d31b610 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ dependencies: specifier: 4.4.6 version: 4.4.6(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) '@posthog/icons': - specifier: 0.2.0 - version: 0.2.0(react-dom@18.2.0)(react@18.2.0) + specifier: 0.4.11 + version: 0.4.11(react-dom@18.2.0)(react@18.2.0) '@posthog/plugin-scaffold': specifier: ^1.4.4 version: 1.4.4 @@ -216,8 +216,8 @@ dependencies: specifier: ^2.11.0 version: 2.11.0 posthog-js: - specifier: 1.91.0 - version: 1.91.0 + specifier: 1.92.1 + version: 1.92.1 posthog-js-lite: specifier: 2.0.0-alpha5 version: 2.0.0-alpha5 @@ -261,8 +261,8 @@ dependencies: specifier: ^1.3.0 version: 1.3.4(react-dom@18.2.0)(react@18.2.0) react-intersection-observer: - specifier: ^9.4.3 - version: 9.4.3(react@18.2.0) + specifier: ^9.5.3 + version: 9.5.3(react@18.2.0) react-markdown: specifier: ^5.0.3 version: 5.0.3(@types/react@17.0.52)(react@18.2.0) @@ -400,8 +400,8 @@ devDependencies: specifier: ^7.5.1 version: 7.5.1(@babel/core@7.22.10)(@swc/core@1.3.93)(esbuild@0.14.54)(react-dom@18.2.0)(react@18.2.0)(typescript@4.9.5)(webpack-cli@5.1.4) '@storybook/test-runner': - specifier: ^0.13.0 - version: 0.13.0(@types/node@18.11.9)(ts-node@10.9.1) + specifier: ^0.15.2 + version: 0.15.2(@types/node@18.11.9)(ts-node@10.9.1) '@storybook/theming': specifier: ^7.5.1 version: 7.5.1(react-dom@18.2.0)(react@18.2.0) @@ -552,6 +552,9 @@ devDependencies: eslint-plugin-react: specifier: ^7.33.2 version: 7.33.2(eslint@8.52.0) + eslint-plugin-simple-import-sort: + specifier: ^10.0.0 + version: 10.0.0(eslint@8.52.0) eslint-plugin-storybook: specifier: ^0.6.15 version: 0.6.15(eslint@8.52.0)(typescript@4.9.5) @@ -639,6 +642,15 @@ devDependencies: style-loader: specifier: ^2.0.0 version: 2.0.0(webpack@5.88.2) + stylelint: + specifier: ^15.11.0 + version: 15.11.0(typescript@4.9.5) + stylelint-config-standard-scss: + specifier: ^11.1.0 + version: 11.1.0(postcss@8.4.31)(stylelint@15.11.0) + stylelint-order: + specifier: ^6.0.3 + version: 6.0.3(stylelint@15.11.0) sucrase: specifier: ^3.29.0 version: 3.29.0 @@ -781,9 +793,9 @@ packages: resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 jsesc: 2.5.2 /@babel/helper-annotate-as-pure@7.22.5: @@ -796,7 +808,7 @@ packages: resolution: {integrity: sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-compilation-targets@7.22.10: @@ -861,19 +873,19 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-member-expression-to-functions@7.22.5: resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-module-imports@7.22.5: resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} @@ -898,7 +910,7 @@ packages: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} @@ -931,24 +943,28 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 /@babel/helper-string-parser@7.22.5: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -963,7 +979,7 @@ packages: dependencies: '@babel/helper-function-name': 7.23.0 '@babel/template': 7.22.15 - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helpers@7.22.10: @@ -991,13 +1007,12 @@ packages: dependencies: '@babel/types': 7.23.0 - /@babel/parser@7.23.3: - resolution: {integrity: sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==} + /@babel/parser@7.23.4: + resolution: {integrity: sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.23.3 - dev: true + '@babel/types': 7.23.4 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.22.10): resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} @@ -2077,8 +2092,8 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.22.13 - '@babel/parser': 7.23.0 - '@babel/types': 7.23.0 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 /@babel/template@7.22.5: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} @@ -2113,14 +2128,13 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - /@babel/types@7.23.3: - resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==} + /@babel/types@7.23.4: + resolution: {integrity: sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.22.5 + '@babel/helper-string-parser': 7.23.4 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - dev: true /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} @@ -2144,6 +2158,40 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@csstools/css-parser-algorithms@2.3.2(@csstools/css-tokenizer@2.2.1): + resolution: {integrity: sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.2.1 + dependencies: + '@csstools/css-tokenizer': 2.2.1 + dev: true + + /@csstools/css-tokenizer@2.2.1: + resolution: {integrity: sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==} + engines: {node: ^14 || ^16 || >=18} + dev: true + + /@csstools/media-query-list-parser@2.1.5(@csstools/css-parser-algorithms@2.3.2)(@csstools/css-tokenizer@2.2.1): + resolution: {integrity: sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.3.2 + '@csstools/css-tokenizer': ^2.2.1 + dependencies: + '@csstools/css-parser-algorithms': 2.3.2(@csstools/css-tokenizer@2.2.1) + '@csstools/css-tokenizer': 2.2.1 + dev: true + + /@csstools/selector-specificity@3.0.0(postcss-selector-parser@6.0.13): + resolution: {integrity: sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + dependencies: + postcss-selector-parser: 6.0.13 + dev: true + /@ctrl/tinycolor@3.4.1: resolution: {integrity: sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==} engines: {node: '>=10'} @@ -2625,71 +2673,28 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console@28.1.3: - resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/types': 28.1.3 - '@types/node': 18.18.4 - chalk: 4.1.2 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - slash: 3.0.0 - dev: true - /@jest/console@29.3.1: resolution: {integrity: sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/node': 18.18.4 chalk: 4.1.2 - jest-message-util: 29.3.1 - jest-util: 29.3.1 + jest-message-util: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 dev: true - /@jest/core@28.1.3(ts-node@10.9.1): - resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 28.1.3 - '@jest/reporters': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@jest/types': 29.6.3 '@types/node': 18.18.4 - ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.5.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 28.1.3 - jest-config: 28.1.3(@types/node@18.18.4)(ts-node@10.9.1) - jest-haste-map: 28.1.3 - jest-message-util: 28.1.3 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-resolve-dependencies: 28.1.3 - jest-runner: 28.1.3 - jest-runtime: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 - jest-watcher: 28.1.3 - micromatch: 4.0.5 - pretty-format: 28.1.3 - rimraf: 3.0.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - - ts-node dev: true /@jest/core@29.3.1(ts-node@10.9.1): @@ -2730,6 +2735,50 @@ packages: slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /@jest/core@29.7.0(ts-node@10.9.1): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.4 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.5.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.18.4)(ts-node@10.9.1) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros - supports-color - ts-node dev: true @@ -2741,16 +2790,6 @@ packages: '@jest/types': 27.5.1 dev: true - /@jest/environment@28.1.3: - resolution: {integrity: sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/fake-timers': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 18.18.4 - jest-mock: 28.1.3 - dev: true - /@jest/environment@29.3.1: resolution: {integrity: sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2761,11 +2800,14 @@ packages: jest-mock: 29.3.1 dev: true - /@jest/expect-utils@28.1.3: - resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 28.0.2 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.4 + jest-mock: 29.7.0 dev: true /@jest/expect-utils@29.3.1: @@ -2775,38 +2817,23 @@ packages: jest-get-type: 29.2.0 dev: true - /@jest/expect@28.1.3: - resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 28.1.3 - jest-snapshot: 28.1.3 - transitivePeerDependencies: - - supports-color + jest-get-type: 29.6.3 dev: true - /@jest/expect@29.3.1: - resolution: {integrity: sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==} + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.3.1 - jest-snapshot: 29.3.1 + expect: 29.7.0 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /@jest/fake-timers@28.1.3: - resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/types': 28.1.3 - '@sinonjs/fake-timers': 9.1.2 - '@types/node': 18.18.4 - jest-message-util: 28.1.3 - jest-mock: 28.1.3 - jest-util: 28.1.3 - dev: true - /@jest/fake-timers@29.3.1: resolution: {integrity: sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2819,32 +2846,45 @@ packages: jest-util: 29.3.1 dev: true - /@jest/globals@28.1.3: - resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.18.4 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.3.1: + resolution: {integrity: sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/expect': 28.1.3 - '@jest/types': 28.1.3 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /@jest/globals@29.3.1: - resolution: {integrity: sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==} + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.3.1 - '@jest/expect': 29.3.1 - '@jest/types': 29.3.1 - jest-mock: 29.3.1 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters@28.1.3: - resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/reporters@29.3.1: + resolution: {integrity: sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -2852,11 +2892,11 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 + '@jest/console': 29.3.1 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.3.1 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 '@types/node': 18.18.4 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -2868,20 +2908,19 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - jest-worker: 28.1.3 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.3.1 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - terminal-link: 2.1.1 v8-to-istanbul: 9.0.1 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters@29.3.1: - resolution: {integrity: sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==} + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -2890,11 +2929,11 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 '@types/node': 18.18.4 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -2902,13 +2941,13 @@ packages: glob: 7.2.3 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.1 + istanbul-lib-instrument: 6.0.1 istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.3.1 - jest-util: 29.3.1 - jest-worker: 29.3.1 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 @@ -2917,13 +2956,6 @@ packages: - supports-color dev: true - /@jest/schemas@28.1.3: - resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@sinclair/typebox': 0.24.51 - dev: true - /@jest/schemas@29.0.0: resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2931,32 +2963,29 @@ packages: '@sinclair/typebox': 0.24.51 dev: true - /@jest/source-map@28.1.2: - resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.17 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@sinclair/typebox': 0.27.8 dev: true /@jest/source-map@29.2.0: resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 callsites: 3.1.0 graceful-fs: 4.2.11 dev: true - /@jest/test-result@28.1.3: - resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 28.1.3 - '@jest/types': 28.1.3 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 + '@jridgewell/trace-mapping': 0.3.20 + callsites: 3.1.0 + graceful-fs: 4.2.11 dev: true /@jest/test-result@29.3.1: @@ -2964,46 +2993,56 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/console': 29.3.1 - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: true - /@jest/test-sequencer@28.1.3: - resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 28.1.3 - graceful-fs: 4.2.11 - jest-haste-map: 28.1.3 - slash: 3.0.0 + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 dev: true /@jest/test-sequencer@29.3.1: resolution: {integrity: sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.3.1 + '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 - jest-haste-map: 29.3.1 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 slash: 3.0.0 dev: true - /@jest/transform@28.1.3: - resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/transform@29.3.1: + resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.10 - '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 - convert-source-map: 1.9.0 + convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 28.1.3 - jest-regex-util: 28.0.2 - jest-util: 28.1.3 + jest-haste-map: 29.3.1 + jest-regex-util: 29.2.0 + jest-util: 29.3.1 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -3012,21 +3051,21 @@ packages: - supports-color dev: true - /@jest/transform@29.3.1: - resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.10 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.20 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 29.3.1 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -3046,11 +3085,11 @@ packages: chalk: 4.1.2 dev: true - /@jest/types@28.1.3: - resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/types@29.3.1: + resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 28.1.3 + '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 18.18.4 @@ -3058,11 +3097,11 @@ packages: chalk: 4.1.2 dev: true - /@jest/types@29.3.1: - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 18.18.4 @@ -3083,7 +3122,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} @@ -3097,7 +3136,7 @@ packages: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 dev: true /@jridgewell/sourcemap-codec@1.4.14: @@ -3109,6 +3148,12 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -3374,8 +3419,8 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@posthog/icons@0.2.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ftFoIropSJaFbxzzt1eGOgJCsbK0+L5KDdxKcpbhl4nMbmCEI/awzj98l+0pp/JAJzDrAsqEou7MvdJrntOGbw==} + /@posthog/icons@0.4.11(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-hpHDrBIZlnc4Z0d8BPjNkMf7gkmR3o7CQGcvXJ8fQzyOL07EU3I0LRfkRHhFaRaZdeDB4TkJkkSDJ8beyuhBPQ==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' @@ -4088,6 +4133,10 @@ packages: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sinonjs/commons@1.8.4: resolution: {integrity: sha512-RpmQdHVo8hCEHDVpO39zToS9jOhR6nw+/lQAzRNq9ErrGV9IeHM71XCn68svVl/euFeVW6BWX4p35gkhbOcSIQ==} deprecated: Breaks compatibility with ES5, use v1.8.5 @@ -4095,6 +4144,18 @@ packages: type-detect: 4.0.8 dev: true + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: true + /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: @@ -4690,7 +4751,7 @@ packages: dependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10(@babel/core@7.22.10) - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 '@storybook/csf': 0.1.1 '@storybook/csf-tools': 7.5.1 '@storybook/node-logger': 7.5.1 @@ -4856,9 +4917,9 @@ packages: resolution: {integrity: sha512-YChGbT1/odLS4RLb2HtK7ixM7mH5s7G5nOsWGKXalbza4SFKZIU2UzllEUsA+X8YfxMHnCD5TC3xLfK0ByxmzQ==} dependencies: '@babel/generator': 7.23.0 - '@babel/parser': 7.23.0 + '@babel/parser': 7.23.4 '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 '@storybook/csf': 0.1.1 '@storybook/types': 7.5.1 fs-extra: 11.1.1 @@ -5068,7 +5129,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 - flat-cache: 3.0.4 + flat-cache: 3.2.0 micromatch: 4.0.5 react-docgen-typescript: 2.2.2(typescript@4.9.5) tslib: 2.6.2 @@ -5223,32 +5284,33 @@ packages: - supports-color dev: true - /@storybook/test-runner@0.13.0(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-QIbfgia/iBy7PeUIwCYtPcyeZCHd21ebaPoMNIsRfwUW+VC12J4iG8cGDfOE7MGbMVz1Uu0elAEBB8NGP/YBtQ==} + /@storybook/test-runner@0.15.2(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-nHwThLvxho9wNAVxtESoAcrQD7UolOAJISwcG9uz3bmtTIm7h5DMlpfX+2DKbJyq5REg8nhcauZv5iFvwBdn1Q==} + engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true dependencies: '@babel/core': 7.22.10 - '@babel/generator': 7.22.10 - '@babel/template': 7.22.5 - '@babel/types': 7.23.0 + '@babel/generator': 7.23.0 + '@babel/template': 7.22.15 + '@babel/types': 7.23.4 '@storybook/core-common': 7.5.1 '@storybook/csf': 0.1.1 '@storybook/csf-tools': 7.5.1 - '@storybook/preview-api': 7.5.1 + '@storybook/preview-api': 7.5.3 '@swc/core': 1.3.93 '@swc/jest': 0.2.29(@swc/core@1.3.93) can-bind-to-host: 1.1.2 commander: 9.4.1 expect-playwright: 0.8.0 glob: 10.3.3 - jest: 28.1.3(@types/node@18.11.9)(ts-node@10.9.1) - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 - jest-junit: 14.0.1 - jest-playwright-preset: 2.0.0(jest-circus@28.1.3)(jest-environment-node@28.1.3)(jest-runner@28.1.3)(jest@28.1.3) - jest-runner: 28.1.3 + jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-junit: 16.0.0 + jest-playwright-preset: 3.0.1(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0) + jest-runner: 29.7.0 jest-serializer-html: 7.1.0 - jest-watch-typeahead: 2.2.2(jest@28.1.3) + jest-watch-typeahead: 2.2.2(jest@29.7.0) node-fetch: 2.6.7 playwright: 1.29.2 read-pkg-up: 7.0.1 @@ -5257,6 +5319,7 @@ packages: transitivePeerDependencies: - '@swc/helpers' - '@types/node' + - babel-plugin-macros - debug - encoding - node-notifier @@ -5853,8 +5916,8 @@ packages: /@types/babel__core@7.20.4: resolution: {integrity: sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==} dependencies: - '@babel/parser': 7.23.3 - '@babel/types': 7.23.3 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 '@types/babel__generator': 7.6.7 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.4 @@ -5863,39 +5926,39 @@ packages: /@types/babel__generator@7.6.6: resolution: {integrity: sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@types/babel__generator@7.6.7: resolution: {integrity: sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==} dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.4 dev: true /@types/babel__template@7.4.3: resolution: {integrity: sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==} dependencies: - '@babel/parser': 7.23.0 - '@babel/types': 7.23.0 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: - '@babel/parser': 7.23.3 - '@babel/types': 7.23.3 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 dev: true /@types/babel__traverse@7.20.3: resolution: {integrity: sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@types/babel__traverse@7.20.4: resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.4 dev: true /@types/body-parser@1.19.4: @@ -5909,7 +5972,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/chart.js@2.9.37: @@ -5937,7 +6000,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/cookie@0.4.1: @@ -6215,7 +6278,7 @@ packages: /@types/express-serve-static-core@4.17.41: resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.18.4 '@types/qs': 6.9.10 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -6381,6 +6444,10 @@ packages: resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} dev: true + /@types/minimist@1.2.5: + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + dev: true + /@types/ms@0.7.31: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true @@ -6545,7 +6612,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/serve-static@1.15.4: @@ -6561,7 +6628,7 @@ packages: dependencies: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/set-cookie-parser@2.4.2: @@ -7434,6 +7501,11 @@ packages: is-shared-array-buffer: 1.0.2 dev: true + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: false @@ -7570,17 +7642,17 @@ packages: '@babel/core': 7.22.10 dev: true - /babel-jest@28.1.3(@babel/core@7.22.10): - resolution: {integrity: sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /babel-jest@29.3.1(@babel/core@7.22.10): + resolution: {integrity: sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.22.10 - '@jest/transform': 28.1.3 - '@types/babel__core': 7.20.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.4 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 28.1.3(@babel/core@7.22.10) + babel-preset-jest: 29.2.0(@babel/core@7.22.10) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -7588,17 +7660,17 @@ packages: - supports-color dev: true - /babel-jest@29.3.1(@babel/core@7.22.10): - resolution: {integrity: sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==} + /babel-jest@29.7.0(@babel/core@7.22.10): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.22.10 - '@jest/transform': 29.3.1 - '@types/babel__core': 7.20.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.4 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.2.0(@babel/core@7.22.10) + babel-preset-jest: 29.6.3(@babel/core@7.22.10) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -7657,24 +7729,24 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist@28.1.3: - resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /babel-plugin-jest-hoist@29.2.0: + resolution: {integrity: sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.23.0 - '@types/babel__core': 7.20.3 - '@types/babel__traverse': 7.20.3 + '@babel/types': 7.23.4 + '@types/babel__core': 7.20.4 + '@types/babel__traverse': 7.20.4 dev: true - /babel-plugin-jest-hoist@29.2.0: - resolution: {integrity: sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==} + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.23.0 - '@types/babel__core': 7.20.3 - '@types/babel__traverse': 7.20.3 + '@babel/types': 7.23.4 + '@types/babel__core': 7.20.4 + '@types/babel__traverse': 7.20.4 dev: true /babel-plugin-named-exports-order@0.0.2: @@ -7744,25 +7816,25 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.10) dev: true - /babel-preset-jest@28.1.3(@babel/core@7.22.10): - resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /babel-preset-jest@29.2.0(@babel/core@7.22.10): + resolution: {integrity: sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.22.10 - babel-plugin-jest-hoist: 28.1.3 + babel-plugin-jest-hoist: 29.2.0 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) dev: true - /babel-preset-jest@29.2.0(@babel/core@7.22.10): - resolution: {integrity: sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==} + /babel-preset-jest@29.6.3(@babel/core@7.22.10): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.22.10 - babel-plugin-jest-hoist: 29.2.0 + babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) dev: true @@ -7784,7 +7856,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /base16@1.0.0: + /balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + dev: true + + /base16@1.0.0: resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} dev: false @@ -7999,7 +8075,7 @@ packages: /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 get-intrinsic: 1.2.2 /call-bind@1.0.5: @@ -8048,7 +8124,6 @@ packages: map-obj: 4.3.0 quick-lru: 5.1.1 type-fest: 1.4.0 - dev: false /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} @@ -8384,6 +8459,10 @@ packages: color-string: 1.9.1 dev: true + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + /colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true @@ -8611,6 +8690,41 @@ packages: yaml: 1.10.2 dev: true + /cosmiconfig@8.3.6(typescript@4.9.5): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 4.9.5 + dev: true + + /create-jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -8657,6 +8771,11 @@ packages: timsort: 0.3.0 dev: true + /css-functions-list@3.2.1: + resolution: {integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==} + engines: {node: '>=12 || >=16'} + dev: true + /css-loader@3.6.0(webpack@5.88.2): resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==} engines: {node: '>= 8.9.0'} @@ -8735,6 +8854,14 @@ packages: source-map: 0.6.1 dev: true + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + /css-what@3.4.2: resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} engines: {node: '>= 6'} @@ -9266,11 +9393,24 @@ packages: ms: 2.1.2 supports-color: 8.1.1 + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} dev: true + /decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + dev: true + /decimal.js@10.4.2: resolution: {integrity: sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==} dev: true @@ -9279,6 +9419,15 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-equal@2.1.0: resolution: {integrity: sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA==} dependencies: @@ -9444,16 +9593,16 @@ packages: - supports-color dev: true - /diff-sequences@28.1.1: - resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: true - /diff-sequences@29.3.1: resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -9652,11 +9801,6 @@ packages: /electron-to-chromium@1.4.492: resolution: {integrity: sha512-36K9b/6skMVwAIEsC7GiQ8I8N3soCALVSHqWHzNDtGemAcI9Xu8hP02cywWM0A794rTHm0b0zHPeLJHtgFVamQ==} - /emittery@0.10.2: - resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} - engines: {node: '>=12'} - dev: true - /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -9761,7 +9905,7 @@ packages: dependencies: call-bind: 1.0.2 es-to-primitive: 1.2.1 - function-bind: 1.1.1 + function-bind: 1.1.2 function.prototype.name: 1.1.5 get-intrinsic: 1.2.2 get-symbol-description: 1.0.0 @@ -9854,7 +9998,7 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 es-set-tostringtag: 2.0.2 - function-bind: 1.1.1 + function-bind: 1.1.2 get-intrinsic: 1.2.2 globalthis: 1.0.3 has-property-descriptors: 1.0.0 @@ -10305,6 +10449,14 @@ packages: string.prototype.matchall: 4.0.8 dev: true + /eslint-plugin-simple-import-sort@10.0.0(eslint@8.52.0): + resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} + peerDependencies: + eslint: '>=5.0.0' + dependencies: + eslint: 8.52.0 + dev: true + /eslint-plugin-storybook@0.6.15(eslint@8.52.0)(typescript@4.9.5): resolution: {integrity: sha512-lAGqVAJGob47Griu29KXYowI4G7KwMoJDOkEip8ujikuDLxU+oWJ1l0WL6F2oDO4QiyUFXvtDkEkISMOPzo+7w==} engines: {node: 12.x || 14.x || >= 16} @@ -10433,7 +10585,7 @@ packages: engines: {node: '>=8.3.0'} dependencies: '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 c8: 7.14.0 transitivePeerDependencies: - supports-color @@ -10529,17 +10681,6 @@ packages: resolution: {integrity: sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==} dev: true - /expect@28.1.3: - resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/expect-utils': 28.1.3 - jest-get-type: 28.0.2 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - dev: true - /expect@29.3.1: resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10551,6 +10692,17 @@ packages: jest-util: 29.3.1 dev: true + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /expr-eval@2.0.2: resolution: {integrity: sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==} dev: false @@ -10657,17 +10809,6 @@ packages: resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} dev: true - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -10772,7 +10913,14 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flat-cache: 3.0.4 + flat-cache: 3.2.0 + dev: true + + /file-entry-cache@7.0.1: + resolution: {integrity: sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==} + engines: {node: '>=12.0.0'} + dependencies: + flat-cache: 3.2.0 dev: true /file-loader@6.2.0(webpack@5.88.2): @@ -10902,16 +11050,17 @@ packages: path-exists: 5.0.0 dev: true - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flatted: 3.2.7 + flatted: 3.2.9 + keyv: 4.5.4 rimraf: 3.0.2 dev: true - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true /flow-parser@0.214.0: @@ -11094,9 +11243,6 @@ packages: requiresBuild: true optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -11144,7 +11290,7 @@ packages: /get-intrinsic@1.1.3: resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 has: 1.0.3 has-symbols: 1.0.3 @@ -11325,6 +11471,13 @@ packages: is-windows: 0.2.0 dev: true + /global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + dev: true + /global-prefix@0.1.5: resolution: {integrity: sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==} engines: {node: '>=0.10.0'} @@ -11342,7 +11495,6 @@ packages: ini: 1.3.8 kind-of: 6.0.3 which: 1.3.1 - dev: false /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -11368,12 +11520,16 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 dev: true + /globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + dev: true + /glur@1.1.2: resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==} @@ -11419,6 +11575,11 @@ packages: uglify-js: 3.17.4 dev: true + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -11525,6 +11686,13 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + /hsl-regex@1.0.0: resolution: {integrity: sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==} dev: true @@ -11567,6 +11735,11 @@ packages: engines: {node: '>=8'} dev: true + /html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + dev: true + /html-to-react@1.5.0: resolution: {integrity: sha512-tjihXBgaJZRRYzmkrJZ/Qf9jFayilFYcb+sJxXXE2BVLk2XsNrGeuNCVvhXmvREULZb9dz6NFTBC96DTR/lQCQ==} dependencies: @@ -11772,6 +11945,11 @@ packages: resolve-from: 4.0.0 dev: true + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -11791,6 +11969,11 @@ packages: engines: {node: '>=8'} dev: true + /indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + dev: true + /indexes-of@1.0.1: resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} dev: true @@ -12144,6 +12327,11 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + /is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -12319,7 +12507,7 @@ packages: engines: {node: '>=8'} dependencies: '@babel/core': 7.22.10 - '@babel/parser': 7.23.0 + '@babel/parser': 7.23.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.1 @@ -12327,6 +12515,19 @@ packages: - supports-color dev: true + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.22.10 + '@babel/parser': 7.23.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-lib-processinfo@2.0.3: resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} engines: {node: '>=8'} @@ -12404,14 +12605,6 @@ packages: moo-color: 1.0.3 dev: true - /jest-changed-files@28.1.3: - resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - execa: 5.1.1 - p-limit: 3.1.0 - dev: true - /jest-changed-files@29.2.0: resolution: {integrity: sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12420,63 +12613,47 @@ packages: p-limit: 3.1.0 dev: true - /jest-circus@28.1.3: - resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/expect': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 18.18.4 - chalk: 4.1.2 - co: 4.6.0 - dedent: 0.7.0 - is-generator-fn: 2.1.0 - jest-each: 28.1.3 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-runtime: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 + execa: 5.1.1 + jest-util: 29.7.0 p-limit: 3.1.0 - pretty-format: 28.1.3 - slash: 3.0.0 - stack-utils: 2.0.5 - transitivePeerDependencies: - - supports-color dev: true - /jest-circus@29.3.1: - resolution: {integrity: sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg==} + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.3.1 - '@jest/expect': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 chalk: 4.1.2 co: 4.6.0 - dedent: 0.7.0 + dedent: 1.5.1 is-generator-fn: 2.1.0 - jest-each: 29.3.1 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-runtime: 29.3.1 - jest-snapshot: 29.3.1 - jest-util: 29.3.1 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 p-limit: 3.1.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 + pure-rand: 6.0.4 slash: 3.0.0 stack-utils: 2.0.5 transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-cli@28.1.3(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-cli@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -12484,26 +12661,27 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 28.1.3(ts-node@10.9.1) - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 + '@jest/core': 29.3.1(ts-node@10.9.1) + '@jest/test-result': 29.3.1 + '@jest/types': 29.3.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 28.1.3(@types/node@18.11.9)(ts-node@10.9.1) - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-config: 29.3.1(@types/node@18.11.9)(ts-node@10.9.1) + jest-util: 29.3.1 + jest-validate: 29.3.1 prompts: 2.4.2 yargs: 17.6.2 transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true - /jest-cli@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} + /jest-cli@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -12512,27 +12690,27 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.3.1(ts-node@10.9.1) - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) exit: 0.1.2 - graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.3.1(@types/node@18.11.9)(ts-node@10.9.1) - jest-util: 29.3.1 - jest-validate: 29.3.1 - prompts: 2.4.2 + jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 yargs: 17.6.2 transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true - /jest-config@28.1.3(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-config@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' ts-node: '>=9.0.0' @@ -12543,36 +12721,37 @@ packages: optional: true dependencies: '@babel/core': 7.22.10 - '@jest/test-sequencer': 28.1.3 - '@jest/types': 28.1.3 + '@jest/test-sequencer': 29.3.1 + '@jest/types': 29.6.3 '@types/node': 18.11.9 - babel-jest: 28.1.3(@babel/core@7.22.10) + babel-jest: 29.3.1(@babel/core@7.22.10) chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 - jest-get-type: 28.0.2 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-runner: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 28.1.3 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1(@swc/core@1.3.93)(@types/node@18.11.9)(typescript@4.9.5) transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-config@28.1.3(@types/node@18.18.4)(ts-node@10.9.1): - resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-config@29.3.1(@types/node@18.18.4)(ts-node@10.9.1): + resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' ts-node: '>=9.0.0' @@ -12583,35 +12762,36 @@ packages: optional: true dependencies: '@babel/core': 7.22.10 - '@jest/test-sequencer': 28.1.3 - '@jest/types': 28.1.3 + '@jest/test-sequencer': 29.3.1 + '@jest/types': 29.6.3 '@types/node': 18.18.4 - babel-jest: 28.1.3(@babel/core@7.22.10) + babel-jest: 29.3.1(@babel/core@7.22.10) chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 - jest-get-type: 28.0.2 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-runner: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 28.1.3 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1(@swc/core@1.3.93)(@types/node@18.11.9)(typescript@4.9.5) transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-config@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + /jest-config@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -12623,35 +12803,36 @@ packages: optional: true dependencies: '@babel/core': 7.22.10 - '@jest/test-sequencer': 29.3.1 - '@jest/types': 29.3.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.11.9 - babel-jest: 29.3.1(@babel/core@7.22.10) + babel-jest: 29.7.0(@babel/core@7.22.10) chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.3.1 - jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-runner: 29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1(@swc/core@1.3.93)(@types/node@18.11.9)(typescript@4.9.5) transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-config@29.3.1(@types/node@18.18.4)(ts-node@10.9.1): - resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + /jest-config@29.7.0(@types/node@18.18.4)(ts-node@10.9.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -12663,43 +12844,34 @@ packages: optional: true dependencies: '@babel/core': 7.22.10 - '@jest/test-sequencer': 29.3.1 - '@jest/types': 29.3.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 - babel-jest: 29.3.1(@babel/core@7.22.10) + babel-jest: 29.7.0(@babel/core@7.22.10) chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.3.1 - jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-runner: 29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1(@swc/core@1.3.93)(@types/node@18.11.9)(typescript@4.9.5) transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-diff@28.1.3: - resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 28.1.1 - jest-get-type: 28.0.2 - pretty-format: 28.1.3 - dev: true - /jest-diff@29.3.1: resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12707,14 +12879,17 @@ packages: chalk: 4.1.2 diff-sequences: 29.3.1 jest-get-type: 29.2.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 dev: true - /jest-docblock@28.1.1: - resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - detect-newline: 3.1.0 + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-docblock@29.2.0: @@ -12724,26 +12899,22 @@ packages: detect-newline: 3.1.0 dev: true - /jest-each@28.1.3: - resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 - chalk: 4.1.2 - jest-get-type: 28.0.2 - jest-util: 28.1.3 - pretty-format: 28.1.3 + detect-newline: 3.1.0 dev: true - /jest-each@29.3.1: - resolution: {integrity: sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA==} + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 chalk: 4.1.2 - jest-get-type: 29.2.0 - jest-util: 29.3.1 - pretty-format: 29.3.1 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 dev: true /jest-environment-jsdom@29.3.1: @@ -12769,33 +12940,16 @@ packages: - utf-8-validate dev: true - /jest-environment-node@28.1.3: - resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/environment': 28.1.3 - '@jest/fake-timers': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 18.18.4 - jest-mock: 28.1.3 - jest-util: 28.1.3 - dev: true - - /jest-environment-node@29.3.1: - resolution: {integrity: sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag==} + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.3.1 - '@jest/fake-timers': 29.3.1 - '@jest/types': 29.3.1 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 - jest-mock: 29.3.1 - jest-util: 29.3.1 - dev: true - - /jest-get-type@28.0.2: - resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true /jest-get-type@29.2.0: @@ -12803,38 +12957,43 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map@28.1.3: - resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.3.1: + resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.5 '@types/node': 18.18.4 anymatch: 3.1.2 fb-watchman: 2.0.2 graceful-fs: 4.2.11 - jest-regex-util: 28.0.2 - jest-util: 28.1.3 - jest-worker: 28.1.3 + jest-regex-util: 29.2.0 + jest-util: 29.7.0 + jest-worker: 29.3.1 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 dev: true - /jest-haste-map@29.3.1: - resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.5 '@types/node': 18.18.4 anymatch: 3.1.2 fb-watchman: 2.0.2 graceful-fs: 4.2.11 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 - jest-worker: 29.3.1 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: @@ -12859,8 +13018,8 @@ packages: ssim.js: 3.5.0 dev: true - /jest-junit@14.0.1: - resolution: {integrity: sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==} + /jest-junit@16.0.0: + resolution: {integrity: sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==} engines: {node: '>=10.12.0'} dependencies: mkdirp: 1.0.4 @@ -12869,30 +13028,20 @@ packages: xml: 1.0.1 dev: true - /jest-leak-detector@28.1.3: - resolution: {integrity: sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - jest-get-type: 28.0.2 - pretty-format: 28.1.3 - dev: true - /jest-leak-detector@29.3.1: resolution: {integrity: sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true - /jest-matcher-utils@28.1.3: - resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.2 - jest-diff: 28.1.3 - jest-get-type: 28.0.2 - pretty-format: 28.1.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-matcher-utils@29.3.1: @@ -12905,19 +13054,14 @@ packages: pretty-format: 29.3.1 dev: true - /jest-message-util@28.1.3: - resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.22.13 - '@jest/types': 28.1.3 - '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.5 - pretty-format: 28.1.3 - slash: 3.0.0 - stack-utils: 2.0.5 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-message-util@29.3.1: @@ -12925,7 +13069,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.22.13 - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 @@ -12935,12 +13079,19 @@ packages: stack-utils: 2.0.5 dev: true - /jest-mock@28.1.3: - resolution: {integrity: sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 - '@types/node': 18.18.4 + '@babel/code-frame': 7.22.13 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.5 dev: true /jest-mock@29.3.1: @@ -12952,20 +13103,29 @@ packages: jest-util: 29.3.1 dev: true - /jest-playwright-preset@2.0.0(jest-circus@28.1.3)(jest-environment-node@28.1.3)(jest-runner@28.1.3)(jest@28.1.3): - resolution: {integrity: sha512-pV5ruTJJMen3lwshUL4dlSqLlP8z4q9MXqWJkmy+sB6HYfzXoqBHzhl+5hslznhnSVTe4Dwu+reiiwcUJpYUbw==} + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.18.4 + jest-util: 29.7.0 + dev: true + + /jest-playwright-preset@3.0.1(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0): + resolution: {integrity: sha512-tHqv+JUmheNMZpmH7XyT5CAMHr3ExTUIY9baMPzcJiLYPvCaPTwig9YvuGGnXV2n+Epmch0Ld4429g6py0nq0w==} peerDependencies: - jest: ^28.0.0 - jest-circus: ^28.0.0 - jest-environment-node: ^28.0.0 - jest-runner: ^28.0.0 + jest: ^29.3.1 + jest-circus: ^29.3.1 + jest-environment-node: ^29.3.1 + jest-runner: ^29.3.1 dependencies: expect-playwright: 0.8.0 - jest: 28.1.3(@types/node@18.11.9)(ts-node@10.9.1) - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 + jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 jest-process-manager: 0.3.1 - jest-runner: 28.1.3 + jest-runner: 29.7.0 nyc: 15.1.0 playwright-core: 1.29.2 rimraf: 3.0.2 @@ -12975,7 +13135,7 @@ packages: - supports-color dev: true - /jest-pnp-resolver@1.2.2(jest-resolve@28.1.3): + /jest-pnp-resolver@1.2.2(jest-resolve@29.3.1): resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} peerDependencies: @@ -12984,10 +13144,10 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 28.1.3 + jest-resolve: 29.3.1 dev: true - /jest-pnp-resolver@1.2.2(jest-resolve@29.3.1): + /jest-pnp-resolver@1.2.2(jest-resolve@29.7.0): resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} peerDependencies: @@ -12996,7 +13156,7 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.3.1 + jest-resolve: 29.7.0 dev: true /jest-process-manager@0.3.1: @@ -13017,24 +13177,14 @@ packages: - supports-color dev: true - /jest-regex-util@28.0.2: - resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: true - /jest-regex-util@29.2.0: resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies@28.1.3: - resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - jest-regex-util: 28.0.2 - jest-snapshot: 28.1.3 - transitivePeerDependencies: - - supports-color + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true /jest-resolve-dependencies@29.3.1: @@ -13042,24 +13192,19 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-regex-util: 29.2.0 - jest-snapshot: 29.3.1 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /jest-resolve@28.1.3: - resolution: {integrity: sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 28.1.3 - jest-pnp-resolver: 1.2.2(jest-resolve@28.1.3) - jest-util: 28.1.3 - jest-validate: 28.1.3 - resolve: 1.22.1 - resolve.exports: 1.1.0 - slash: 3.0.0 + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color dev: true /jest-resolve@29.3.1: @@ -13070,40 +13215,26 @@ packages: graceful-fs: 4.2.11 jest-haste-map: 29.3.1 jest-pnp-resolver: 1.2.2(jest-resolve@29.3.1) - jest-util: 29.3.1 + jest-util: 29.7.0 jest-validate: 29.3.1 resolve: 1.22.1 resolve.exports: 1.1.0 slash: 3.0.0 dev: true - /jest-runner@28.1.3: - resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 28.1.3 - '@jest/environment': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 18.18.4 chalk: 4.1.2 - emittery: 0.10.2 graceful-fs: 4.2.11 - jest-docblock: 28.1.1 - jest-environment-node: 28.1.3 - jest-haste-map: 28.1.3 - jest-leak-detector: 28.1.3 - jest-message-util: 28.1.3 - jest-resolve: 28.1.3 - jest-runtime: 28.1.3 - jest-util: 28.1.3 - jest-watcher: 28.1.3 - jest-worker: 28.1.3 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.2(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.1 + resolve.exports: 2.0.2 + slash: 3.0.0 dev: true /jest-runner@29.3.1: @@ -13111,22 +13242,22 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/console': 29.3.1 - '@jest/environment': 29.3.1 - '@jest/test-result': 29.3.1 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/node': 18.18.4 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 jest-docblock: 29.2.0 - jest-environment-node: 29.3.1 + jest-environment-node: 29.7.0 jest-haste-map: 29.3.1 jest-leak-detector: 29.3.1 - jest-message-util: 29.3.1 + jest-message-util: 29.7.0 jest-resolve: 29.3.1 - jest-runtime: 29.3.1 - jest-util: 29.3.1 + jest-runtime: 29.7.0 + jest-util: 29.7.0 jest-watcher: 29.3.1 jest-worker: 29.3.1 p-limit: 3.1.0 @@ -13135,32 +13266,31 @@ packages: - supports-color dev: true - /jest-runtime@28.1.3: - resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/fake-timers': 28.1.3 - '@jest/globals': 28.1.3 - '@jest/source-map': 28.1.2 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.4 chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 - execa: 5.1.1 - glob: 7.2.3 + emittery: 0.13.1 graceful-fs: 4.2.11 - jest-haste-map: 28.1.3 - jest-message-util: 28.1.3 - jest-mock: 28.1.3 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 - slash: 3.0.0 - strip-bom: 4.0.0 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: true @@ -13169,26 +13299,56 @@ packages: resolution: {integrity: sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.3.1 - '@jest/fake-timers': 29.3.1 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 '@jest/globals': 29.3.1 '@jest/source-map': 29.2.0 - '@jest/test-result': 29.3.1 + '@jest/test-result': 29.7.0 '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 + '@types/node': 18.18.4 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.3.1 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-haste-map: 29.3.1 - jest-message-util: 29.3.1 - jest-mock: 29.3.1 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-snapshot: 29.3.1 - jest-util: 29.3.1 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: @@ -13201,74 +13361,71 @@ packages: diffable-html: 4.1.0 dev: true - /jest-snapshot@28.1.3: - resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-snapshot@29.3.1: + resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.10 '@babel/generator': 7.23.0 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.10) '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.10) '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 - '@jest/expect-utils': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@babel/types': 7.23.4 + '@jest/expect-utils': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.6.3 '@types/babel__traverse': 7.20.3 '@types/prettier': 2.7.1 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) chalk: 4.1.2 - expect: 28.1.3 + expect: 29.3.1 graceful-fs: 4.2.11 - jest-diff: 28.1.3 - jest-get-type: 28.0.2 - jest-haste-map: 28.1.3 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-util: 28.1.3 + jest-diff: 29.3.1 + jest-get-type: 29.2.0 + jest-haste-map: 29.3.1 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 natural-compare: 1.4.0 - pretty-format: 28.1.3 + pretty-format: 29.7.0 semver: 7.5.4 transitivePeerDependencies: - supports-color dev: true - /jest-snapshot@29.3.1: - resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.10 '@babel/generator': 7.23.0 '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.10) '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.10) - '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 - '@jest/expect-utils': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/babel__traverse': 7.20.3 - '@types/prettier': 2.7.1 + '@babel/types': 7.23.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) chalk: 4.1.2 - expect: 29.3.1 + expect: 29.7.0 graceful-fs: 4.2.11 - jest-diff: 29.3.1 - jest-get-type: 29.2.0 - jest-haste-map: 29.3.1 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-util: 29.3.1 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 natural-compare: 1.4.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 semver: 7.5.4 transitivePeerDependencies: - supports-color dev: true - /jest-util@28.1.3: - resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-util@29.3.1: + resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.3.1 '@types/node': 18.18.4 chalk: 4.1.2 ci-info: 3.5.0 @@ -13276,11 +13433,11 @@ packages: picomatch: 2.3.1 dev: true - /jest-util@29.3.1: - resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 '@types/node': 18.18.4 chalk: 4.1.2 ci-info: 3.5.0 @@ -13288,31 +13445,31 @@ packages: picomatch: 2.3.1 dev: true - /jest-validate@28.1.3: - resolution: {integrity: sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-validate@29.3.1: + resolution: {integrity: sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.6.3 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 28.0.2 + jest-get-type: 29.2.0 leven: 3.1.0 - pretty-format: 28.1.3 + pretty-format: 29.7.0 dev: true - /jest-validate@29.3.1: - resolution: {integrity: sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==} + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.3 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.2.0 + jest-get-type: 29.6.3 leven: 3.1.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 dev: true - /jest-watch-typeahead@2.2.2(jest@28.1.3): + /jest-watch-typeahead@2.2.2(jest@29.7.0): resolution: {integrity: sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ==} engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -13320,39 +13477,39 @@ packages: dependencies: ansi-escapes: 6.0.0 chalk: 5.2.0 - jest: 28.1.3(@types/node@18.11.9)(ts-node@10.9.1) - jest-regex-util: 29.2.0 - jest-watcher: 29.3.1 + jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-regex-util: 29.6.3 + jest-watcher: 29.7.0 slash: 5.0.0 string-length: 5.0.1 strip-ansi: 7.0.1 dev: true - /jest-watcher@28.1.3: - resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-watcher@29.3.1: + resolution: {integrity: sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 ansi-escapes: 4.3.2 chalk: 4.1.2 - emittery: 0.10.2 - jest-util: 28.1.3 + emittery: 0.13.1 + jest-util: 29.7.0 string-length: 4.0.2 dev: true - /jest-watcher@29.3.1: - resolution: {integrity: sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==} + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.18.4 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.3.1 + jest-util: 29.7.0 string-length: 4.0.2 dev: true @@ -13365,28 +13522,29 @@ packages: supports-color: 8.1.1 dev: true - /jest-worker@28.1.3: - resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-worker@29.3.1: + resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.18.4 + jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest-worker@29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.18.4 - jest-util: 29.3.1 + jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@28.1.3(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -13394,18 +13552,19 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 28.1.3(ts-node@10.9.1) - '@jest/types': 28.1.3 + '@jest/core': 29.3.1(ts-node@10.9.1) + '@jest/types': 29.3.1 import-local: 3.1.0 - jest-cli: 28.1.3(@types/node@18.11.9)(ts-node@10.9.1) + jest-cli: 29.3.1(@types/node@18.11.9)(ts-node@10.9.1) transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true - /jest@29.3.1(@types/node@18.11.9)(ts-node@10.9.1): - resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} + /jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -13414,12 +13573,13 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.3.1(ts-node@10.9.1) - '@jest/types': 29.3.1 + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.3.1(@types/node@18.11.9)(ts-node@10.9.1) + jest-cli: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true @@ -13468,7 +13628,7 @@ packages: '@babel/preset-env': ^7.1.6 dependencies: '@babel/core': 7.22.10 - '@babel/parser': 7.23.0 + '@babel/parser': 7.23.4 '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.10) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.22.10) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.22.10) @@ -13542,6 +13702,10 @@ packages: engines: {node: '>=4'} hasBin: true + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + /json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} dev: true @@ -13718,6 +13882,12 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -13732,6 +13902,10 @@ packages: engines: {node: '>= 8'} dev: true + /known-css-properties@0.29.0: + resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} + dev: true + /lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} @@ -13967,6 +14141,10 @@ packages: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: true + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + /lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} dev: true @@ -14059,10 +14237,14 @@ packages: tmpl: 1.0.5 dev: true + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + /map-obj@4.3.0: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - dev: false /map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} @@ -14119,6 +14301,10 @@ packages: react: 18.2.0 dev: true + /mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + dev: true + /maxmind@4.3.8: resolution: {integrity: sha512-HrfxEu5yPBPtTy/OT+W5bPQwEfLUX0EHqe2EbJiB47xQMumHqXvSP7PAwzV8Z++NRCmQwy4moQrTSt0+dH+Jmg==} engines: {node: '>=12', npm: '>=6'} @@ -14171,6 +14357,10 @@ packages: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: true + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + /mdn-data@2.0.4: resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} dev: true @@ -14196,6 +14386,24 @@ packages: map-or-similar: 1.5.0 dev: true + /meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + dev: true + /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -14285,6 +14493,15 @@ packages: brace-expansion: 2.0.1 dev: true + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -14533,6 +14750,16 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.11.0 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + dev: true + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -15168,7 +15395,7 @@ packages: resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} dependencies: postcss: 7.0.39 - postcss-selector-parser: 6.0.10 + postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true @@ -15235,6 +15462,10 @@ packages: webpack: 5.88.2(@swc/core@1.3.93)(esbuild@0.14.54)(webpack-cli@5.1.4) dev: true + /postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + dev: true + /postcss-merge-longhand@4.0.11: resolution: {integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==} engines: {node: '>=6.9.0'} @@ -15319,7 +15550,7 @@ packages: dependencies: icss-utils: 4.1.1 postcss: 7.0.39 - postcss-selector-parser: 6.0.10 + postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true @@ -15331,7 +15562,7 @@ packages: dependencies: icss-utils: 5.1.0(postcss@8.4.31) postcss: 8.4.31 - postcss-selector-parser: 6.0.10 + postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true @@ -15340,7 +15571,7 @@ packages: engines: {node: '>= 6'} dependencies: postcss: 7.0.39 - postcss-selector-parser: 6.0.10 + postcss-selector-parser: 6.0.13 dev: true /postcss-modules-scope@3.0.0(postcss@8.4.31): @@ -15350,7 +15581,7 @@ packages: postcss: ^8.1.0 dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.0.10 + postcss-selector-parser: 6.0.13 dev: true /postcss-modules-values@3.0.0: @@ -15480,6 +15711,28 @@ packages: postcss-value-parser: 3.3.1 dev: true + /postcss-resolve-nested-selector@0.1.1: + resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} + dev: true + + /postcss-safe-parser@6.0.0(postcss@8.4.31): + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.31 + dev: true + + /postcss-scss@4.0.9(postcss@8.4.31): + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + dependencies: + postcss: 8.4.31 + dev: true + /postcss-selector-parser@3.1.2: resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==} engines: {node: '>=8'} @@ -15489,14 +15742,22 @@ packages: uniq: 1.0.1 dev: true - /postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 dev: true + /postcss-sorting@8.0.2(postcss@8.4.31): + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + dependencies: + postcss: 8.4.31 + dev: true + /postcss-svgo@4.0.3: resolution: {integrity: sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==} engines: {node: '>=6.9.0'} @@ -15544,8 +15805,8 @@ packages: resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==} dev: false - /posthog-js@1.91.0: - resolution: {integrity: sha512-VqyfzxjSlD5AIs2yWQxDvjsaC3GeFbqMbZBXqBeXyikDJBskKFCyxR6iCw+uYpNd8hWdxqIlAwiYaV1dnQWJVA==} + /posthog-js@1.92.1: + resolution: {integrity: sha512-xtuTfM/acfDauiEfIdKF6d911KUZQ7RLii2COAYEoPWr3cVUFoNUoRQz9QJvgDlV2j22Zwl+mnXacUeua+Yi1A==} dependencies: fflate: 0.4.8 dev: false @@ -15591,21 +15852,20 @@ packages: ansi-styles: 5.2.0 react-is: 17.0.2 - /pretty-format@28.1.3: - resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /pretty-format@29.3.1: + resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 28.1.3 - ansi-regex: 5.0.1 + '@jest/schemas': 29.0.0 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true - /pretty-format@29.3.1: - resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true @@ -15896,6 +16156,10 @@ packages: resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} dev: false + /pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + dev: true + /q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} @@ -15936,7 +16200,6 @@ packages: /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - dev: false /quickselect@2.0.0: resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} @@ -16596,8 +16859,8 @@ packages: react: 18.2.0 dev: true - /react-intersection-observer@9.4.3(react@18.2.0): - resolution: {integrity: sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ==} + /react-intersection-observer@9.5.3(react@18.2.0): + resolution: {integrity: sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==} peerDependencies: react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: @@ -16834,6 +17097,15 @@ packages: type-fest: 0.8.1 dev: true + /read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + dev: true + /read-pkg@4.0.1: resolution: {integrity: sha512-+UBirHHDm5J+3WDmLBZYSklRYg82nMlz+enn+GMZ22nSR2f4bzxmhso6rzQW/3mT2PVzpzDTiYIZahk8UmZ44w==} engines: {node: '>=6'} @@ -16853,6 +17125,16 @@ packages: type-fest: 0.6.0 dev: true + /read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + dev: true + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -16916,6 +17198,14 @@ packages: strip-indent: 3.0.0 dev: true + /redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + dependencies: + indent-string: 5.0.0 + strip-indent: 4.0.0 + dev: true + /redux@4.2.0: resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} dependencies: @@ -17122,6 +17412,11 @@ packages: engines: {node: '>=10'} dev: true + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -17928,6 +18223,13 @@ packages: min-indent: 1.0.1 dev: true + /strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + dependencies: + min-indent: 1.0.1 + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -17953,6 +18255,10 @@ packages: webpack: 5.88.2(@swc/core@1.3.93)(esbuild@0.14.54)(webpack-cli@5.1.4) dev: true + /style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + dev: true + /stylehacks@4.0.3: resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} engines: {node: '>=6.9.0'} @@ -17962,6 +18268,129 @@ packages: postcss-selector-parser: 3.1.2 dev: true + /stylelint-config-recommended-scss@13.1.0(postcss@8.4.31)(stylelint@15.11.0): + resolution: {integrity: sha512-8L5nDfd+YH6AOoBGKmhH8pLWF1dpfY816JtGMePcBqqSsLU+Ysawx44fQSlMOJ2xTfI9yTGpup5JU77c17w1Ww==} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^15.10.0 + peerDependenciesMeta: + postcss: + optional: true + dependencies: + postcss: 8.4.31 + postcss-scss: 4.0.9(postcss@8.4.31) + stylelint: 15.11.0(typescript@4.9.5) + stylelint-config-recommended: 13.0.0(stylelint@15.11.0) + stylelint-scss: 5.3.1(stylelint@15.11.0) + dev: true + + /stylelint-config-recommended@13.0.0(stylelint@15.11.0): + resolution: {integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + stylelint: ^15.10.0 + dependencies: + stylelint: 15.11.0(typescript@4.9.5) + dev: true + + /stylelint-config-standard-scss@11.1.0(postcss@8.4.31)(stylelint@15.11.0): + resolution: {integrity: sha512-5gnBgeNTgRVdchMwiFQPuBOtj9QefYtfXiddrOMJA2pI22zxt6ddI2s+e5Oh7/6QYl7QLJujGnaUR5YyGq72ow==} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^15.10.0 + peerDependenciesMeta: + postcss: + optional: true + dependencies: + postcss: 8.4.31 + stylelint: 15.11.0(typescript@4.9.5) + stylelint-config-recommended-scss: 13.1.0(postcss@8.4.31)(stylelint@15.11.0) + stylelint-config-standard: 34.0.0(stylelint@15.11.0) + dev: true + + /stylelint-config-standard@34.0.0(stylelint@15.11.0): + resolution: {integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + stylelint: ^15.10.0 + dependencies: + stylelint: 15.11.0(typescript@4.9.5) + stylelint-config-recommended: 13.0.0(stylelint@15.11.0) + dev: true + + /stylelint-order@6.0.3(stylelint@15.11.0): + resolution: {integrity: sha512-1j1lOb4EU/6w49qZeT2SQVJXm0Ht+Qnq9GMfUa3pMwoyojIWfuA+JUDmoR97Bht1RLn4ei0xtLGy87M7d29B1w==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 + dependencies: + postcss: 8.4.31 + postcss-sorting: 8.0.2(postcss@8.4.31) + stylelint: 15.11.0(typescript@4.9.5) + dev: true + + /stylelint-scss@5.3.1(stylelint@15.11.0): + resolution: {integrity: sha512-5I9ZDIm77BZrjOccma5WyW2nJEKjXDd4Ca8Kk+oBapSO4pewSlno3n+OyimcyVJJujQZkBN2D+xuMkIamSc6hA==} + peerDependencies: + stylelint: ^14.5.1 || ^15.0.0 + dependencies: + known-css-properties: 0.29.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.1 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + stylelint: 15.11.0(typescript@4.9.5) + dev: true + + /stylelint@15.11.0(typescript@4.9.5): + resolution: {integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + '@csstools/css-parser-algorithms': 2.3.2(@csstools/css-tokenizer@2.2.1) + '@csstools/css-tokenizer': 2.2.1 + '@csstools/media-query-list-parser': 2.1.5(@csstools/css-parser-algorithms@2.3.2)(@csstools/css-tokenizer@2.2.1) + '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 8.3.6(typescript@4.9.5) + css-functions-list: 3.2.1 + css-tree: 2.3.1 + debug: 4.3.4(supports-color@8.1.1) + fast-glob: 3.3.1 + fastest-levenshtein: 1.0.16 + file-entry-cache: 7.0.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.2.4 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.29.0 + mathml-tag-names: 2.1.3 + meow: 10.1.5 + micromatch: 4.0.5 + normalize-path: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.31 + postcss-resolve-nested-selector: 0.1.1 + postcss-safe-parser: 6.0.0(postcss@8.4.31) + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 3.0.0 + svg-tags: 1.0.0 + table: 6.8.1 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /sucrase@3.29.0: resolution: {integrity: sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==} engines: {node: '>=8'} @@ -18006,9 +18435,9 @@ packages: dependencies: has-flag: 4.0.0 - /supports-hyperlinks@2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} + /supports-hyperlinks@3.0.0: + resolution: {integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==} + engines: {node: '>=14.18'} dependencies: has-flag: 4.0.0 supports-color: 7.2.0 @@ -18018,6 +18447,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + dev: true + /svgo@1.3.2: resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} engines: {node: '>=4.0.0'} @@ -18069,6 +18502,17 @@ packages: resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==} dev: false + /table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.11.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -18135,14 +18579,6 @@ packages: unique-string: 2.0.0 dev: true - /terminal-link@2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} - dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.3.0 - dev: true - /terser-webpack-plugin@5.3.9(@swc/core@1.3.93)(esbuild@0.14.54)(webpack@5.88.2): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} @@ -18323,6 +18759,11 @@ packages: hasBin: true dev: true + /trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} + dev: true + /trough@1.0.5: resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} dev: false @@ -18476,7 +18917,6 @@ packages: /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} - dev: false /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} @@ -18889,7 +19329,7 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 dev: true @@ -19321,6 +19761,14 @@ packages: signal-exit: 3.0.7 dev: true + /write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + dev: true + /ws@6.2.2: resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} peerDependencies: diff --git a/posthog/api/capture.py b/posthog/api/capture.py index a7d72f9ca1f3e..ac954a3b8d6d2 100644 --- a/posthog/api/capture.py +++ b/posthog/api/capture.py @@ -262,11 +262,11 @@ def drop_events_over_quota(token: str, events: List[Any]) -> List[Any]: if not settings.EE_AVAILABLE: return events - from ee.billing.quota_limiting import QuotaResource, list_limited_team_tokens + from ee.billing.quota_limiting import QuotaResource, list_limited_team_attributes results = [] - limited_tokens_events = list_limited_team_tokens(QuotaResource.EVENTS) - limited_tokens_recordings = list_limited_team_tokens(QuotaResource.RECORDINGS) + limited_tokens_events = list_limited_team_attributes(QuotaResource.EVENTS) + limited_tokens_recordings = list_limited_team_attributes(QuotaResource.RECORDINGS) for event in events: if event.get("event") in SESSION_RECORDING_EVENT_NAMES: diff --git a/posthog/api/cohort.py b/posthog/api/cohort.py index c525be8d263be..90324a48bfda0 100644 --- a/posthog/api/cohort.py +++ b/posthog/api/cohort.py @@ -1,5 +1,16 @@ import csv import json + +from django.db import DatabaseError +import structlog + +from posthog.models.feature_flag.flag_matching import ( + FeatureFlagMatcher, + FlagsMatcherCache, + get_feature_flag_hash_key_overrides, +) +from posthog.models.person.person import PersonDistinctId +from posthog.models.property.property import Property from posthog.queries.insight import insight_sync_execute import posthoganalytics from posthog.metrics import LABEL_TEAM_ID @@ -8,7 +19,7 @@ from typing import Any, Dict, cast from django.conf import settings -from django.db.models import QuerySet +from django.db.models import QuerySet, Prefetch, prefetch_related_objects, OuterRef, Subquery from django.db.models.expressions import F from django.utils import timezone from rest_framework import serializers, viewsets @@ -67,6 +78,7 @@ from posthog.queries.util import get_earliest_timestamp from posthog.tasks.calculate_cohort import ( calculate_cohort_from_list, + insert_cohort_from_feature_flag, insert_cohort_from_insight_filter, update_cohort, ) @@ -80,6 +92,8 @@ labelnames=[LABEL_TEAM_ID], ) +logger = structlog.get_logger(__name__) + class CohortSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) @@ -116,6 +130,8 @@ def _handle_static(self, cohort: Cohort, context: Dict) -> None: request = self.context["request"] if request.FILES.get("csv"): self._calculate_static_by_csv(request.FILES["csv"], cohort) + elif context.get("from_feature_flag_key"): + insert_cohort_from_feature_flag.delay(cohort.pk, context["from_feature_flag_key"], self.context["team_id"]) else: filter_data = request.GET.dict() existing_cohort_id = context.get("from_cohort_id") @@ -539,3 +555,142 @@ def insert_actors_into_cohort_by_query(cohort: Cohort, query: str, params: Dict[ cohort.errors_calculating = F("errors_calculating") + 1 cohort.save(update_fields=["errors_calculating", "is_calculating"]) capture_exception(err) + + +def get_cohort_actors_for_feature_flag(cohort_id: int, flag: str, team_id: int, batchsize: int = 1_000): + # :TODO: Find a way to incorporate this into the same code path as feature flag evaluation + try: + feature_flag = FeatureFlag.objects.get(team_id=team_id, key=flag) + except FeatureFlag.DoesNotExist: + return [] + + if not feature_flag.active or feature_flag.deleted or feature_flag.aggregation_group_type_index is not None: + return [] + + cohort = Cohort.objects.get(pk=cohort_id) + matcher_cache = FlagsMatcherCache(team_id) + uuids_to_add_to_cohort = [] + cohorts_cache = {} + + if feature_flag.uses_cohorts: + # TODO: Consider disabling flags with cohorts for creating static cohorts + # because this is currently a lot more inefficient for flag matching, + # as we're required to go to the database for each person. + cohorts_cache = {cohort.pk: cohort for cohort in Cohort.objects.filter(team_id=team_id, deleted=False)} + + default_person_properties = {} + for condition in feature_flag.conditions: + property_list = Filter(data=condition).property_groups.flat + for property in property_list: + default_person_properties.update(get_default_person_property(property, cohorts_cache)) + + try: + # QuerySet.Iterator() doesn't work with pgbouncer, it will load everything into memory and then stream + # which doesn't work for us, so need a manual chunking here. + # Because of this pgbouncer transaction pooling mode, we can't use server-side cursors. + queryset = Person.objects.filter(team_id=team_id).order_by("id") + # get batchsize number of people at a time + start = 0 + batch_of_persons = queryset[start : start + batchsize] + while batch_of_persons: + # TODO: Check if this subquery bulk fetch limiting is better than just doing a join for all distinct ids + # OR, if row by row getting single distinct id is better + # distinct_id = PersonDistinctId.objects.filter(person=person, team_id=team_id).values_list( + # "distinct_id", flat=True + # )[0] + distinct_id_subquery = Subquery( + PersonDistinctId.objects.filter(person_id=OuterRef("person_id")).values_list("id", flat=True)[:3] + ) + prefetch_related_objects( + batch_of_persons, + Prefetch( + "persondistinctid_set", + to_attr="distinct_ids_cache", + queryset=PersonDistinctId.objects.filter(id__in=distinct_id_subquery), + ), + ) + + all_persons = list(batch_of_persons) + if len(all_persons) == 0: + break + + for person in all_persons: + # ignore almost-deleted persons / persons with no distinct ids + if len(person.distinct_ids) == 0: + continue + + distinct_id = person.distinct_ids[0] + person_overrides = {} + if feature_flag.ensure_experience_continuity: + # :TRICKY: This is inefficient because it tries to get the hashkey overrides one by one. + # But reusing functions is better for maintainability. Revisit optimising if this becomes a bottleneck. + person_overrides = get_feature_flag_hash_key_overrides( + team_id, [distinct_id], person_id_to_distinct_id_mapping={person.id: distinct_id} + ) + + try: + match = FeatureFlagMatcher( + [feature_flag], + distinct_id, + groups={}, + cache=matcher_cache, + hash_key_overrides=person_overrides, + property_value_overrides={**default_person_properties, **person.properties}, + group_property_value_overrides={}, + cohorts_cache=cohorts_cache, + ).get_match(feature_flag) + if match.match: + uuids_to_add_to_cohort.append(str(person.uuid)) + except (DatabaseError, ValueError, ValidationError): + logger.exception( + "Error evaluating feature flag for person", person_uuid=str(person.uuid), team_id=team_id + ) + except Exception as err: + # matching errors are not fatal, so we just log them and move on. + # Capturing in sentry for now just in case there are some unexpected errors + # we did not account for. + capture_exception(err) + + if len(uuids_to_add_to_cohort) >= batchsize - 1: + cohort.insert_users_list_by_uuid( + uuids_to_add_to_cohort, insert_in_clickhouse=True, batchsize=batchsize + ) + uuids_to_add_to_cohort = [] + + start += batchsize + batch_of_persons = queryset[start : start + batchsize] + + if len(uuids_to_add_to_cohort) > 0: + cohort.insert_users_list_by_uuid(uuids_to_add_to_cohort, insert_in_clickhouse=True, batchsize=batchsize) + + except Exception as err: + if settings.DEBUG or settings.TEST: + raise err + capture_exception(err) + + +def get_default_person_property(prop: Property, cohorts_cache: Dict[int, Cohort]): + default_person_properties = {} + + if prop.operator not in ("is_set", "is_not_set") and prop.type == "person": + default_person_properties[prop.key] = "" + elif prop.type == "cohort" and not isinstance(prop.value, list): + try: + parsed_cohort_id = int(prop.value) + except (ValueError, TypeError): + return None + cohort = cohorts_cache.get(parsed_cohort_id) + if cohort: + return get_default_person_properties_for_cohort(cohort, cohorts_cache) + return default_person_properties + + +def get_default_person_properties_for_cohort(cohort: Cohort, cohorts_cache: Dict[int, Cohort]) -> Dict[str, str]: + """ + Returns a dictionary of default person properties to use when evaluating a feature flag + """ + default_person_properties = {} + for property in cohort.properties.flat: + default_person_properties.update(get_default_person_property(property, cohorts_cache)) + + return default_person_properties diff --git a/posthog/api/decide.py b/posthog/api/decide.py index b5117599b5833..ccb6670af4584 100644 --- a/posthog/api/decide.py +++ b/posthog/api/decide.py @@ -13,6 +13,7 @@ from statshog.defaults.django import statsd from posthog.api.geoip import get_geoip_properties +from posthog.api.survey import SURVEY_TARGETING_FLAG_PREFIX from posthog.api.utils import get_project_id, get_token from posthog.database_healthcheck import DATABASE_FOR_FLAG_MATCHING from posthog.exceptions import RequestParsingError, generate_exception_response @@ -221,9 +222,16 @@ def get_decide(request: HttpRequest): else False ) - if settings.NEW_ANALYTICS_CAPTURE_TEAM_IDS and str(team.id) in settings.NEW_ANALYTICS_CAPTURE_TEAM_IDS: - if random() < settings.NEW_ANALYTICS_CAPTURE_SAMPLING_RATE: - response["analytics"] = {"endpoint": settings.NEW_ANALYTICS_CAPTURE_ENDPOINT} + if str(team.id) not in settings.NEW_ANALYTICS_CAPTURE_EXCLUDED_TEAM_IDS: + if ( + "*" in settings.NEW_ANALYTICS_CAPTURE_TEAM_IDS + or str(team.id) in settings.NEW_ANALYTICS_CAPTURE_TEAM_IDS + ): + if random() < settings.NEW_ANALYTICS_CAPTURE_SAMPLING_RATE: + response["analytics"] = {"endpoint": settings.NEW_ANALYTICS_CAPTURE_ENDPOINT} + + if settings.ELEMENT_CHAIN_AS_STRING_TEAMS and str(team.id) in settings.ELEMENT_CHAIN_AS_STRING_TEAMS: + response["elementsChainAsString"] = True if team.session_recording_opt_in and ( on_permitted_recording_domain(team, request) or not team.recording_domains @@ -268,10 +276,12 @@ def get_decide(request: HttpRequest): if feature_flags: # Billing analytics for decide requests with feature flags - # Sample no. of decide requests with feature flags - if settings.DECIDE_BILLING_SAMPLING_RATE and random() < settings.DECIDE_BILLING_SAMPLING_RATE: - count = int(1 / settings.DECIDE_BILLING_SAMPLING_RATE) - increment_request_count(team.pk, count) + # Don't count if all requests are for survey targeting flags only. + if not all(flag.startswith(SURVEY_TARGETING_FLAG_PREFIX) for flag in feature_flags.keys()): + # Sample no. of decide requests with feature flags + if settings.DECIDE_BILLING_SAMPLING_RATE and random() < settings.DECIDE_BILLING_SAMPLING_RATE: + count = int(1 / settings.DECIDE_BILLING_SAMPLING_RATE) + increment_request_count(team.pk, count) else: # no auth provided diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index f513e9e74b6a4..92add84a0bcab 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -1,5 +1,6 @@ import json from typing import Any, Dict, List, Optional, cast +from datetime import datetime from django.db.models import QuerySet, Q, deletion from django.conf import settings @@ -16,6 +17,7 @@ from rest_framework.request import Request from rest_framework.response import Response from sentry_sdk import capture_exception +from posthog.api.cohort import CohortSerializer from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.routing import StructuredViewSetMixin @@ -503,11 +505,11 @@ def local_evaluation(self, request: request.Request, **kwargs): should_send_cohorts = "send_cohorts" in request.GET cohorts = {} - seen_cohorts_cache: Dict[str, Cohort] = {} + seen_cohorts_cache: Dict[int, Cohort] = {} if should_send_cohorts: seen_cohorts_cache = { - str(cohort.pk): cohort + cohort.pk: cohort for cohort in Cohort.objects.using(DATABASE_FOR_LOCAL_EVALUATION).filter( team_id=self.team_id, deleted=False ) @@ -547,12 +549,11 @@ def local_evaluation(self, request: request.Request, **kwargs): ): # don't duplicate queries for already added cohorts if id not in cohorts: - parsed_cohort_id = str(id) - if parsed_cohort_id in seen_cohorts_cache: - cohort = seen_cohorts_cache[parsed_cohort_id] + if id in seen_cohorts_cache: + cohort = seen_cohorts_cache[id] else: cohort = Cohort.objects.using(DATABASE_FOR_LOCAL_EVALUATION).get(id=id) - seen_cohorts_cache[parsed_cohort_id] = cohort + seen_cohorts_cache[id] = cohort if not cohort.is_static: cohorts[cohort.pk] = cohort.properties.to_dict() @@ -626,6 +627,28 @@ def user_blast_radius(self, request: request.Request, **kwargs): } ) + @action(methods=["POST"], detail=True) + def create_static_cohort_for_flag(self, request: request.Request, **kwargs): + feature_flag = self.get_object() + feature_flag_key = feature_flag.key + cohort_serializer = CohortSerializer( + data={ + "is_static": True, + "key": feature_flag_key, + "name": f'Users with feature flag {feature_flag_key} enabled at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', + }, + context={ + "request": request, + "team": self.team, + "team_id": self.team_id, + "from_feature_flag_key": feature_flag_key, + }, + ) + + cohort_serializer.is_valid(raise_exception=True) + cohort_serializer.save() + return Response({"cohort": cohort_serializer.data}, status=201) + @action(methods=["GET"], url_path="activity", detail=False) def all_activity(self, request: request.Request, **kwargs): limit = int(request.query_params.get("limit", "10")) diff --git a/posthog/api/insight.py b/posthog/api/insight.py index a31f2dd9dbe05..20ec5e93d0619 100644 --- a/posthog/api/insight.py +++ b/posthog/api/insight.py @@ -21,7 +21,6 @@ from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers from sentry_sdk import capture_exception -from statshog.defaults.django import statsd from posthog import schema from posthog.api.documentation import extend_schema @@ -32,6 +31,7 @@ TrendResultsSerializer, TrendSerializer, ) +from posthog.clickhouse.cancel import cancel_query_on_cluster from posthog.api.routing import StructuredViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin @@ -43,7 +43,6 @@ synchronously_update_cache, ) from posthog.caching.insights_api import should_refresh_insight -from posthog.client import sync_execute from posthog.constants import ( BREAKDOWN_VALUES_LIMIT, INSIGHT, @@ -95,7 +94,6 @@ ClickHouseSustainedRateThrottle, ) from posthog.settings import CAPTURE_TIME_TO_SEE_DATA, SITE_URL -from posthog.settings.data_stores import CLICKHOUSE_CLUSTER from prometheus_client import Counter from posthog.user_permissions import UserPermissionsSerializerMixin from posthog.utils import ( @@ -1034,11 +1032,7 @@ def activity(self, request: request.Request, **kwargs): def cancel(self, request: request.Request, **kwargs): if "client_query_id" not in request.data: raise serializers.ValidationError({"client_query_id": "Field is required."}) - sync_execute( - f"KILL QUERY ON CLUSTER '{CLICKHOUSE_CLUSTER}' WHERE query_id LIKE %(client_query_id)s", - {"client_query_id": f"{self.team.pk}_{request.data['client_query_id']}%"}, - ) - statsd.incr("clickhouse.query.cancellation_requested", tags={"team_id": self.team.pk}) + cancel_query_on_cluster(team_id=self.team.pk, client_query_id=request.data["client_query_id"]) return Response(status=status.HTTP_201_CREATED) @action(methods=["POST"], detail=False) diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py index 6f339f2976a5a..44648bd2cd0f2 100644 --- a/posthog/api/organization_feature_flag.py +++ b/posthog/api/organization_feature_flag.py @@ -1,8 +1,4 @@ -from posthog.api.routing import StructuredViewSetMixin -from posthog.api.feature_flag import FeatureFlagSerializer -from posthog.api.feature_flag import CanEditFeatureFlag -from posthog.models import FeatureFlag, Team -from posthog.permissions import OrganizationMemberPermissions +from typing import Dict from django.core.exceptions import ObjectDoesNotExist from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated @@ -12,6 +8,15 @@ viewsets, status, ) +from posthog.api.cohort import CohortSerializer +from posthog.api.routing import StructuredViewSetMixin +from posthog.api.feature_flag import FeatureFlagSerializer +from posthog.api.feature_flag import CanEditFeatureFlag +from posthog.api.shared import UserBasicSerializer +from posthog.models import FeatureFlag, Team +from posthog.models.cohort import Cohort +from posthog.models.filters.filter import Filter +from posthog.permissions import OrganizationMemberPermissions class OrganizationFeatureFlagView( @@ -40,15 +45,10 @@ def retrieve(self, request, *args, **kwargs): { "flag_id": flag.id, "team_id": flag.team_id, - "created_by": { - "id": flag.created_by.id, - "uuid": flag.created_by.uuid, - "distinct_id": flag.created_by.distinct_id, - "first_name": flag.created_by.first_name, - "email": flag.created_by.email, - "is_email_verified": flag.created_by.is_email_verified, - }, - "filters": flag.filters, + "created_by": UserBasicSerializer(flag.created_by).data + if hasattr(flag, "created_by") and flag.created_by + else None, + "filters": flag.get_filters(), "created_at": flag.created_at, "active": flag.active, } @@ -86,7 +86,7 @@ def copy_flags(self, request, *args, **kwargs): for target_project_id in target_project_ids: # Target project does not exist try: - Team.objects.get(id=target_project_id) + target_project = Team.objects.get(id=target_project_id) except ObjectDoesNotExist: failed_projects.append( { @@ -96,19 +96,84 @@ def copy_flags(self, request, *args, **kwargs): ) continue - context = { - "request": request, - "team_id": target_project_id, - } + # get all linked cohorts, sorted by creation order + seen_cohorts_cache: Dict[int, Cohort] = {} + sorted_cohort_ids = flag_to_copy.get_cohort_ids( + seen_cohorts_cache=seen_cohorts_cache, sort_by_topological_order=True + ) + + # destination cohort id is different from original cohort id - create mapping + name_to_dest_cohort_id: Dict[str, int] = {} + # create cohorts in the destination project + if len(sorted_cohort_ids): + for cohort_id in sorted_cohort_ids: + original_cohort = seen_cohorts_cache[cohort_id] + + # search in destination project by name + destination_cohort = Cohort.objects.filter( + name=original_cohort.name, team_id=target_project_id, deleted=False + ).first() + + # create new cohort in the destination project + if not destination_cohort: + prop_group = Filter( + data={"properties": original_cohort.properties.to_dict(), "is_simplified": True} + ).property_groups + + for prop in prop_group.flat: + if prop.type == "cohort" and not isinstance(prop.value, list): + try: + original_child_cohort_id = int(prop.value) + original_child_cohort = seen_cohorts_cache[original_child_cohort_id] + prop.value = name_to_dest_cohort_id[original_child_cohort.name] + except (ValueError, TypeError): + continue + + destination_cohort_serializer = CohortSerializer( + data={ + "team": target_project, + "name": original_cohort.name, + "groups": [], + "filters": {"properties": prop_group.to_dict()}, + "description": original_cohort.description, + "is_static": original_cohort.is_static, + }, + context={ + "request": request, + "team_id": target_project.id, + }, + ) + destination_cohort_serializer.is_valid(raise_exception=True) + destination_cohort = destination_cohort_serializer.save() + + if destination_cohort is not None: + name_to_dest_cohort_id[original_cohort.name] = destination_cohort.id + + # reference correct destination cohort ids in the flag + for group in flag_to_copy.conditions: + props = group.get("properties", []) + for prop in props: + if isinstance(prop, dict) and prop.get("type") == "cohort": + try: + original_cohort_id = int(prop["value"]) + cohort_name = (seen_cohorts_cache[original_cohort_id]).name + prop["value"] = name_to_dest_cohort_id[cohort_name] + except (ValueError, TypeError): + continue + flag_data = { "key": flag_to_copy.key, "name": flag_to_copy.name, - "filters": flag_to_copy.filters, + "filters": flag_to_copy.get_filters(), "active": flag_to_copy.active, "rollout_percentage": flag_to_copy.rollout_percentage, "ensure_experience_continuity": flag_to_copy.ensure_experience_continuity, "deleted": False, } + context = { + "request": request, + "team_id": target_project_id, + } existing_flag = FeatureFlag.objects.filter( key=feature_flag_key, team_id=target_project_id, deleted=False diff --git a/posthog/api/query.py b/posthog/api/query.py index 224aedce40464..021139911cb96 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -1,11 +1,11 @@ import json import re -from typing import Dict, Optional, cast, Any, List +import uuid +from typing import Dict -from django.http import HttpResponse, JsonResponse +from django.http import JsonResponse from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, OpenApiResponse -from pydantic import BaseModel from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import ParseError, ValidationError, NotAuthenticated @@ -17,46 +17,31 @@ from posthog import schema from posthog.api.documentation import extend_schema +from posthog.api.services.query import process_query from posthog.api.routing import StructuredViewSetMixin +from posthog.clickhouse.client.execute_async import ( + cancel_query, + enqueue_process_query_task, + get_query_status, +) from posthog.clickhouse.query_tagging import tag_queries from posthog.errors import ExposedCHQueryError from posthog.hogql.ai import PromptUnclear, write_sql_from_prompt -from posthog.hogql.database.database import create_hogql_database, serialize_database from posthog.hogql.errors import HogQLException -from posthog.hogql.metadata import get_hogql_metadata -from posthog.hogql.modifiers import create_default_modifiers_for_team -from posthog.hogql_queries.query_runner import get_query_runner -from posthog.models import Team from posthog.models.user import User from posthog.permissions import ( ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission, ) -from posthog.queries.time_to_see_data.serializers import ( - SessionEventsQuerySerializer, - SessionsQuerySerializer, -) -from posthog.queries.time_to_see_data.sessions import get_session_events, get_sessions from posthog.rate_limit import ( AIBurstRateThrottle, AISustainedRateThrottle, TeamRateThrottle, ) -from posthog.schema import HogQLMetadata +from posthog.schema import QueryStatus from posthog.utils import refresh_requested_by_client -QUERY_WITH_RUNNER = [ - "LifecycleQuery", - "TrendsQuery", - "WebOverviewQuery", - "WebTopSourcesQuery", - "WebTopClicksQuery", - "WebTopPagesQuery", - "WebStatsTableQuery", -] -QUERY_WITH_RUNNER_NO_CACHE = ["EventsQuery", "PersonsQuery", "HogQLQuery", "SessionsTimelineQuery"] - class QueryThrottle(TeamRateThrottle): scope = "query" @@ -116,40 +101,73 @@ def get_throttles(self): OpenApiParameter( "client_query_id", OpenApiTypes.STR, - description="Client provided query ID. Can be used to cancel queries.", + description="Client provided query ID. Can be used to retrieve the status or cancel the query.", + ), + OpenApiParameter( + "async", + OpenApiTypes.BOOL, + description=( + "(Experimental) " + "Whether to run the query asynchronously. Defaults to False." + " If True, the `id` of the query can be used to check the status and to cancel it." + ), ), ], responses={ 200: OpenApiResponse(description="Query results"), }, ) - def list(self, request: Request, **kw) -> HttpResponse: - self._tag_client_query_id(request.GET.get("client_query_id")) - query_json = QuerySchemaParser.validate_query(self._query_json_from_request(request)) - # allow lists as well as dicts in response with safe=False - try: - return JsonResponse(process_query(self.team, query_json, request=request), safe=False) - except HogQLException as e: - raise ValidationError(str(e)) - except ExposedCHQueryError as e: - raise ValidationError(str(e), e.code_name) - - def post(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs) -> JsonResponse: request_json = request.data query_json = request_json.get("query") - self._tag_client_query_id(request_json.get("client_query_id")) - # allow lists as well as dicts in response with safe=False + query_async = request_json.get("async", False) + refresh_requested = refresh_requested_by_client(request) + + client_query_id = request_json.get("client_query_id") or uuid.uuid4().hex + self._tag_client_query_id(client_query_id) + + if query_async: + query_id = enqueue_process_query_task( + team_id=self.team.pk, + query_json=query_json, + query_id=client_query_id, + refresh_requested=refresh_requested, + ) + return JsonResponse(QueryStatus(id=query_id, team_id=self.team.pk).model_dump(), safe=False) + try: - return JsonResponse(process_query(self.team, query_json, request=request), safe=False) - except HogQLException as e: - raise ValidationError(str(e)) - except ExposedCHQueryError as e: - raise ValidationError(str(e), e.code_name) + result = process_query(self.team, query_json, refresh_requested=refresh_requested) + return JsonResponse(result, safe=False) + except (HogQLException, ExposedCHQueryError) as e: + raise ValidationError(str(e), getattr(e, "code_name", None)) except Exception as e: self.handle_column_ch_error(e) capture_exception(e) raise e + @extend_schema( + description="(Experimental)", + responses={ + 200: OpenApiResponse(description="Query status"), + }, + ) + @extend_schema( + description="(Experimental)", + responses={ + 200: OpenApiResponse(description="Query status"), + }, + ) + def retrieve(self, request: Request, pk=None, *args, **kwargs) -> JsonResponse: + status = get_query_status(team_id=self.team.pk, query_id=pk) + return JsonResponse(status.__dict__, safe=False) + + @extend_schema( + description="(Experimental)", + ) + def destroy(self, request, pk=None, *args, **kwargs): + cancel_query(self.team.pk, pk) + return Response(status=204) + @action(methods=["GET"], detail=False) def draft_sql(self, request: Request, *args, **kwargs) -> Response: if not isinstance(request.user, User): @@ -177,8 +195,10 @@ def handle_column_ch_error(self, error): return def _tag_client_query_id(self, query_id: str | None): - if query_id is not None: - tag_queries(client_query_id=query_id) + if query_id is None: + return + + tag_queries(client_query_id=query_id) def _query_json_from_request(self, request): if request.method == "POST": @@ -205,73 +225,3 @@ def parsing_error(ex): except (json.JSONDecodeError, UnicodeDecodeError) as error_main: raise ValidationError("Invalid JSON: %s" % (str(error_main))) return query - - -def _unwrap_pydantic(response: Any) -> Dict | List: - if isinstance(response, list): - return [_unwrap_pydantic(item) for item in response] - - elif isinstance(response, BaseModel): - resp1: Dict[str, Any] = {} - for key in response.__fields__.keys(): - resp1[key] = _unwrap_pydantic(getattr(response, key)) - return resp1 - - elif isinstance(response, dict): - resp2: Dict[str, Any] = {} - for key in response.keys(): - resp2[key] = _unwrap_pydantic(response.get(key)) - return resp2 - - return response - - -def _unwrap_pydantic_dict(response: Any) -> Dict: - return cast(dict, _unwrap_pydantic(response)) - - -def process_query( - team: Team, - query_json: Dict, - in_export_context: Optional[bool] = False, - request: Optional[Request] = None, -) -> Dict: - # query_json has been parsed by QuerySchemaParser - # it _should_ be impossible to end up in here with a "bad" query - query_kind = query_json.get("kind") - tag_queries(query=query_json) - - if query_kind in QUERY_WITH_RUNNER: - refresh_requested = refresh_requested_by_client(request) if request else False - query_runner = get_query_runner(query_json, team, in_export_context=in_export_context) - return _unwrap_pydantic_dict(query_runner.run(refresh_requested=refresh_requested)) - elif query_kind in QUERY_WITH_RUNNER_NO_CACHE: - query_runner = get_query_runner(query_json, team, in_export_context=in_export_context) - return _unwrap_pydantic_dict(query_runner.calculate()) - elif query_kind == "HogQLMetadata": - metadata_query = HogQLMetadata.model_validate(query_json) - metadata_response = get_hogql_metadata(query=metadata_query, team=team) - return _unwrap_pydantic_dict(metadata_response) - elif query_kind == "DatabaseSchemaQuery": - database = create_hogql_database(team.pk, modifiers=create_default_modifiers_for_team(team)) - return serialize_database(database) - elif query_kind == "TimeToSeeDataSessionsQuery": - sessions_query_serializer = SessionsQuerySerializer(data=query_json) - sessions_query_serializer.is_valid(raise_exception=True) - return {"results": get_sessions(sessions_query_serializer).data} - elif query_kind == "TimeToSeeDataQuery": - serializer = SessionEventsQuerySerializer( - data={ - "team_id": team.pk, - "session_start": query_json["sessionStart"], - "session_end": query_json["sessionEnd"], - "session_id": query_json["sessionId"], - } - ) - serializer.is_valid(raise_exception=True) - return get_session_events(serializer) or {} - else: - if query_json.get("source"): - return process_query(team, query_json["source"]) - - raise ValidationError(f"Unsupported query kind: {query_kind}") diff --git a/posthog/api/search.py b/posthog/api/search.py index 9f0f1fe77a39f..cbdd898949fd1 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -1,10 +1,11 @@ +import functools import re from typing import Any from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.db.models import Model, Value, CharField, F, QuerySet -from django.db.models.functions import Cast +from django.db.models.functions import Cast, JSONObject # type: ignore from django.http import HttpResponse -from rest_framework import viewsets +from rest_framework import viewsets, serializers from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -12,24 +13,95 @@ from posthog.api.routing import StructuredViewSetMixin from posthog.permissions import ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission from posthog.models import Action, Cohort, Insight, Dashboard, FeatureFlag, Experiment, Team +from posthog.models.notebook.notebook import Notebook + +LIMIT = 25 + + +ENTITY_MAP = { + "insight": { + "klass": Insight, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description", "filters", "query"], + }, + "dashboard": { + "klass": Dashboard, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "experiment": { + "klass": Experiment, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "feature_flag": {"klass": FeatureFlag, "search_fields": {"key": "A", "name": "C"}, "extra_fields": ["key", "name"]}, + "notebook": { + "klass": Notebook, + "search_fields": {"title": "A", "text_content": "C"}, + "extra_fields": ["title", "text_content"], + }, + "action": { + "klass": Action, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, + "cohort": { + "klass": Cohort, + "search_fields": {"name": "A", "description": "C"}, + "extra_fields": ["name", "description"], + }, +} +""" +Map of entity names to their class, search_fields and extra_fields. + +The value in search_fields corresponds to the PostgreSQL weighting i.e. A, B, C or D. +""" + + +class QuerySerializer(serializers.Serializer): + """Validates and formats query params.""" + + q = serializers.CharField(required=False, default="") + entities = serializers.MultipleChoiceField(required=False, choices=list(ENTITY_MAP.keys())) + + def validate_q(self, value: str): + return process_query(value) class SearchViewSet(StructuredViewSetMixin, viewsets.ViewSet): permission_classes = [IsAuthenticated, ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission] def list(self, request: Request, **kw) -> HttpResponse: - query = process_query(request.GET.get("q", "").strip()) - counts = {} + # parse query params + query_serializer = QuerySerializer(data=self.request.query_params) + query_serializer.is_valid(raise_exception=True) + params = query_serializer.validated_data + + counts = {key: None for key in ENTITY_MAP} + # get entities to search from params or default to all entities + entities = params["entities"] if len(params["entities"]) > 0 else set(ENTITY_MAP.keys()) + query = params["q"] # empty queryset to union things onto it qs = Dashboard.objects.annotate(type=Value("empty", output_field=CharField())).filter(team=self.team).none() - for klass in (Action, Cohort, Insight, Dashboard, Experiment, FeatureFlag): - klass_qs, type = class_queryset(klass, team=self.team, query=query) + # add entities + for entity_meta in [ENTITY_MAP[entity] for entity in entities]: + klass_qs, entity_name = class_queryset( + klass=entity_meta.get("klass"), # type: ignore + team=self.team, + query=query, + search_fields=entity_meta.get("search_fields"), # type: ignore + extra_fields=entity_meta.get("extra_fields"), + ) qs = qs.union(klass_qs) - counts[type] = klass_qs.count() + counts[entity_name] = klass_qs.count() + + # order by rank + if query: + qs = qs.order_by("-rank") - return Response({"results": qs, "counts": counts}) + return Response({"results": qs[:LIMIT], "counts": counts}) UNSAFE_CHARACTERS = r"[\'&|!<>():]" @@ -49,29 +121,48 @@ def process_query(query: str): return query -def class_queryset(klass: type[Model], team: Team, query: str | None): +def class_queryset( + klass: type[Model], + team: Team, + query: str | None, + search_fields: dict[str, str], + extra_fields: dict | None, +): """Builds a queryset for the class.""" - type = class_to_type(klass) - values = ["type", "result_id", "name"] + entity_type = class_to_entity_name(klass) + values = ["type", "result_id", "extra_fields"] - qs: QuerySet[Any] = klass.objects.filter(team=team) - qs = qs.annotate(type=Value(type, output_field=CharField())) + qs: QuerySet[Any] = klass.objects.filter(team=team) # filter team + qs = qs.annotate(type=Value(entity_type, output_field=CharField())) # entity type - if type == "insight": + # entity id + if entity_type == "insight" or entity_type == "notebook": qs = qs.annotate(result_id=F("short_id")) else: qs = qs.annotate(result_id=Cast("pk", CharField())) + # extra fields + if extra_fields: + qs = qs.annotate(extra_fields=JSONObject(**{field: field for field in extra_fields})) + else: + qs = qs.annotate(extra_fields=JSONObject()) + + # full-text search rank if query: - qs = qs.annotate(rank=SearchRank(SearchVector("name"), SearchQuery(query, search_type="raw"))) + search_vectors = [SearchVector(key, weight=value, config="simple") for key, value in search_fields.items()] + combined_vector = functools.reduce(lambda a, b: a + b, search_vectors) + qs = qs.annotate( + rank=SearchRank(combined_vector, SearchQuery(query, config="simple", search_type="raw")), + ) qs = qs.filter(rank__gt=0.05) - qs = qs.order_by("-rank") values.append("rank") + # specify fields to fetch qs = qs.values(*values) - return qs, type + + return qs, entity_type -def class_to_type(klass: type[Model]): +def class_to_entity_name(klass: type[Model]): """Converts the class name to snake case.""" return re.sub("(?!^)([A-Z]+)", r"_\1", klass.__name__).lower() diff --git a/frontend/src/lib/components/AddToDashboard/AddToDashboard.scss b/posthog/api/services/__init__.py similarity index 100% rename from frontend/src/lib/components/AddToDashboard/AddToDashboard.scss rename to posthog/api/services/__init__.py diff --git a/posthog/api/services/query.py b/posthog/api/services/query.py new file mode 100644 index 0000000000000..1ef831bde1b82 --- /dev/null +++ b/posthog/api/services/query.py @@ -0,0 +1,97 @@ +import structlog +from typing import Any, Dict, List, Optional, cast + +from pydantic import BaseModel +from rest_framework.exceptions import ValidationError + +from posthog.clickhouse.query_tagging import tag_queries +from posthog.hogql.database.database import create_hogql_database, serialize_database +from posthog.hogql.metadata import get_hogql_metadata +from posthog.hogql.modifiers import create_default_modifiers_for_team +from posthog.hogql_queries.query_runner import get_query_runner +from posthog.models import Team +from posthog.queries.time_to_see_data.serializers import SessionEventsQuerySerializer, SessionsQuerySerializer +from posthog.queries.time_to_see_data.sessions import get_session_events, get_sessions +from posthog.schema import HogQLMetadata + +logger = structlog.get_logger(__name__) + +QUERY_WITH_RUNNER = [ + "LifecycleQuery", + "TrendsQuery", + "WebOverviewQuery", + "WebTopSourcesQuery", + "WebTopClicksQuery", + "WebTopPagesQuery", + "WebStatsTableQuery", +] +QUERY_WITH_RUNNER_NO_CACHE = ["EventsQuery", "PersonsQuery", "HogQLQuery", "SessionsTimelineQuery"] + + +def _unwrap_pydantic(response: Any) -> Dict | List: + if isinstance(response, list): + return [_unwrap_pydantic(item) for item in response] + + elif isinstance(response, BaseModel): + resp1: Dict[str, Any] = {} + for key in response.__fields__.keys(): + resp1[key] = _unwrap_pydantic(getattr(response, key)) + return resp1 + + elif isinstance(response, dict): + resp2: Dict[str, Any] = {} + for key in response.keys(): + resp2[key] = _unwrap_pydantic(response.get(key)) + return resp2 + + return response + + +def _unwrap_pydantic_dict(response: Any) -> Dict: + return cast(dict, _unwrap_pydantic(response)) + + +def process_query( + team: Team, + query_json: Dict, + in_export_context: Optional[bool] = False, + refresh_requested: Optional[bool] = False, +) -> Dict: + # query_json has been parsed by QuerySchemaParser + # it _should_ be impossible to end up in here with a "bad" query + query_kind = query_json.get("kind") + tag_queries(query=query_json) + + if query_kind in QUERY_WITH_RUNNER: + query_runner = get_query_runner(query_json, team, in_export_context=in_export_context) + return _unwrap_pydantic_dict(query_runner.run(refresh_requested=refresh_requested)) + elif query_kind in QUERY_WITH_RUNNER_NO_CACHE: + query_runner = get_query_runner(query_json, team, in_export_context=in_export_context) + return _unwrap_pydantic_dict(query_runner.calculate()) + elif query_kind == "HogQLMetadata": + metadata_query = HogQLMetadata.model_validate(query_json) + metadata_response = get_hogql_metadata(query=metadata_query, team=team) + return _unwrap_pydantic_dict(metadata_response) + elif query_kind == "DatabaseSchemaQuery": + database = create_hogql_database(team.pk, modifiers=create_default_modifiers_for_team(team)) + return serialize_database(database) + elif query_kind == "TimeToSeeDataSessionsQuery": + sessions_query_serializer = SessionsQuerySerializer(data=query_json) + sessions_query_serializer.is_valid(raise_exception=True) + return {"results": get_sessions(sessions_query_serializer).data} + elif query_kind == "TimeToSeeDataQuery": + serializer = SessionEventsQuerySerializer( + data={ + "team_id": team.pk, + "session_start": query_json["sessionStart"], + "session_end": query_json["sessionEnd"], + "session_id": query_json["sessionId"], + } + ) + serializer.is_valid(raise_exception=True) + return get_session_events(serializer) or {} + else: + if query_json.get("source"): + return process_query(team, query_json["source"]) + + raise ValidationError(f"Unsupported query kind: {query_kind}") diff --git a/posthog/api/survey.py b/posthog/api/survey.py index a2b3e8c3fcdd3..ef3e8c166dac8 100644 --- a/posthog/api/survey.py +++ b/posthog/api/survey.py @@ -221,19 +221,29 @@ def update(self, instance: Survey, validated_data): existing_flag_serializer.is_valid(raise_exception=True) existing_flag_serializer.save() else: - new_flag = self._create_new_targeting_flag(instance.name, new_filters) + new_flag = self._create_new_targeting_flag(instance.name, new_filters, bool(instance.start_date)) validated_data["targeting_flag_id"] = new_flag.id validated_data.pop("targeting_flag_filters") + end_date = validated_data.get("end_date") + if instance.targeting_flag: + # turn off feature flag if survey is ended + if end_date is None: + instance.targeting_flag.active = True + else: + instance.targeting_flag.active = False + instance.targeting_flag.save() + return super().update(instance, validated_data) - def _create_new_targeting_flag(self, name, filters): + def _create_new_targeting_flag(self, name, filters, active=False): feature_flag_key = slugify(f"{SURVEY_TARGETING_FLAG_PREFIX}{name}") feature_flag_serializer = FeatureFlagSerializer( data={ "key": feature_flag_key, "name": f"Targeting flag for survey {name}", "filters": filters, + "active": active, }, context=self.context, ) diff --git a/posthog/api/test/__snapshots__/test_action.ambr b/posthog/api/test/__snapshots__/test_action.ambr index 66a6b7ac1190e..e09dabc5bf688 100644 --- a/posthog/api/test/__snapshots__/test_action.ambr +++ b/posthog/api/test/__snapshots__/test_action.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ @@ -226,7 +227,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ @@ -552,7 +554,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_annotation.ambr b/posthog/api/test/__snapshots__/test_annotation.ambr index 50d15b6145259..1373bf5f4060b 100644 --- a/posthog/api/test/__snapshots__/test_annotation.ambr +++ b/posthog/api/test/__snapshots__/test_annotation.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ @@ -150,7 +151,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ @@ -474,7 +476,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 93183504138f1..08263aafa6673 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -81,12 +81,29 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10 + ' + SELECT "posthog_pluginconfig"."id", + "posthog_pluginconfig"."web_token", + "posthog_pluginsourcefile"."updated_at", + "posthog_plugin"."updated_at", + "posthog_pluginconfig"."updated_at" + FROM "posthog_pluginconfig" + INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") + INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") + WHERE ("posthog_pluginconfig"."enabled" + AND "posthog_pluginsourcefile"."filename" = 'site.ts' + AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' + AND "posthog_pluginconfig"."team_id" = 2) + ' +--- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.2 ' SELECT "posthog_organizationmembership"."id", @@ -306,7 +323,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -461,7 +479,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -604,7 +623,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 diff --git a/posthog/api/test/__snapshots__/test_early_access_feature.ambr b/posthog/api/test/__snapshots__/test_early_access_feature.ambr index 5328b0eaa8e62..d7908693f9cb2 100644 --- a/posthog/api/test/__snapshots__/test_early_access_feature.ambr +++ b/posthog/api/test/__snapshots__/test_early_access_feature.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -174,7 +175,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 /*controller='posthog.api.early_access_feature.early_access_features',route='%5Eapi/early_access_features/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_element.ambr b/posthog/api/test/__snapshots__/test_element.ambr index 2f1e429bac578..97b03322b3e2d 100644 --- a/posthog/api/test/__snapshots__/test_element.ambr +++ b/posthog/api/test/__snapshots__/test_element.ambr @@ -78,7 +78,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='element-stats',route='api/element/stats/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 6fc6c63c17f20..ffe583b425eac 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -334,6 +334,2196 @@ HAVING max(is_deleted) = 0 SETTINGS optimize_aggregation_in_order = 1) ' --- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.1 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.10 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 2 + OFFSET 4 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.11 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.12 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.13 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 10 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.14 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 1) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.15 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 10 + OFFSET 10 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.16 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.17 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.2 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 2 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.3 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.4 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.5 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.6 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.7 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 2 + OFFSET 2 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.8 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.9 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature-new' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.1 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.10 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.11 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.2 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE (NOT "posthog_cohort"."deleted" + AND "posthog_cohort"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.3 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.4 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.5 + ' + SELECT ("posthog_person"."id" IS NULL + OR "posthog_person"."id" IS NULL + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR "posthog_person"."id" IS NULL) AS "flag_X_condition_0" + FROM "posthog_person" + INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") + WHERE ("posthog_persondistinctid"."distinct_id" = 'person1' + AND "posthog_persondistinctid"."team_id" = 2 + AND "posthog_person"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.6 + ' + SELECT ("posthog_person"."id" IS NOT NULL + OR "posthog_person"."id" IS NULL + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR "posthog_person"."id" IS NULL) AS "flag_X_condition_0" + FROM "posthog_person" + INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") + WHERE ("posthog_persondistinctid"."distinct_id" = 'person2' + AND "posthog_persondistinctid"."team_id" = 2 + AND "posthog_person"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.7 + ' + SELECT ("posthog_person"."id" IS NULL + OR "posthog_person"."id" IS NOT NULL + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR "posthog_person"."id" IS NULL) AS "flag_X_condition_0" + FROM "posthog_person" + INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") + WHERE ("posthog_persondistinctid"."distinct_id" = 'person3' + AND "posthog_persondistinctid"."team_id" = 2 + AND "posthog_person"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.8 + ' + SELECT ("posthog_person"."id" IS NULL + OR "posthog_person"."id" IS NULL + OR EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U0 + WHERE (U0."cohort_id" = 2 + AND U0."cohort_id" = 2 + AND U0."person_id" = "posthog_person"."id") + LIMIT 1) + OR "posthog_person"."id" IS NULL) AS "flag_X_condition_0" + FROM "posthog_person" + INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") + WHERE ("posthog_persondistinctid"."distinct_id" = 'person4' + AND "posthog_persondistinctid"."team_id" = 2 + AND "posthog_person"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too.9 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + OFFSET 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.1 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.10 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.11 + ' + SELECT ("posthog_person"."properties" -> 'key') IS NOT NULL AS "flag_X_condition_0" + FROM "posthog_person" + INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") + WHERE ("posthog_persondistinctid"."distinct_id" = 'person3' + AND "posthog_persondistinctid"."team_id" = 2 + AND "posthog_person"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.12 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + OFFSET 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.13 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.14 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.2 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.3 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.4 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + OFFSET 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.5 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.6 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.7 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature-new' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.8 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_default_person_properties_adjustment.9 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_deleted_flag + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.1 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.10 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.11 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 21 + OFFSET 5000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.12 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 5000 + OFFSET 5000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.13 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.14 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.2 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.3 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.4 + ' + SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key", + "posthog_featureflaghashkeyoverride"."hash_key", + "posthog_featureflaghashkeyoverride"."person_id" + FROM "posthog_featureflaghashkeyoverride" + WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) + AND "posthog_featureflaghashkeyoverride"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.5 + ' + SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key", + "posthog_featureflaghashkeyoverride"."hash_key", + "posthog_featureflaghashkeyoverride"."person_id" + FROM "posthog_featureflaghashkeyoverride" + WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) + AND "posthog_featureflaghashkeyoverride"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.6 + ' + SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key", + "posthog_featureflaghashkeyoverride"."hash_key", + "posthog_featureflaghashkeyoverride"."person_id" + FROM "posthog_featureflaghashkeyoverride" + WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) + AND "posthog_featureflaghashkeyoverride"."team_id" = 2) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.7 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + OFFSET 1000 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.8 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.9 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature3' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.1 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.2 + ' + DECLARE "_django_curs_X" NO SCROLL + CURSOR WITHOUT HOLD + FOR + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.3 + ' + SELECT "posthog_persondistinctid"."distinct_id" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."person_id" = 2 + AND "posthog_persondistinctid"."team_id" = 2) + LIMIT 1 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_group_flag.4 + ' + SELECT "posthog_grouptypemapping"."id", + "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."group_type", + "posthog_grouptypemapping"."group_type_index", + "posthog_grouptypemapping"."name_singular", + "posthog_grouptypemapping"."name_plural" + FROM "posthog_grouptypemapping" + WHERE "posthog_grouptypemapping"."team_id" = 2 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_inactive_flag + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_invalid_flags + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_non_existing_flag + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature2' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort + ' + SELECT "posthog_user"."id", + "posthog_user"."password", + "posthog_user"."last_login", + "posthog_user"."first_name", + "posthog_user"."last_name", + "posthog_user"."is_staff", + "posthog_user"."is_active", + "posthog_user"."date_joined", + "posthog_user"."uuid", + "posthog_user"."current_organization_id", + "posthog_user"."current_team_id", + "posthog_user"."email", + "posthog_user"."pending_email", + "posthog_user"."temporary_token", + "posthog_user"."distinct_id", + "posthog_user"."is_email_verified", + "posthog_user"."has_seen_product_intro_for", + "posthog_user"."email_opt_in", + "posthog_user"."partial_notification_settings", + "posthog_user"."anonymize_data", + "posthog_user"."toolbar_mode", + "posthog_user"."events_column_config" + FROM "posthog_user" + WHERE "posthog_user"."id" = 2 + LIMIT 21 /**/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.1 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.10 + ' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."id" IN + (SELECT U0."id" + FROM "posthog_persondistinctid" U0 + WHERE U0."person_id" = "posthog_persondistinctid"."person_id" + LIMIT 3) + AND "posthog_persondistinctid"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */)) /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.11 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 + OFFSET 1000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.12 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.13 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.14 + ' + /* user_id:189 celery:posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag */ + SELECT count(DISTINCT person_id) + FROM person_static_cohort + WHERE team_id = 2 + AND cohort_id = 2 + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.15 + ' + /* user_id:0 request:_snapshot_ */ + SELECT id + FROM person + INNER JOIN + (SELECT person_id + FROM person_static_cohort + WHERE team_id = 2 + AND cohort_id = 2 + GROUP BY person_id, + cohort_id, + team_id) cohort_persons ON cohort_persons.person_id = person.id + WHERE team_id = 2 + GROUP BY id + HAVING max(is_deleted) = 0 + ORDER BY argMax(person.created_at, version) DESC, id DESC + LIMIT 100 SETTINGS optimize_aggregation_in_order = 1 + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.16 + ' + /* user_id:0 request:_snapshot_ */ + SELECT id + FROM person + INNER JOIN + (SELECT person_id + FROM person_static_cohort + WHERE team_id = 2 + AND cohort_id = 2 + GROUP BY person_id, + cohort_id, + team_id) cohort_persons ON cohort_persons.person_id = person.id + WHERE team_id = 2 + GROUP BY id + HAVING max(is_deleted) = 0 + ORDER BY argMax(person.created_at, version) DESC, id DESC + LIMIT 100 SETTINGS optimize_aggregation_in_order = 1 + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.17 + ' + SELECT "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id" + FROM "posthog_persondistinctid" + WHERE ("posthog_persondistinctid"."distinct_id" IN ('person3') + AND "posthog_persondistinctid"."team_id" = 2) /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.18 + ' + SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key", + "posthog_featureflaghashkeyoverride"."hash_key", + "posthog_featureflaghashkeyoverride"."person_id" + FROM "posthog_featureflaghashkeyoverride" + WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) + AND "posthog_featureflaghashkeyoverride"."team_id" = 2) /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.19 + ' + SELECT "posthog_person"."uuid" + FROM "posthog_person" + WHERE ("posthog_person"."team_id" = 2 + AND "posthog_person"."uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid /* ... */) + AND NOT (EXISTS + (SELECT (1) AS "a" + FROM "posthog_cohortpeople" U1 + WHERE (U1."cohort_id" = 2 + AND U1."person_id" = "posthog_person"."id") + LIMIT 1))) /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.2 + ' + SELECT "posthog_organizationmembership"."id", + "posthog_organizationmembership"."organization_id", + "posthog_organizationmembership"."user_id", + "posthog_organizationmembership"."level", + "posthog_organizationmembership"."joined_at", + "posthog_organizationmembership"."updated_at", + "posthog_organization"."id", + "posthog_organization"."name", + "posthog_organization"."slug", + "posthog_organization"."created_at", + "posthog_organization"."updated_at", + "posthog_organization"."plugins_access_level", + "posthog_organization"."for_internal_metrics", + "posthog_organization"."is_member_join_email_enabled", + "posthog_organization"."enforce_2fa", + "posthog_organization"."customer_id", + "posthog_organization"."available_product_features", + "posthog_organization"."usage", + "posthog_organization"."never_drop_data", + "posthog_organization"."setup_section_2_completed", + "posthog_organization"."personalization", + "posthog_organization"."domain_whitelist", + "posthog_organization"."available_features" + FROM "posthog_organizationmembership" + INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id") + WHERE "posthog_organizationmembership"."user_id" = 2 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.20 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.3 + ' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.4 + ' + SELECT "posthog_organization"."id", + "posthog_organization"."name", + "posthog_organization"."slug", + "posthog_organization"."created_at", + "posthog_organization"."updated_at", + "posthog_organization"."plugins_access_level", + "posthog_organization"."for_internal_metrics", + "posthog_organization"."is_member_join_email_enabled", + "posthog_organization"."enforce_2fa", + "posthog_organization"."customer_id", + "posthog_organization"."available_product_features", + "posthog_organization"."usage", + "posthog_organization"."never_drop_data", + "posthog_organization"."setup_section_2_completed", + "posthog_organization"."personalization", + "posthog_organization"."domain_whitelist", + "posthog_organization"."available_features" + FROM "posthog_organization" + WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.5 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics", + "posthog_user"."id", + "posthog_user"."password", + "posthog_user"."last_login", + "posthog_user"."first_name", + "posthog_user"."last_name", + "posthog_user"."is_staff", + "posthog_user"."is_active", + "posthog_user"."date_joined", + "posthog_user"."uuid", + "posthog_user"."current_organization_id", + "posthog_user"."current_team_id", + "posthog_user"."email", + "posthog_user"."pending_email", + "posthog_user"."temporary_token", + "posthog_user"."distinct_id", + "posthog_user"."is_email_verified", + "posthog_user"."requested_password_reset_at", + "posthog_user"."has_seen_product_intro_for", + "posthog_user"."email_opt_in", + "posthog_user"."partial_notification_settings", + "posthog_user"."anonymize_data", + "posthog_user"."toolbar_mode", + "posthog_user"."events_column_config" + FROM "posthog_featureflag" + LEFT OUTER JOIN "posthog_user" ON ("posthog_featureflag"."created_by_id" = "posthog_user"."id") + WHERE ("posthog_featureflag"."team_id" = 2 + AND "posthog_featureflag"."id" = 2) + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.6 + ' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 2 + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.7 + ' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."key" = 'some-feature' + AND "posthog_featureflag"."team_id" = 2) + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.8 + ' + SELECT "posthog_cohort"."id", + "posthog_cohort"."name", + "posthog_cohort"."description", + "posthog_cohort"."team_id", + "posthog_cohort"."deleted", + "posthog_cohort"."filters", + "posthog_cohort"."version", + "posthog_cohort"."pending_version", + "posthog_cohort"."count", + "posthog_cohort"."created_by_id", + "posthog_cohort"."created_at", + "posthog_cohort"."is_calculating", + "posthog_cohort"."last_calculation", + "posthog_cohort"."errors_calculating", + "posthog_cohort"."is_static", + "posthog_cohort"."groups" + FROM "posthog_cohort" + WHERE "posthog_cohort"."id" = 2 + LIMIT 21 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- +# name: TestFeatureFlag.test_creating_static_cohort.9 + ' + SELECT "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_person" + WHERE "posthog_person"."team_id" = 2 + ORDER BY "posthog_person"."id" ASC + LIMIT 1000 /*controller='project_feature_flags-create-static-cohort-for-flag',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/create_static_cohort_for_flag/%3F%24'*/ + ' +--- # name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db ' WITH target_person_ids AS diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index a5d6fadd37533..4d3b882aa5b51 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -170,7 +170,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime('2012-01-08 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1)) - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'))) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0))) AND (step_0 = 1 OR step_1 = 1) )) WHERE step_0 = 1 )) @@ -215,11 +215,11 @@ person.person_props as person_props , if(event = 'user signed up' AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')), 1, 0) as step_0, + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)), 1, 0) as step_0, if(step_0 = 1, timestamp, null) as latest_0, if(event = 'user did things' AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')), 1, 0) as step_1, + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)), 1, 0) as step_1, if(step_1 = 1, timestamp, null) as latest_1 FROM events e INNER JOIN @@ -438,7 +438,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(greater(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1)) - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'))) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -506,7 +506,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(greater(toInt64OrNull(nullIf(nullIf(events.mat_int_value, ''), 'null')), 10), 0), 1)) - AND (like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'))) + AND (ifNull(like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'), 0))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -548,7 +548,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')) + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -590,7 +590,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND (and(ifNull(less(toInt64OrNull(nullIf(nullIf(events.mat_int_value, ''), 'null')), 10), 0), 1) - AND like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%')) + AND ifNull(like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'), 0)) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -669,7 +669,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -719,7 +720,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -852,7 +854,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1075,7 +1078,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1225,6 +1229,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -1352,6 +1357,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1460,6 +1466,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1595,7 +1602,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1687,7 +1695,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1771,7 +1780,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1828,7 +1838,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index 08d3e86211a64..e2b87fe73b2a1 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -125,7 +125,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -221,7 +222,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -313,7 +315,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -515,7 +518,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -648,7 +652,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -787,7 +792,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -879,7 +885,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1085,7 +1092,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1211,7 +1219,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1268,7 +1277,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1414,7 +1424,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1429,8 +1440,8 @@ "posthog_experiment"."filters", "posthog_experiment"."parameters", "posthog_experiment"."secondary_metrics", - "posthog_experiment"."feature_flag_id", "posthog_experiment"."created_by_id", + "posthog_experiment"."feature_flag_id", "posthog_experiment"."start_date", "posthog_experiment"."end_date", "posthog_experiment"."created_at", @@ -1675,7 +1686,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid /*controller='organization_feature_flags-detail',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cfeature_flag_key%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' diff --git a/posthog/api/test/__snapshots__/test_preflight.ambr b/posthog/api/test/__snapshots__/test_preflight.ambr index 2d2cb9a03cbfe..dcd94e83ea36c 100644 --- a/posthog/api/test/__snapshots__/test_preflight.ambr +++ b/posthog/api/test/__snapshots__/test_preflight.ambr @@ -89,7 +89,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='posthog.views.preflight_check',route='%5E_preflight/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 05501e8c5ac45..d467a08e15b7a 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -41,8 +41,7 @@ 'a%sd', concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', '')), '')) FROM events - WHERE and(equals(events.team_id, 2), ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', ''), '%/%'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', '')) - and isNull('%/%')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', ''), '%/%'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, @@ -93,8 +92,7 @@ 'a%sd', concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(nullIf(nullIf(events.mat_key, ''), 'null')), '')) FROM events - WHERE and(equals(events.team_id, 2), ifNull(ilike(nullIf(nullIf(events.mat_path, ''), 'null'), '%/%'), isNull(nullIf(nullIf(events.mat_path, ''), 'null')) - and isNull('%/%')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), ifNull(ilike(nullIf(nullIf(events.mat_path, ''), 'null'), '%/%'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, @@ -533,7 +531,7 @@ SELECT count(), events.event FROM events - WHERE and(equals(events.team_id, 2), or(equals(events.event, 'sign up'), like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', ''), '%val2')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), or(equals(events.event, 'sign up'), ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', ''), '%val2'), 0)), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) GROUP BY events.event ORDER BY count() DESC, events.event ASC LIMIT 101 diff --git a/posthog/api/test/__snapshots__/test_survey.ambr b/posthog/api/test/__snapshots__/test_survey.ambr index 4536dfb45977e..1d5a134d8111f 100644 --- a/posthog/api/test/__snapshots__/test_survey.ambr +++ b/posthog/api/test/__snapshots__/test_survey.ambr @@ -150,7 +150,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 /*controller='posthog.api.survey.surveys',route='%5Eapi/surveys/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/batch_exports/test_delete.py b/posthog/api/test/batch_exports/test_delete.py index 20375cecbb768..cc07ed4675151 100644 --- a/posthog/api/test/batch_exports/test_delete.py +++ b/posthog/api/test/batch_exports/test_delete.py @@ -241,3 +241,48 @@ def test_deletes_are_partitioned_by_team_id(client: HttpClient): # Make sure we can still get the export with the right user response = get_batch_export(client, team.pk, batch_export_id) assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db(transaction=True) +def test_delete_batch_export_even_without_underlying_schedule(client: HttpClient): + """Test deleting a BatchExport completes even if underlying Schedule was already deleted.""" + temporal = sync_connect() + + destination_data = { + "type": "S3", + "config": { + "bucket_name": "my-production-s3-bucket", + "region": "us-east-1", + "prefix": "posthog-events/", + "aws_access_key_id": "abc123", + "aws_secret_access_key": "secret", + }, + } + batch_export_data = { + "name": "my-production-s3-bucket-destination", + "destination": destination_data, + "interval": "hour", + } + + organization = create_organization("Test Org") + team = create_team(organization) + user = create_user("test@user.com", "Test User", organization) + client.force_login(user) + + with start_test_worker(temporal): + batch_export = create_batch_export_ok(client, team.pk, batch_export_data) + batch_export_id = batch_export["id"] + + handle = temporal.get_schedule_handle(batch_export_id) + async_to_sync(handle.delete)() + + with pytest.raises(RPCError): + describe_schedule(temporal, batch_export_id) + + delete_batch_export_ok(client, team.pk, batch_export_id) + + response = get_batch_export(client, team.pk, batch_export_id) + assert response.status_code == status.HTTP_404_NOT_FOUND + + with pytest.raises(RPCError): + describe_schedule(temporal, batch_export_id) diff --git a/posthog/api/test/batch_exports/test_log_entry.py b/posthog/api/test/batch_exports/test_log_entry.py index 23d59b68e924a..8012766464ffd 100644 --- a/posthog/api/test/batch_exports/test_log_entry.py +++ b/posthog/api/test/batch_exports/test_log_entry.py @@ -171,6 +171,54 @@ def test_log_level_filter(batch_export, team, level): assert results[1].batch_export_id == str(batch_export["id"]) +@pytest.mark.django_db +@pytest.mark.parametrize( + "level", + [ + BatchExportLogEntryLevel.INFO, + BatchExportLogEntryLevel.WARNING, + BatchExportLogEntryLevel.ERROR, + BatchExportLogEntryLevel.DEBUG, + ], +) +def test_log_level_filter_with_lowercase(batch_export, team, level): + """Test fetching a batch export log entries of a particular level.""" + with freeze_time("2023-09-22 01:00:00"): + for message in ("Test log 1", "Test log 2"): + create_batch_export_log_entry( + team_id=team.pk, + batch_export_id=str(batch_export["id"]), + run_id=None, + message=message, + level=level.lower(), + ) + + results = [] + timeout = 10 + start = dt.datetime.utcnow() + + while not results: + results = fetch_batch_export_log_entries( + team_id=team.pk, + batch_export_id=batch_export["id"], + level_filter=[level], + after=dt.datetime(2023, 9, 22, 0, 59, 59), + before=dt.datetime(2023, 9, 22, 1, 0, 1), + ) + if (dt.datetime.utcnow() - start) > dt.timedelta(seconds=timeout): + break + + results.sort(key=lambda record: record.message) + + assert len(results) == 2 + assert results[0].message == "Test log 1" + assert results[0].level == level + assert results[0].batch_export_id == str(batch_export["id"]) + assert results[1].message == "Test log 2" + assert results[1].level == level + assert results[1].batch_export_id == str(batch_export["id"]) + + @pytest.mark.django_db def test_batch_export_log_api(client, batch_export, team): """Test fetching batch export log entries using the API.""" diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index 83d7a875677d2..bd8428da8a7c1 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -196,7 +197,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -324,6 +326,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -535,6 +538,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -701,6 +705,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -879,6 +884,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -1046,6 +1052,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1284,7 +1291,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1341,7 +1349,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1489,7 +1498,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1606,7 +1616,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1663,7 +1674,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1809,7 +1821,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1942,7 +1955,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2195,7 +2209,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2435,7 +2450,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -2571,6 +2587,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -2708,7 +2725,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2820,7 +2838,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -2926,7 +2945,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3076,7 +3096,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3173,6 +3194,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -3295,7 +3317,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3411,7 +3434,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3552,7 +3576,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3865,7 +3890,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -4023,7 +4049,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4157,7 +4184,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4249,7 +4277,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4410,7 +4439,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4467,7 +4497,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4583,7 +4614,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4740,7 +4772,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5158,7 +5191,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5299,7 +5333,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5391,7 +5426,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5514,7 +5550,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -5598,7 +5635,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5655,7 +5693,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5771,7 +5810,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5918,7 +5958,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -6081,6 +6122,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -6480,7 +6522,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -6637,6 +6680,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -6818,6 +6862,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -6984,6 +7029,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -7115,7 +7161,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -7212,6 +7259,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -7379,6 +7427,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -8011,7 +8060,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8279,7 +8329,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8440,7 +8491,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8497,7 +8549,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8613,7 +8666,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8770,7 +8824,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8893,7 +8948,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9021,7 +9077,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9168,7 +9225,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9477,7 +9535,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -9624,7 +9683,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9728,7 +9788,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9865,6 +9926,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -9986,7 +10048,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -10125,6 +10188,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -10306,6 +10370,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -10455,7 +10520,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -10566,6 +10632,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -10718,7 +10785,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -10903,7 +10971,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -11014,6 +11083,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -11166,7 +11236,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -11313,6 +11384,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -11540,7 +11612,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ diff --git a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr index fc373eefb7a43..32ff35e826dd3 100644 --- a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr +++ b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%3F%24'*/ @@ -168,7 +169,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -225,7 +227,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -334,7 +337,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-all-activity',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/activity/%3F%24'*/ @@ -546,7 +550,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -657,6 +662,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -763,7 +769,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ diff --git a/posthog/api/test/test_decide.py b/posthog/api/test/test_decide.py index b8bd4e3b331f2..8c670c3243529 100644 --- a/posthog/api/test/test_decide.py +++ b/posthog/api/test/test_decide.py @@ -2843,7 +2843,7 @@ def test_decide_analytics_samples_dont_break_with_zero_sampling(self, *args): self.assertEqual(client.hgetall(f"posthog:decide_requests:{self.team.pk}"), {}) @patch("posthog.models.feature_flag.flag_analytics.CACHE_BUCKET_SIZE", 10) - def test_decide_analytics_fires_with_survey_linked_and_targeting_flags(self, *args): + def test_decide_analytics_only_fires_with_non_survey_targeting_flags(self, *args): ff = FeatureFlag.objects.create( team=self.team, rollout_percentage=50, @@ -2905,7 +2905,7 @@ def test_decide_analytics_fires_with_survey_linked_and_targeting_flags(self, *ar ) @patch("posthog.models.feature_flag.flag_analytics.CACHE_BUCKET_SIZE", 10) - def test_decide_analytics_fire_for_survey_targeting_flags(self, *args): + def test_decide_analytics_does_not_fire_for_survey_targeting_flags(self, *args): FeatureFlag.objects.create( team=self.team, rollout_percentage=50, @@ -2960,10 +2960,7 @@ def test_decide_analytics_fire_for_survey_targeting_flags(self, *args): client = redis.get_client() # check that single increment made it to redis - self.assertEqual( - client.hgetall(f"posthog:decide_requests:{self.team.pk}"), - {b"165192618": b"1"}, - ) + self.assertEqual(client.hgetall(f"posthog:decide_requests:{self.team.pk}"), {}) @patch("posthog.models.feature_flag.flag_analytics.CACHE_BUCKET_SIZE", 10) def test_decide_new_capture_activation(self, *args): @@ -2994,6 +2991,34 @@ def test_decide_new_capture_activation(self, *args): self.assertEqual(response.status_code, 200) self.assertFalse("analytics" in response.json()) + with self.settings(NEW_ANALYTICS_CAPTURE_TEAM_IDS={"0", "*"}, NEW_ANALYTICS_CAPTURE_SAMPLING_RATE=1.0): + response = self._post_decide(api_version=3) + self.assertEqual(response.status_code, 200) + self.assertTrue("analytics" in response.json()) + self.assertEqual(response.json()["analytics"]["endpoint"], "/i/v0/e/") + + with self.settings( + NEW_ANALYTICS_CAPTURE_TEAM_IDS={"*"}, + NEW_ANALYTICS_CAPTURE_EXCLUDED_TEAM_IDS={str(self.team.id)}, + NEW_ANALYTICS_CAPTURE_SAMPLING_RATE=1.0, + ): + response = self._post_decide(api_version=3) + self.assertEqual(response.status_code, 200) + self.assertFalse("analytics" in response.json()) + + def test_decide_element_chain_as_string(self, *args): + self.client.logout() + with self.settings(ELEMENT_CHAIN_AS_STRING_TEAMS={str(self.team.id)}): + response = self._post_decide(api_version=3) + self.assertEqual(response.status_code, 200) + self.assertTrue("elementsChainAsString" in response.json()) + self.assertTrue(response.json()["elementsChainAsString"]) + + with self.settings(ELEMENT_CHAIN_AS_STRING_TEAMS={"0"}): + response = self._post_decide(api_version=3) + self.assertEqual(response.status_code, 200) + self.assertFalse("elementsChainAsString" in response.json()) + class TestDatabaseCheckForDecide(BaseTest, QueryMatchingTest): """ diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py index 0c6581a389561..31f46aabff9a0 100644 --- a/posthog/api/test/test_feature_flag.py +++ b/posthog/api/test/test_feature_flag.py @@ -12,6 +12,7 @@ from freezegun.api import freeze_time from rest_framework import status from posthog import redis +from posthog.api.cohort import get_cohort_actors_for_feature_flag from posthog.api.feature_flag import FeatureFlagSerializer from posthog.constants import AvailableFeature @@ -23,6 +24,7 @@ FeatureFlagDashboards, ) from posthog.models.dashboard import Dashboard +from posthog.models.feature_flag.feature_flag import FeatureFlagHashKeyOverride from posthog.models.group.util import create_group from posthog.models.organization import Organization from posthog.models.person import Person @@ -34,6 +36,7 @@ ClickhouseTestMixin, QueryMatchingTest, _create_person, + flush_persons_and_events, snapshot_clickhouse_queries, snapshot_postgres_queries_context, FuzzyInt, @@ -41,7 +44,7 @@ from posthog.test.db_context_capturing import capture_db_queries -class TestFeatureFlag(APIBaseTest): +class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin): feature_flag: FeatureFlag = None # type: ignore maxDiff = None @@ -1171,6 +1174,34 @@ def test_getting_flags_is_not_nplus1(self) -> None: response = self.client.get(f"/api/projects/{self.team.id}/feature_flags") self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_getting_flags_with_no_creator(self) -> None: + FeatureFlag.objects.all().delete() + + self.client.post( + f"/api/projects/{self.team.id}/feature_flags/", + data={ + "name": f"flag", + "key": f"flag_0", + "filters": {"groups": [{"rollout_percentage": 5}]}, + }, + format="json", + ).json() + + FeatureFlag.objects.create( + created_by=None, + team=self.team, + key="flag_role_access", + name="Flag role access", + ) + + with self.assertNumQueries(FuzzyInt(11, 12)): + response = self.client.get(f"/api/projects/{self.team.id}/feature_flags") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["results"]), 2) + sorted_results = sorted(response.json()["results"], key=lambda x: x["key"]) + self.assertEqual(sorted_results[1]["created_by"], None) + self.assertEqual(sorted_results[1]["key"], "flag_role_access") + @patch("posthog.api.feature_flag.report_user_action") def test_my_flags(self, mock_capture): self.client.post( @@ -3539,6 +3570,511 @@ def test_feature_flag_dashboard_already_exists(self): self.assertEquals(len(response_json["analytics_dashboards"]), 1) + @freeze_time("2021-01-01") + @snapshot_clickhouse_queries + def test_creating_static_cohort(self): + flag = FeatureFlag.objects.create( + team=self.team, + rollout_percentage=100, + filters={ + "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}], + "multivariate": None, + }, + name="some feature", + key="some-feature", + created_by=self.user, + ) + + _create_person( + team=self.team, + distinct_ids=[f"person1"], + properties={"key": "value"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person2"], + properties={"key": "value2"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person3"], + properties={"key2": "value3"}, + ) + flush_persons_and_events() + + with snapshot_postgres_queries_context(self), self.settings( + CELERY_TASK_ALWAYS_EAGER=True, PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False + ): + response = self.client.post( + f"/api/projects/{self.team.id}/feature_flags/{flag.id}/create_static_cohort_for_flag", + {}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # fires an async task for computation, but celery runs sync in tests + cohort_id = response.json()["cohort"]["id"] + cohort = Cohort.objects.get(id=cohort_id) + self.assertEqual(cohort.name, "Users with feature flag some-feature enabled at 2021-01-01 00:00:00") + self.assertEqual(cohort.count, 1) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 1, response) + + +class TestCohortGenerationForFeatureFlag(APIBaseTest, ClickhouseTestMixin): + def test_creating_static_cohort_with_deleted_flag(self): + FeatureFlag.objects.create( + team=self.team, + rollout_percentage=100, + filters={ + "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}], + "multivariate": None, + }, + name="some feature", + key="some-feature", + created_by=self.user, + deleted=True, + ) + + _create_person( + team=self.team, + distinct_ids=[f"person1"], + properties={"key": "value"}, + ) + flush_persons_and_events() + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with self.assertNumQueries(1): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + # don't even try inserting anything, because invalid flag, so None instead of 0 + self.assertEqual(cohort.count, None) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 0, response) + + def test_creating_static_cohort_with_inactive_flag(self): + FeatureFlag.objects.create( + team=self.team, + rollout_percentage=100, + filters={ + "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}], + "multivariate": None, + }, + name="some feature", + key="some-feature2", + created_by=self.user, + active=False, + ) + + _create_person( + team=self.team, + distinct_ids=[f"person1"], + properties={"key": "value"}, + ) + flush_persons_and_events() + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with self.assertNumQueries(1): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + # don't even try inserting anything, because invalid flag, so None instead of 0 + self.assertEqual(cohort.count, None) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 0, response) + + @freeze_time("2021-01-01") + def test_creating_static_cohort_with_group_flag(self): + FeatureFlag.objects.create( + team=self.team, + rollout_percentage=100, + filters={ + "groups": [{"properties": [{"key": "key", "value": "value", "type": "group", "group_type_index": 1}]}], + "multivariate": None, + "aggregation_group_type_index": 1, + }, + name="some feature", + key="some-feature3", + created_by=self.user, + ) + + _create_person( + team=self.team, + distinct_ids=[f"person1"], + properties={"key": "value"}, + ) + flush_persons_and_events() + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with self.assertNumQueries(1): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature3", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + # don't even try inserting anything, because invalid flag, so None instead of 0 + self.assertEqual(cohort.count, None) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 0, response) + + def test_creating_static_cohort_with_no_person_distinct_ids(self): + FeatureFlag.objects.create( + team=self.team, + rollout_percentage=100, + filters={ + "groups": [{"properties": [], "rollout_percentage": 100}], + "multivariate": None, + }, + name="some feature", + key="some-feature2", + created_by=self.user, + ) + + Person.objects.create(team=self.team) + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with self.assertNumQueries(5): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + # don't even try inserting anything, because invalid flag, so None instead of 0 + self.assertEqual(cohort.count, None) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 0, response) + + def test_creating_static_cohort_with_non_existing_flag(self): + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with self.assertNumQueries(1): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + # don't even try inserting anything, because invalid flag, so None instead of 0 + self.assertEqual(cohort.count, None) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 0, response) + + def test_creating_static_cohort_with_experience_continuity_flag(self): + FeatureFlag.objects.create( + team=self.team, + filters={ + "groups": [ + {"properties": [{"key": "key", "value": "value", "type": "person"}], "rollout_percentage": 50} + ], + "multivariate": None, + }, + name="some feature", + key="some-feature2", + created_by=self.user, + ensure_experience_continuity=True, + ) + + p1 = _create_person(team=self.team, distinct_ids=[f"person1"], properties={"key": "value"}, immediate=True) + _create_person( + team=self.team, + distinct_ids=[f"person2"], + properties={"key": "value"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person3"], + properties={"key": "value"}, + ) + flush_persons_and_events() + + FeatureFlagHashKeyOverride.objects.create( + feature_flag_key="some-feature2", + person=p1, + team=self.team, + hash_key="123", + ) + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + # TODO: Ensure server-side cursors are disabled, since in production we use this with pgbouncer + with snapshot_postgres_queries_context(self), self.assertNumQueries(12): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + self.assertEqual(cohort.count, 1) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 1, response) + + def test_creating_static_cohort_iterator(self): + FeatureFlag.objects.create( + team=self.team, + filters={ + "groups": [ + {"properties": [{"key": "key", "value": "value", "type": "person"}], "rollout_percentage": 100} + ], + "multivariate": None, + }, + name="some feature", + key="some-feature2", + created_by=self.user, + ) + + _create_person( + team=self.team, + distinct_ids=[f"person1"], + properties={"key": "value"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person2"], + properties={"key": "value"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person3"], + properties={"key": "value"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person4"], + properties={"key": "valuu3"}, + ) + flush_persons_and_events() + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + # Extra queries because each batch adds its own queries + with snapshot_postgres_queries_context(self), self.assertNumQueries(17): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk, batchsize=2) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + self.assertEqual(cohort.count, 3) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 3, response) + + # if the batch is big enough, it's fewer queries + with self.assertNumQueries(9): + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk, batchsize=10) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + self.assertEqual(cohort.count, 3) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 3, response) + + def test_creating_static_cohort_with_default_person_properties_adjustment(self): + FeatureFlag.objects.create( + team=self.team, + filters={ + "groups": [ + { + "properties": [{"key": "key", "value": "value", "type": "person", "operator": "icontains"}], + "rollout_percentage": 100, + } + ], + "multivariate": None, + }, + name="some feature", + key="some-feature2", + created_by=self.user, + ensure_experience_continuity=False, + ) + FeatureFlag.objects.create( + team=self.team, + filters={ + "groups": [ + { + "properties": [{"key": "key", "value": "value", "type": "person", "operator": "is_set"}], + "rollout_percentage": 100, + } + ], + "multivariate": None, + }, + name="some feature", + key="some-feature-new", + created_by=self.user, + ensure_experience_continuity=False, + ) + + _create_person(team=self.team, distinct_ids=[f"person1"], properties={"key": "value"}) + _create_person( + team=self.team, + distinct_ids=[f"person2"], + properties={"key": "vaalue"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person3"], + properties={"key22": "value"}, + ) + flush_persons_and_events() + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with snapshot_postgres_queries_context(self), self.assertNumQueries(9): + # no queries to evaluate flags, because all evaluated using override properties + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + self.assertEqual(cohort.count, 1) + + response = self.client.get(f"/api/cohort/{cohort.pk}/persons") + self.assertEqual(len(response.json()["results"]), 1, response) + + cohort2 = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort2", + ) + + with snapshot_postgres_queries_context(self), self.assertNumQueries(13): + # need to evaluate flags for person3 using db, because is_set operator can't have defaults added. + get_cohort_actors_for_feature_flag(cohort2.pk, "some-feature-new", self.team.pk) + + cohort2.refresh_from_db() + self.assertEqual(cohort2.name, "some cohort2") + self.assertEqual(cohort2.count, 2) + + def test_creating_static_cohort_with_cohort_flag_adds_cohort_props_as_default_too(self): + cohort_nested = Cohort.objects.create( + team=self.team, + filters={ + "properties": { + "type": "OR", + "values": [ + { + "type": "OR", + "values": [ + {"key": "does-not-exist", "value": "none", "type": "person"}, + ], + } + ], + } + }, + ) + cohort_static = Cohort.objects.create( + team=self.team, + is_static=True, + ) + cohort_existing = Cohort.objects.create( + team=self.team, + filters={ + "properties": { + "type": "OR", + "values": [ + { + "type": "OR", + "values": [ + {"key": "group", "value": "none", "type": "person"}, + {"key": "group2", "value": [1, 2, 3], "type": "person"}, + {"key": "id", "value": cohort_static.pk, "type": "cohort"}, + {"key": "id", "value": cohort_nested.pk, "type": "cohort"}, + ], + } + ], + } + }, + name="cohort1", + ) + FeatureFlag.objects.create( + team=self.team, + filters={ + "groups": [ + { + "properties": [{"key": "id", "value": cohort_existing.pk, "type": "cohort"}], + "rollout_percentage": 100, + }, + {"properties": [{"key": "key", "value": "value", "type": "person"}], "rollout_percentage": 100}, + ], + "multivariate": None, + }, + name="some feature", + key="some-feature-new", + created_by=self.user, + ensure_experience_continuity=False, + ) + + _create_person(team=self.team, distinct_ids=[f"person1"], properties={"key": "value"}) + _create_person( + team=self.team, + distinct_ids=[f"person2"], + properties={"group": "none"}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person3"], + properties={"key22": "value", "group2": 2}, + ) + _create_person( + team=self.team, + distinct_ids=[f"person4"], + properties={}, + ) + flush_persons_and_events() + + cohort_static.insert_users_by_list([f"person4"]) + + cohort = Cohort.objects.create( + team=self.team, + is_static=True, + name="some cohort", + ) + + with snapshot_postgres_queries_context(self), self.assertNumQueries(26): + # forced to evaluate flags by going to db, because cohorts need db query to evaluate + get_cohort_actors_for_feature_flag(cohort.pk, "some-feature-new", self.team.pk) + + cohort.refresh_from_db() + self.assertEqual(cohort.name, "some cohort") + self.assertEqual(cohort.count, 4) + class TestBlastRadius(ClickhouseTestMixin, APIBaseTest): @snapshot_clickhouse_queries diff --git a/posthog/api/test/test_feature_flag_utils.py b/posthog/api/test/test_feature_flag_utils.py new file mode 100644 index 0000000000000..157fe4f5be9c9 --- /dev/null +++ b/posthog/api/test/test_feature_flag_utils.py @@ -0,0 +1,73 @@ +from typing import Dict, Set +from posthog.test.base import ( + APIBaseTest, +) +from posthog.models.cohort import Cohort +from posthog.models.cohort.util import sort_cohorts_topologically + + +class TestFeatureFlagUtils(APIBaseTest): + def setUp(self): + super().setUp() + + def test_cohorts_sorted_topologically(self): + cohorts = {} + + def create_cohort(name): + cohorts[name] = Cohort.objects.create( + team=self.team, + name=name, + filters={ + "properties": { + "type": "AND", + "values": [ + {"key": "name", "value": "test", "type": "person"}, + ], + } + }, + ) + + create_cohort("a") + create_cohort("b") + create_cohort("c") + + # (c)-->(b) + cohorts["c"].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts["b"].pk, + "type": "cohort", + "negation": True, + } + cohorts["c"].save() + + # (a)-->(c) + cohorts["a"].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts["c"].pk, + "type": "cohort", + "negation": True, + } + cohorts["a"].save() + + cohort_ids = {cohorts["a"].pk, cohorts["b"].pk, cohorts["c"].pk} + seen_cohorts_cache = { + cohorts["a"].pk: cohorts["a"], + cohorts["b"].pk: cohorts["b"], + cohorts["c"].pk: cohorts["c"], + } + + # (a)-->(c)-->(b) + # create b first, since it doesn't depend on any other cohorts + # then c, because it depends on b + # then a, because it depends on c + + # thus destination creation order: b, c, a + destination_creation_order = [cohorts["b"].pk, cohorts["c"].pk, cohorts["a"].pk] + topologically_sorted_cohort_ids = sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) + self.assertEqual(topologically_sorted_cohort_ids, destination_creation_order) + + def test_empty_cohorts_set(self): + cohort_ids: Set[int] = set() + seen_cohorts_cache: Dict[int, Cohort] = {} + topologically_sorted_cohort_ids = sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) + self.assertEqual(topologically_sorted_cohort_ids, []) diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py index cd78e5c238f20..78e72269b20bb 100644 --- a/posthog/api/test/test_organization_feature_flag.py +++ b/posthog/api/test/test_organization_feature_flag.py @@ -1,6 +1,8 @@ from rest_framework import status +from posthog.models.cohort.util import sort_cohorts_topologically from posthog.models.user import User from posthog.models.team.team import Team +from posthog.models.cohort import Cohort from ee.models.organization_resource_access import OrganizationResourceAccess from posthog.constants import AvailableFeature from posthog.models import FeatureFlag @@ -51,7 +53,7 @@ def test_get_feature_flag_success(self): "email": self.user.email, "is_email_verified": self.user.is_email_verified, }, - "filters": flag.filters, + "filters": flag.get_filters(), "created_at": flag.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z", "active": flag.active, } @@ -241,6 +243,29 @@ def test_copy_feature_flag_update_existing(self): set(flag_response.keys()), ) + def test_copy_feature_flag_with_old_legacy_flags(self): + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + target_project = self.team_2 + + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key="flag-to-copy-here", + filters={}, + rollout_percentage=self.rollout_percentage_to_copy, + ) + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": self.feature_flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["success"]), 1) + self.assertEqual(len(response.json()["failed"]), 0) + def test_copy_feature_flag_update_override_deleted(self): target_project = self.team_2 target_project_2 = Team.objects.create(organization=self.organization) @@ -428,3 +453,230 @@ def test_copy_feature_flag_cannot_edit(self): } response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_copy_feature_flag_cohort_nonexistent_in_destination(self): + cohorts = {} + creation_order = [] + + def create_cohort(name, children): + creation_order.append(name) + properties = [{"key": "$some_prop", "value": "nomatchihope", "type": "person"}] + if children: + properties = [{"key": "id", "type": "cohort", "value": child.pk} for child in children] + + cohorts[name] = Cohort.objects.create( + team=self.team, + name=str(name), + filters={ + "properties": { + "type": "AND", + "values": properties, + } + }, + ) + + # link cohorts + create_cohort(1, None) + create_cohort(3, None) + create_cohort(2, [cohorts[1]]) + create_cohort(4, [cohorts[2], cohorts[3]]) + create_cohort(5, [cohorts[4]]) + create_cohort(6, None) + create_cohort(7, [cohorts[5], cohorts[6]]) # "head" cohort + + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key="flag-with-cohort", + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": cohorts[7].pk, # link "head" cohort + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + target_project = self.team_2 + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # check all cohorts were created in the destination project + for name in creation_order: + found_cohort = Cohort.objects.filter(name=str(name), team_id=target_project.id).exists() + self.assertTrue(found_cohort) + + def test_copy_feature_flag_cohort_nonexistent_in_destination_2(self): + feature_flag_key = "flag-with-cohort" + cohorts = {} + + def create_cohort(name): + cohorts[name] = Cohort.objects.create( + team=self.team, + name=name, + filters={ + "properties": { + "type": "AND", + "values": [ + {"key": "name", "value": "test", "type": "person"}, + ], + } + }, + ) + + create_cohort("a") + create_cohort("b") + create_cohort("c") + create_cohort("d") + + def connect(parent, child): + cohorts[parent].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts[child].pk, + "type": "cohort", + } + cohorts[parent].save() + + connect("d", "b") + connect("a", "d") + connect("c", "a") + + head_cohort = cohorts["c"] + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key=feature_flag_key, + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": head_cohort.pk, # link "head" cohort + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + target_project = self.team_2 + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # check all cohorts were created in the destination project + for name in cohorts.keys(): + found_cohort = Cohort.objects.filter(name=name, team_id=target_project.id)[0] + self.assertTrue(found_cohort) + + # destination flag contains the head cohort + destination_flag = FeatureFlag.objects.get(key=feature_flag_key, team_id=target_project.id) + destination_flag_head_cohort_id = destination_flag.filters["groups"][0]["properties"][0]["value"] + destination_head_cohort = Cohort.objects.get(pk=destination_flag_head_cohort_id, team_id=target_project.id) + self.assertEqual(destination_head_cohort.name, head_cohort.name) + self.assertNotEqual(destination_head_cohort.id, head_cohort.id) + + # get topological order of the original cohorts + original_cohorts_cache = {} + for _, cohort in cohorts.items(): + original_cohorts_cache[cohort.id] = cohort + original_cohort_ids = {cohort_id for cohort_id in original_cohorts_cache.keys()} + topologically_sorted_original_cohort_ids = sort_cohorts_topologically( + original_cohort_ids, original_cohorts_cache + ) + + # drill down the destination cohorts in the reverse topological order + # the order of names should match the reverse topological order of the original cohort names + topologically_sorted_original_cohort_ids_reversed = topologically_sorted_original_cohort_ids[::-1] + + def traverse(cohort, index): + expected_cohort_id = topologically_sorted_original_cohort_ids_reversed[index] + expected_name = original_cohorts_cache[expected_cohort_id].name + self.assertEqual(expected_name, cohort.name) + + prop = cohort.filters["properties"]["values"][0] + if prop["type"] == "cohort": + next_cohort_id = prop["value"] + next_cohort = Cohort.objects.get(pk=next_cohort_id, team_id=target_project.id) + traverse(next_cohort, index + 1) + + traverse(destination_head_cohort, 0) + + def test_copy_feature_flag_destination_cohort_not_overridden(self): + cohort_name = "cohort-1" + target_project = self.team_2 + original_cohort = Cohort.objects.create( + team=self.team, + name=cohort_name, + groups=[{"properties": [{"key": "$some_prop", "value": "original_value", "type": "person"}]}], + ) + + destination_cohort_prop_value = "destination_value" + Cohort.objects.create( + team=target_project, + name=cohort_name, + groups=[{"properties": [{"key": "$some_prop", "value": destination_cohort_prop_value, "type": "person"}]}], + ) + + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key="flag-with-cohort", + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": original_cohort.pk, + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + destination_cohort = Cohort.objects.filter(name=cohort_name, team=target_project).first() + self.assertTrue(destination_cohort is not None) + # check destination value not overwritten + + if destination_cohort is not None: + self.assertTrue(destination_cohort.groups[0]["properties"][0]["value"] == destination_cohort_prop_value) diff --git a/posthog/api/test/test_query.py b/posthog/api/test/test_query.py index b49cd25b83287..ff03704605014 100644 --- a/posthog/api/test/test_query.py +++ b/posthog/api/test/test_query.py @@ -1,11 +1,11 @@ import json +from unittest import mock from unittest.mock import patch -from urllib.parse import quote from freezegun import freeze_time from rest_framework import status -from posthog.api.query import process_query +from posthog.api.services.query import process_query from posthog.models.property_definition import PropertyDefinition, PropertyType from posthog.models.utils import UUIDT from posthog.schema import ( @@ -336,51 +336,9 @@ def test_person_property_filter(self): response = self.client.post(f"/api/projects/{self.team.id}/query/", {"query": query.dict()}).json() self.assertEqual(len(response["results"]), 2) - def test_json_undefined_constant_error(self): - response = self.client.get( - f"/api/projects/{self.team.id}/query/?query=%7B%22kind%22%3A%22EventsQuery%22%2C%22select%22%3A%5B%22*%22%5D%2C%22limit%22%3AInfinity%7D" - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "type": "validation_error", - "code": "invalid_input", - "detail": "Unsupported constant found in JSON: Infinity", - "attr": None, - }, - ) - - response = self.client.get( - f"/api/projects/{self.team.id}/query/?query=%7B%22kind%22%3A%22EventsQuery%22%2C%22select%22%3A%5B%22*%22%5D%2C%22limit%22%3ANaN%7D" - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "type": "validation_error", - "code": "invalid_input", - "detail": "Unsupported constant found in JSON: NaN", - "attr": None, - }, - ) - def test_safe_clickhouse_error_passed_through(self): query = {"kind": "EventsQuery", "select": ["timestamp + 'string'"]} - # Safe errors are passed through in GET requests - response_get = self.client.get(f"/api/projects/{self.team.id}/query/?query={quote(json.dumps(query))}") - self.assertEqual(response_get.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_get.json(), - self.validation_error_response( - "Illegal types DateTime64(6, 'UTC') and String of arguments of function plus: " - "While processing toTimeZone(timestamp, 'UTC') + 'string'.", - "illegal_type_of_argument", - ), - ) - - # Safe errors are passed through in POST requests too response_post = self.client.post(f"/api/projects/{self.team.id}/query/", {"query": query}) self.assertEqual(response_post.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( @@ -396,11 +354,6 @@ def test_safe_clickhouse_error_passed_through(self): def test_unsafe_clickhouse_error_is_swallowed(self, sqlparse_format_mock): query = {"kind": "EventsQuery", "select": ["timestamp"]} - # Unsafe errors are swallowed in GET requests (in this case we should not expose malformed SQL) - response_get = self.client.get(f"/api/projects/{self.team.id}/query/?query={quote(json.dumps(query))}") - self.assertEqual(response_get.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - - # Unsafe errors are swallowed in POST requests too response_post = self.client.post(f"/api/projects/{self.team.id}/query/", {"query": query}) self.assertEqual(response_post.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -832,3 +785,87 @@ def test_full_hogql_query_values(self): ) self.assertEqual(response.get("results", [])[0][0], 20) + + +class TestQueryRetrieve(APIBaseTest): + def setUp(self): + super().setUp() + self.team_id = self.team.pk + self.valid_query_id = "12345" + self.invalid_query_id = "invalid-query-id" + self.redis_client_mock = mock.Mock() + self.redis_get_patch = mock.patch("posthog.redis.get_client", return_value=self.redis_client_mock) + self.redis_get_patch.start() + + def tearDown(self): + self.redis_get_patch.stop() + + def test_with_valid_query_id(self): + self.redis_client_mock.get.return_value = json.dumps( + { + "id": self.valid_query_id, + "team_id": self.team_id, + "error": False, + "complete": True, + "results": ["result1", "result2"], + } + ).encode() + response = self.client.get(f"/api/projects/{self.team.id}/query/{self.valid_query_id}/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["complete"], True, response.content) + + def test_with_invalid_query_id(self): + self.redis_client_mock.get.return_value = None + response = self.client.get(f"/api/projects/{self.team.id}/query/{self.invalid_query_id}/") + self.assertEqual(response.status_code, 404) + + def test_completed_query(self): + self.redis_client_mock.get.return_value = json.dumps( + { + "id": self.valid_query_id, + "team_id": self.team_id, + "complete": True, + "results": ["result1", "result2"], + } + ).encode() + response = self.client.get(f"/api/projects/{self.team.id}/query/{self.valid_query_id}/") + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["complete"]) + + def test_running_query(self): + self.redis_client_mock.get.return_value = json.dumps( + { + "id": self.valid_query_id, + "team_id": self.team_id, + "complete": False, + } + ).encode() + response = self.client.get(f"/api/projects/{self.team.id}/query/{self.valid_query_id}/") + self.assertEqual(response.status_code, 200) + self.assertFalse(response.json()["complete"]) + + def test_failed_query(self): + self.redis_client_mock.get.return_value = json.dumps( + { + "id": self.valid_query_id, + "team_id": self.team_id, + "error": True, + "error_message": "Query failed", + } + ).encode() + response = self.client.get(f"/api/projects/{self.team.id}/query/{self.valid_query_id}/") + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["error"]) + + def test_destroy(self): + self.redis_client_mock.get.return_value = json.dumps( + { + "id": self.valid_query_id, + "team_id": self.team_id, + "error": True, + "error_message": "Query failed", + } + ).encode() + response = self.client.delete(f"/api/projects/{self.team.id}/query/{self.valid_query_id}/") + self.assertEqual(response.status_code, 204) + self.redis_client_mock.delete.assert_called_once() diff --git a/posthog/api/test/test_search.py b/posthog/api/test/test_search.py index 3324dc18db6f7..36f31c8ec9ef4 100644 --- a/posthog/api/test/test_search.py +++ b/posthog/api/test/test_search.py @@ -5,15 +5,22 @@ from posthog.api.search import process_query from posthog.test.base import APIBaseTest -from posthog.models import Dashboard, FeatureFlag, Team +from posthog.models import Dashboard, FeatureFlag, Team, Insight class TestSearch(APIBaseTest): + insight_1: Insight + dashboard_1: Dashboard + def setUp(self): super().setUp() + Insight.objects.create(team=self.team, derived_name="derived name") + self.insight_1 = Insight.objects.create(team=self.team, name="second insight") + Insight.objects.create(team=self.team, name="third insight") + Dashboard.objects.create(team=self.team, created_by=self.user) - Dashboard.objects.create(name="second dashboard", team=self.team, created_by=self.user) + self.dashboard_1 = Dashboard.objects.create(name="second dashboard", team=self.team, created_by=self.user) Dashboard.objects.create(name="third dashboard", team=self.team, created_by=self.user) FeatureFlag.objects.create(key="a", team=self.team, created_by=self.user) @@ -24,25 +31,67 @@ def test_search(self): response = self.client.get("/api/projects/@current/search?q=sec") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 2) + self.assertEqual(len(response.json()["results"]), 3) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 1) self.assertEqual(response.json()["counts"]["feature_flag"], 1) + self.assertEqual(response.json()["counts"]["insight"], 1) def test_search_without_query(self): response = self.client.get("/api/projects/@current/search") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 6) + self.assertEqual(len(response.json()["results"]), 9) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 3) self.assertEqual(response.json()["counts"]["feature_flag"], 3) + self.assertEqual(response.json()["counts"]["insight"], 3) + + def test_search_filtered_by_entity(self): + response = self.client.get("/api/projects/@current/search?q=sec&entities=insight&entities=dashboard") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()["results"]), 2) + self.assertEqual(response.json()["counts"]["dashboard"], 1) + self.assertEqual(response.json()["counts"]["insight"], 1) + + def test_response_format_and_ids(self): + response = self.client.get("/api/projects/@current/search?q=sec&entities=insight&entities=dashboard") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["results"][0], + { + "rank": response.json()["results"][0]["rank"], + "type": "dashboard", + "result_id": str(self.dashboard_1.id), + "extra_fields": {"description": "", "name": "second dashboard"}, + }, + ) + self.assertEqual( + response.json()["results"][1], + { + "rank": response.json()["results"][1]["rank"], + "type": "insight", + "result_id": self.insight_1.short_id, + "extra_fields": {"name": "second insight", "description": None, "filters": {}, "query": None}, + }, + ) + + def test_extra_fields(self): + response = self.client.get("/api/projects/@current/search?entities=insight") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["results"][0]["extra_fields"], + {"name": None, "description": None, "filters": {}, "query": None}, + ) def test_search_with_fully_invalid_query(self): response = self.client.get("/api/projects/@current/search?q=%3E") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()["results"]), 6) + self.assertEqual(len(response.json()["results"]), 9) self.assertEqual(response.json()["counts"]["action"], 0) self.assertEqual(response.json()["counts"]["dashboard"], 3) self.assertEqual(response.json()["counts"]["feature_flag"], 3) diff --git a/posthog/api/test/test_signup.py b/posthog/api/test/test_signup.py index e106dd6cbddf2..00c101e4487ee 100644 --- a/posthog/api/test/test_signup.py +++ b/posthog/api/test/test_signup.py @@ -3,9 +3,9 @@ from typing import Dict, Optional, cast from unittest import mock from unittest.mock import ANY, patch +from zoneinfo import ZoneInfo import pytest -from zoneinfo import ZoneInfo from django.core import mail from django.urls.base import reverse from django.utils import timezone @@ -543,6 +543,7 @@ def test_social_signup_with_allowed_domain_on_self_hosted( @patch("posthoganalytics.capture") @mock.patch("ee.billing.billing_manager.BillingManager.update_billing_distinct_ids") + @mock.patch("ee.billing.billing_manager.BillingManager.update_billing_customer_email") @mock.patch("social_core.backends.base.BaseAuth.request") @mock.patch("posthog.api.authentication.get_instance_available_sso_providers") @mock.patch("posthog.tasks.user_identify.identify_task") @@ -553,11 +554,13 @@ def test_social_signup_with_allowed_domain_on_cloud( mock_sso_providers, mock_request, mock_update_distinct_ids, + mock_update_billing_customer_email, mock_capture, ): with self.is_cloud(True): self.run_test_for_allowed_domain(mock_sso_providers, mock_request, mock_capture) assert mock_update_distinct_ids.called_once() + assert mock_update_billing_customer_email.called_once() @mock.patch("social_core.backends.base.BaseAuth.request") @mock.patch("posthog.api.authentication.get_instance_available_sso_providers") diff --git a/posthog/api/test/test_survey.py b/posthog/api/test/test_survey.py index 92008ce32657d..75cd3d1c91e5b 100644 --- a/posthog/api/test/test_survey.py +++ b/posthog/api/test/test_survey.py @@ -365,7 +365,7 @@ def test_updating_survey_with_targeting_creates_or_updates_targeting_flag(self): "groups": [{"variant": None, "properties": [], "rollout_percentage": 20}] } - def test_updating_survey_to_remove_targeting_doesnt_delete_targeting_flag(self): + def test_updating_survey_to_send_none_targeting_doesnt_delete_targeting_flag(self): survey_with_targeting = self.client.post( f"/api/projects/{self.team.id}/surveys/", data={ @@ -409,7 +409,7 @@ def test_updating_survey_to_remove_targeting_doesnt_delete_targeting_flag(self): assert FeatureFlag.objects.filter(id=flagId).exists() - def test_updating_survey_to_send_none_targeting_deletes_targeting_flag(self): + def test_updating_survey_to_remove_targeting_deletes_targeting_flag(self): survey_with_targeting = self.client.post( f"/api/projects/{self.team.id}/surveys/", data={ @@ -697,6 +697,58 @@ def test_deleting_survey_deletes_targeting_flag(self): assert deleted_survey.status_code == status.HTTP_204_NO_CONTENT assert not FeatureFlag.objects.filter(id=response.json()["targeting_flag"]["id"]).exists() + def test_inactive_surveys_disables_targeting_flag(self): + survey_with_targeting = self.client.post( + f"/api/projects/{self.team.id}/surveys/", + data={ + "name": "survey with targeting", + "type": "popover", + "targeting_flag_filters": { + "groups": [ + { + "variant": None, + "rollout_percentage": None, + "properties": [ + { + "key": "billing_plan", + "value": ["cloud"], + "operator": "exact", + "type": "person", + } + ], + } + ] + }, + "conditions": {"url": "https://app.posthog.com/notebooks"}, + }, + format="json", + ).json() + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is False + # launch survey + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "start_date": datetime.now() - timedelta(days=1), + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is True + # stop the survey + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "end_date": datetime.now() + timedelta(days=1), + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is False + # resume survey again + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "end_date": None, + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is True + def test_can_list_surveys(self): self.client.post( f"/api/projects/{self.team.id}/surveys/", diff --git a/posthog/api/test/test_team.py b/posthog/api/test/test_team.py index 982abb1def824..3fec54edaba40 100644 --- a/posthog/api/test/test_team.py +++ b/posthog/api/test/test_team.py @@ -1,6 +1,7 @@ import json from typing import List, cast -from unittest.mock import ANY, MagicMock, patch +from unittest import mock +from unittest.mock import MagicMock, call, patch from asgiref.sync import sync_to_async from django.core.cache import cache @@ -219,15 +220,16 @@ def test_delete_team_own_second(self, mock_capture: MagicMock, mock_delete_bulky AsyncDeletion.objects.filter(team_id=team.id, deletion_type=DeletionType.Team, key=str(team.id)).count(), 1, ) - mock_capture.assert_called_once_with( - self.user.distinct_id, - "team deleted", - properties={}, - groups={ - "instance": ANY, - "organization": str(self.organization.id), - "project": str(self.team.uuid), - }, + mock_capture.assert_has_calls( + calls=[ + call( + self.user.distinct_id, + "membership level changed", + properties={"new_level": 8, "previous_level": 1}, + groups=mock.ANY, + ), + call(self.user.distinct_id, "team deleted", properties={}, groups=mock.ANY), + ] ) mock_delete_bulky_postgres_data.assert_called_once_with(team_ids=[team.pk]) diff --git a/posthog/batch_exports/http.py b/posthog/batch_exports/http.py index 8d6005ec663f8..cef17ab628f32 100644 --- a/posthog/batch_exports/http.py +++ b/posthog/batch_exports/http.py @@ -2,6 +2,7 @@ from typing import Any import posthoganalytics +import structlog from django.db import transaction from django.utils.timezone import now from rest_framework import mixins, request, response, serializers, viewsets @@ -27,6 +28,7 @@ BatchExportIdError, BatchExportServiceError, BatchExportServiceRPCError, + BatchExportServiceScheduleNotFound, backfill_export, cancel_running_batch_export_backfill, delete_schedule, @@ -49,6 +51,8 @@ from posthog.temporal.client import sync_connect from posthog.utils import relative_date_parse +logger = structlog.get_logger(__name__) + def validate_date_input(date_input: Any) -> dt.datetime: """Parse any datetime input as a proper dt.datetime. @@ -320,10 +324,22 @@ def unpause(self, request: request.Request, *args, **kwargs) -> response.Respons return response.Response({"paused": False}) def perform_destroy(self, instance: BatchExport): - """Perform a BatchExport destroy by clearing Temporal and Django state.""" - instance.deleted = True + """Perform a BatchExport destroy by clearing Temporal and Django state. + + If the underlying Temporal Schedule doesn't exist, we ignore the error and proceed with the delete anyways. + The Schedule could have been manually deleted causing Django and Temporal to go out of sync. For whatever reason, + since we are deleting, we assume that we can recover from this state by finishing the delete operation by calling + instance.save(). + """ temporal = sync_connect() - delete_schedule(temporal, str(instance.pk)) + + instance.deleted = True + + try: + delete_schedule(temporal, str(instance.pk)) + except BatchExportServiceScheduleNotFound as e: + logger.warning("The Schedule %s could not be deleted as it was not found", e.schedule_id) + instance.save() for backfill in BatchExportBackfill.objects.filter(batch_export=instance): diff --git a/posthog/batch_exports/models.py b/posthog/batch_exports/models.py index 30ad08bc13c86..ab1a7b8b80db0 100644 --- a/posthog/batch_exports/models.py +++ b/posthog/batch_exports/models.py @@ -242,11 +242,11 @@ def fetch_batch_export_log_entries( clickhouse_where_parts.append("message ILIKE %(search)s") clickhouse_kwargs["search"] = f"%{search}%" if len(level_filter) > 0: - clickhouse_where_parts.append("level in %(levels)s") + clickhouse_where_parts.append("upper(level) in %(levels)s") clickhouse_kwargs["levels"] = level_filter clickhouse_query = f""" - SELECT team_id, log_source_id AS batch_export_id, instance_id AS run_id, timestamp, level, message FROM log_entries + SELECT team_id, log_source_id AS batch_export_id, instance_id AS run_id, timestamp, upper(level) as level, message FROM log_entries WHERE {' AND '.join(clickhouse_where_parts)} ORDER BY timestamp DESC {f'LIMIT {limit}' if limit else ''} """ diff --git a/posthog/batch_exports/service.py b/posthog/batch_exports/service.py index fc74d6f51f253..38cecda263aaa 100644 --- a/posthog/batch_exports/service.py +++ b/posthog/batch_exports/service.py @@ -3,6 +3,7 @@ from dataclasses import asdict, dataclass, fields from uuid import UUID +import temporalio from asgiref.sync import async_to_sync from temporalio.client import ( Client, @@ -163,6 +164,14 @@ class BatchExportServiceRPCError(BatchExportServiceError): """Exception raised when the underlying Temporal RPC fails.""" +class BatchExportServiceScheduleNotFound(BatchExportServiceRPCError): + """Exception raised when the underlying Temporal RPC fails because a schedule was not found.""" + + def __init__(self, schedule_id: str): + self.schedule_id = schedule_id + super().__init__(f"The Temporal Schedule {schedule_id} was not found (maybe it was deleted?)") + + def pause_batch_export(temporal: Client, batch_export_id: str, note: str | None = None) -> None: """Pause this BatchExport. @@ -250,7 +259,14 @@ async def unpause_schedule(temporal: Client, schedule_id: str, note: str | None async def delete_schedule(temporal: Client, schedule_id: str) -> None: """Delete a Temporal Schedule.""" handle = temporal.get_schedule_handle(schedule_id) - await handle.delete() + + try: + await handle.delete() + except temporalio.service.RPCError as e: + if e.status == temporalio.service.RPCStatusCode.NOT_FOUND: + raise BatchExportServiceScheduleNotFound(schedule_id) + else: + raise BatchExportServiceRPCError() from e @async_to_sync diff --git a/posthog/caching/calculate_results.py b/posthog/caching/calculate_results.py index be11c4ffe48b5..f7ee632e2ad48 100644 --- a/posthog/caching/calculate_results.py +++ b/posthog/caching/calculate_results.py @@ -141,7 +141,7 @@ def calculate_for_query_based_insight( ) # local import to avoid circular reference - from posthog.api.query import process_query + from posthog.api.services.query import process_query # TODO need to properly check that hogql is enabled? return cache_key, cache_type, process_query(team, insight.query, True) diff --git a/posthog/celery.py b/posthog/celery.py index a7b62848bfab3..53c67214783ee 100644 --- a/posthog/celery.py +++ b/posthog/celery.py @@ -27,7 +27,8 @@ from posthog.cloud_utils import is_cloud from posthog.metrics import pushed_metrics_registry from posthog.redis import get_client -from posthog.utils import get_crontab, get_instance_region +from posthog.utils import get_crontab +from posthog.ph_client import get_ph_client # set the default Django settings module for the 'celery' program. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "posthog.settings") @@ -333,6 +334,13 @@ def setup_periodic_tasks(sender: Celery, **kwargs): name="sync datawarehouse sources that have settled in s3 bucket", ) + # Every 30 minutes try to retrieve and calculate total rows synced in period + sender.add_periodic_task( + crontab(minute="*/30"), + calculate_external_data_rows_synced.s(), + name="calculate external data rows synced", + ) + # Set up clickhouse query instrumentation @task_prerun.connect @@ -387,24 +395,19 @@ def redis_heartbeat(): @app.task(ignore_result=True, bind=True) -def enqueue_clickhouse_execute_with_progress( - self, team_id, query_id, query, args=None, settings=None, with_column_types=False -): +def process_query_task(self, team_id, query_id, query_json, in_export_context=False, refresh_requested=False): """ - Kick off query with progress reporting - Iterate over the progress status - Save status to redis + Kick off query Once complete save results to redis """ - from posthog.client import execute_with_progress - - execute_with_progress( - team_id, - query_id, - query, - args, - settings, - with_column_types, + from posthog.client import execute_process_query + + execute_process_query( + team_id=team_id, + query_id=query_id, + query_json=query_json, + in_export_context=in_export_context, + refresh_requested=refresh_requested, task_id=self.request.id, ) @@ -507,10 +510,10 @@ def pg_row_count(): CLICKHOUSE_TABLES = [ - "events", + "sharded_events", "person", "person_distinct_id2", - "session_replay_events", + "sharded_session_replay_events", "log_entries", ] if not is_cloud(): @@ -532,9 +535,8 @@ def clickhouse_lag(): ) for table in CLICKHOUSE_TABLES: try: - QUERY = ( - """select max(_timestamp) observed_ts, now() now_ts, now() - max(_timestamp) as lag from {table};""" - ) + QUERY = """SELECT max(_timestamp) observed_ts, now() now_ts, now() - max(_timestamp) as lag + FROM {table}""" query = QUERY.format(table=table) lag = sync_execute(query)[0][2] statsd.gauge( @@ -680,9 +682,8 @@ def clickhouse_row_count(): ) for table in CLICKHOUSE_TABLES: try: - QUERY = ( - """select count(1) freq from {table} where _timestamp >= toStartOfDay(date_sub(DAY, 2, now()));""" - ) + QUERY = """SELECT sum(rows) rows from system.parts + WHERE table = '{table}' and active;""" query = QUERY.format(table=table) rows = sync_execute(query)[0][0] row_count_gauge.labels(table_name=table).set(rows) @@ -737,10 +738,11 @@ def clickhouse_part_count(): from posthog.client import sync_execute QUERY = """ - select table, count(1) freq - from system.parts - group by table - order by freq desc; + SELECT table, count(1) freq + FROM system.parts + WHERE active + GROUP BY table + ORDER BY freq DESC; """ rows = sync_execute(QUERY) @@ -903,29 +905,10 @@ def debug_task(self): @app.task(ignore_result=True) def calculate_decide_usage() -> None: from django.db.models import Q - from posthoganalytics import Posthog - from posthog.models import Team from posthog.models.feature_flag.flag_analytics import capture_team_decide_usage - if not is_cloud(): - return - - # send EU data to EU, US data to US - api_key = None - host = None - region = get_instance_region() - if region == "EU": - api_key = "phc_dZ4GK1LRjhB97XozMSkEwPXx7OVANaJEwLErkY1phUF" - host = "https://eu.posthog.com" - elif region == "US": - api_key = "sTMFPsFhdP1Ssg" - host = "https://app.posthog.com" - - if not api_key: - return - - ph_client = Posthog(api_key, host=host) + ph_client = get_ph_client() for team in Team.objects.select_related("organization").exclude( Q(organization__for_internal_metrics=True) | Q(is_demo=True) @@ -935,6 +918,22 @@ def calculate_decide_usage() -> None: ph_client.shutdown() +@app.task(ignore_result=True) +def calculate_external_data_rows_synced() -> None: + from django.db.models import Q + from posthog.models import Team + from posthog.tasks.warehouse import ( + capture_workspace_rows_synced_by_team, + check_external_data_source_billing_limit_by_team, + ) + + for team in Team.objects.select_related("organization").exclude( + Q(organization__for_internal_metrics=True) | Q(is_demo=True) | Q(external_data_workspace_id__isnull=True) + ): + capture_workspace_rows_synced_by_team.delay(team.pk) + check_external_data_source_billing_limit_by_team.delay(team.pk) + + @app.task(ignore_result=True) def find_flags_with_enriched_analytics(): from datetime import datetime, timedelta @@ -1092,7 +1091,7 @@ def ee_persist_finished_recordings(): @app.task(ignore_result=True) def sync_datawarehouse_sources(): try: - from posthog.warehouse.sync_resource import sync_resources + from posthog.tasks.warehouse import sync_resources except ImportError: pass else: diff --git a/posthog/clickhouse/cancel.py b/posthog/clickhouse/cancel.py new file mode 100644 index 0000000000000..e05eea7ad3d64 --- /dev/null +++ b/posthog/clickhouse/cancel.py @@ -0,0 +1,14 @@ +from statshog.defaults.django import statsd + +from posthog.api.services.query import logger +from posthog.clickhouse.client import sync_execute +from posthog.settings import CLICKHOUSE_CLUSTER + + +def cancel_query_on_cluster(team_id: int, client_query_id: str) -> None: + result = sync_execute( + f"KILL QUERY ON CLUSTER '{CLICKHOUSE_CLUSTER}' WHERE query_id LIKE %(client_query_id)s", + {"client_query_id": f"{team_id}_{client_query_id}%"}, + ) + logger.info("Cancelled query %s for team %s, result: %s", client_query_id, team_id, result) + statsd.incr("clickhouse.query.cancellation_requested", tags={"team_id": team_id}) diff --git a/posthog/clickhouse/client/__init__.py b/posthog/clickhouse/client/__init__.py index f2ad255c395e1..a249ebbabb4ad 100644 --- a/posthog/clickhouse/client/__init__.py +++ b/posthog/clickhouse/client/__init__.py @@ -1,8 +1,8 @@ from posthog.clickhouse.client.execute import query_with_columns, sync_execute -from posthog.clickhouse.client.execute_async import execute_with_progress +from posthog.clickhouse.client.execute_async import execute_process_query __all__ = [ "sync_execute", "query_with_columns", - "execute_with_progress", + "execute_process_query", ] diff --git a/posthog/clickhouse/client/execute_async.py b/posthog/clickhouse/client/execute_async.py index 3bb28c3f20075..7e42d52d4836c 100644 --- a/posthog/clickhouse/client/execute_async.py +++ b/posthog/clickhouse/client/execute_async.py @@ -1,172 +1,93 @@ -import hashlib +import datetime import json -import time -from dataclasses import asdict as dataclass_asdict -from dataclasses import dataclass -from time import perf_counter -from typing import Any, Optional - -from posthog import celery -from clickhouse_driver import Client as SyncClient -from django.conf import settings as app_settings -from statshog.defaults.django import statsd - -from posthog import redis -from posthog.celery import enqueue_clickhouse_execute_with_progress -from posthog.clickhouse.client.execute import _prepare_query -from posthog.errors import wrap_query_error -from posthog.settings import ( - CLICKHOUSE_CA, - CLICKHOUSE_DATABASE, - CLICKHOUSE_HOST, - CLICKHOUSE_PASSWORD, - CLICKHOUSE_SECURE, - CLICKHOUSE_USER, - CLICKHOUSE_VERIFY, -) - -REDIS_STATUS_TTL = 600 # 10 minutes - - -@dataclass -class QueryStatus: - team_id: int - num_rows: float = 0 - total_rows: float = 0 - error: bool = False - complete: bool = False - error_message: str = "" - results: Any = None - start_time: Optional[float] = None - end_time: Optional[float] = None - task_id: Optional[str] = None - - -def generate_redis_results_key(query_id): - REDIS_KEY_PREFIX_ASYNC_RESULTS = "query_with_progress" - key = f"{REDIS_KEY_PREFIX_ASYNC_RESULTS}:{query_id}" - return key - - -def execute_with_progress( +import uuid + +import structlog +from rest_framework.exceptions import NotFound + +from posthog import celery, redis +from posthog.celery import process_query_task +from posthog.clickhouse.query_tagging import tag_queries +from posthog.schema import QueryStatus + +logger = structlog.get_logger(__name__) + +REDIS_STATUS_TTL_SECONDS = 600 # 10 minutes +REDIS_KEY_PREFIX_ASYNC_RESULTS = "query_async" + + +class QueryNotFoundError(NotFound): + pass + + +class QueryRetrievalError(Exception): + pass + + +def generate_redis_results_key(query_id: str, team_id: int) -> str: + return f"{REDIS_KEY_PREFIX_ASYNC_RESULTS}:{team_id}:{query_id}" + + +def execute_process_query( team_id, query_id, - query, - args=None, - settings=None, - with_column_types=False, - update_freq=0.2, + query_json, + in_export_context, + refresh_requested, task_id=None, ): - """ - Kick off query with progress reporting - Iterate over the progress status - Save status to redis - Once complete save results to redis - """ - - key = generate_redis_results_key(query_id) - ch_client = SyncClient( - host=CLICKHOUSE_HOST, - database=CLICKHOUSE_DATABASE, - secure=CLICKHOUSE_SECURE, - user=CLICKHOUSE_USER, - password=CLICKHOUSE_PASSWORD, - ca_certs=CLICKHOUSE_CA, - verify=CLICKHOUSE_VERIFY, - settings={"max_result_rows": "10000"}, - ) + key = generate_redis_results_key(query_id, team_id) redis_client = redis.get_client() - start_time = perf_counter() - - prepared_sql, prepared_args, tags = _prepare_query(client=ch_client, query=query, args=args) + from posthog.models import Team + from posthog.api.services.query import process_query - query_status = QueryStatus(team_id, task_id=task_id) + team = Team.objects.get(pk=team_id) - start_time = time.time() + query_status = QueryStatus( + id=query_id, + team_id=team_id, + task_id=task_id, + complete=False, + error=True, # Assume error in case nothing below ends up working + start_time=datetime.datetime.utcnow(), + ) + value = query_status.model_dump_json() try: - progress = ch_client.execute_with_progress( - prepared_sql, - params=prepared_args, - settings=settings, - with_column_types=with_column_types, + tag_queries(client_query_id=query_id, team_id=team_id) + results = process_query( + team=team, query_json=query_json, in_export_context=in_export_context, refresh_requested=refresh_requested ) - for num_rows, total_rows in progress: - query_status = QueryStatus( - team_id=team_id, - num_rows=num_rows, - total_rows=total_rows, - complete=False, - error=False, - error_message="", - results=None, - start_time=start_time, - task_id=task_id, - ) - redis_client.set(key, json.dumps(dataclass_asdict(query_status)), ex=REDIS_STATUS_TTL) - time.sleep(update_freq) - else: - rv = progress.get_result() - query_status = QueryStatus( - team_id=team_id, - num_rows=query_status.num_rows, - total_rows=query_status.total_rows, - complete=True, - error=False, - start_time=query_status.start_time, - end_time=time.time(), - error_message="", - results=rv, - task_id=task_id, - ) - redis_client.set(key, json.dumps(dataclass_asdict(query_status)), ex=REDIS_STATUS_TTL) - + logger.info("Got results for team %s query %s", team_id, query_id) + query_status.complete = True + query_status.error = False + query_status.results = results + query_status.expiration_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=REDIS_STATUS_TTL_SECONDS) + query_status.end_time = datetime.datetime.utcnow() + value = query_status.model_dump_json() except Exception as err: - err = wrap_query_error(err) - tags["failed"] = True - tags["reason"] = type(err).__name__ - statsd.incr("clickhouse_sync_execution_failure") - query_status = QueryStatus( - team_id=team_id, - num_rows=query_status.num_rows, - total_rows=query_status.total_rows, - complete=False, - error=True, - start_time=query_status.start_time, - end_time=time.time(), - error_message=str(err), - results=None, - task_id=task_id, - ) - redis_client.set(key, json.dumps(dataclass_asdict(query_status)), ex=REDIS_STATUS_TTL) - + query_status.results = None # Clear results in case they are faulty + query_status.error_message = str(err) + logger.error("Error processing query for team %s query %s: %s", team_id, query_id, err) + value = query_status.model_dump_json() raise err finally: - ch_client.disconnect() + redis_client.set(key, value, ex=REDIS_STATUS_TTL_SECONDS) - execution_time = perf_counter() - start_time - statsd.timing("clickhouse_sync_execution_time", execution_time * 1000.0) - - if app_settings.SHELL_PLUS_PRINT_SQL: - print("Execution time: %.6fs" % (execution_time,)) # noqa T201 - - -def enqueue_execute_with_progress( +def enqueue_process_query_task( team_id, - query, - args=None, - settings=None, - with_column_types=False, - bypass_celery=False, + query_json, query_id=None, + refresh_requested=False, + bypass_celery=False, force=False, ): if not query_id: - query_id = _query_hash(query, team_id, args) - key = generate_redis_results_key(query_id) + query_id = uuid.uuid4().hex + + key = generate_redis_results_key(query_id, team_id) redis_client = redis.get_client() if force: @@ -187,49 +108,53 @@ def enqueue_execute_with_progress( # If we've seen this query before return the query_id and don't resubmit it. return query_id - # Immediately set status so we don't have race with celery - query_status = QueryStatus(team_id=team_id, start_time=time.time()) - redis_client.set(key, json.dumps(dataclass_asdict(query_status)), ex=REDIS_STATUS_TTL) + # Immediately set status, so we don't have race with celery + query_status = QueryStatus(id=query_id, team_id=team_id) + redis_client.set(key, query_status.model_dump_json(), ex=REDIS_STATUS_TTL_SECONDS) if bypass_celery: # Call directly ( for testing ) - enqueue_clickhouse_execute_with_progress(team_id, query_id, query, args, settings, with_column_types) + process_query_task(team_id, query_id, query_json, in_export_context=True, refresh_requested=refresh_requested) else: - enqueue_clickhouse_execute_with_progress.delay(team_id, query_id, query, args, settings, with_column_types) + task = process_query_task.delay( + team_id, query_id, query_json, in_export_context=True, refresh_requested=refresh_requested + ) + query_status.task_id = task.id + redis_client.set(key, query_status.model_dump_json(), ex=REDIS_STATUS_TTL_SECONDS) return query_id -def get_status_or_results(team_id, query_id): - """ - Returns QueryStatus data class - QueryStatus data class contains either: - Current status of running query - Results of completed query - Error payload of failed query - """ +def get_query_status(team_id, query_id): redis_client = redis.get_client() - key = generate_redis_results_key(query_id) + key = generate_redis_results_key(query_id, team_id) + try: byte_results = redis_client.get(key) - if byte_results: - str_results = byte_results.decode("utf-8") - else: - return QueryStatus(team_id, error=True, error_message="Query is unknown to backend") - query_status = QueryStatus(**json.loads(str_results)) - if query_status.team_id != team_id: - raise Exception("Requesting team is not executing team") except Exception as e: - query_status = QueryStatus(team_id, error=True, error_message=str(e)) - return query_status + raise QueryRetrievalError(f"Error retrieving query {query_id} for team {team_id}") from e + if not byte_results: + raise QueryNotFoundError(f"Query {query_id} not found for team {team_id}") -def _query_hash(query: str, team_id: int, args: Any) -> str: - """ - Takes a query and returns a hex encoded hash of the query and args - """ - if args: - key = hashlib.md5((str(team_id) + query + json.dumps(args)).encode("utf-8")).hexdigest() - else: - key = hashlib.md5((str(team_id) + query).encode("utf-8")).hexdigest() - return key + return QueryStatus(**json.loads(byte_results)) + + +def cancel_query(team_id, query_id): + query_status = get_query_status(team_id, query_id) + + if query_status.task_id: + logger.info("Got task id %s, attempting to revoke", query_status.task_id) + celery.app.control.revoke(query_status.task_id, terminate=True) + + from posthog.clickhouse.cancel import cancel_query_on_cluster + + logger.info("Revoked task id %s, attempting to cancel on cluster", query_status.task_id) + cancel_query_on_cluster(team_id, query_id) + + redis_client = redis.get_client() + key = generate_redis_results_key(query_id, team_id) + logger.info("Deleting redis query key %s", key) + redis_client.delete(key) + + return True diff --git a/posthog/clickhouse/client/test/__snapshots__/test_execute_async.ambr b/posthog/clickhouse/client/test/__snapshots__/test_execute_async.ambr new file mode 100644 index 0000000000000..282191d2015c7 --- /dev/null +++ b/posthog/clickhouse/client/test/__snapshots__/test_execute_async.ambr @@ -0,0 +1,8 @@ +# name: ClickhouseClientTestCase.test_async_query_client + ' + SELECT plus(1, 1) + LIMIT 10000 SETTINGS readonly=2, + max_execution_time=600, + allow_experimental_object_type=1 + ' +--- diff --git a/posthog/clickhouse/client/test/test_execute_async.py b/posthog/clickhouse/client/test/test_execute_async.py new file mode 100644 index 0000000000000..4958c23b3f0a0 --- /dev/null +++ b/posthog/clickhouse/client/test/test_execute_async.py @@ -0,0 +1,153 @@ +import uuid +from unittest.mock import patch + +from django.test import TestCase + +from posthog.clickhouse.client import execute_async as client +from posthog.client import sync_execute +from posthog.hogql.errors import HogQLException +from posthog.models import Organization, Team +from posthog.test.base import ClickhouseTestMixin, snapshot_clickhouse_queries + + +def build_query(sql): + return { + "kind": "HogQLQuery", + "query": sql, + } + + +class ClickhouseClientTestCase(TestCase, ClickhouseTestMixin): + def setUp(self): + self.organization = Organization.objects.create(name="test") + self.team = Team.objects.create(organization=self.organization) + self.team_id = self.team.pk + + @snapshot_clickhouse_queries + def test_async_query_client(self): + query = build_query("SELECT 1+1") + team_id = self.team_id + query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True) + result = client.get_query_status(team_id, query_id) + self.assertFalse(result.error, result.error_message) + self.assertTrue(result.complete) + self.assertEqual(result.results["results"], [[2]]) + + def test_async_query_client_errors(self): + query = build_query("SELECT WOW SUCH DATA FROM NOWHERE THIS WILL CERTAINLY WORK") + self.assertRaises( + HogQLException, + client.enqueue_process_query_task, + **{"team_id": (self.team_id), "query_json": query, "bypass_celery": True}, + ) + query_id = uuid.uuid4().hex + try: + client.enqueue_process_query_task(self.team_id, query, query_id=query_id, bypass_celery=True) + except Exception: + pass + + result = client.get_query_status(self.team_id, query_id) + self.assertTrue(result.error) + self.assertRegex(result.error_message, "Unknown table") + + def test_async_query_client_uuid(self): + query = build_query("SELECT toUUID('00000000-0000-0000-0000-000000000000')") + team_id = self.team_id + query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True) + result = client.get_query_status(team_id, query_id) + self.assertFalse(result.error, result.error_message) + self.assertTrue(result.complete) + self.assertEqual(result.results["results"], [["00000000-0000-0000-0000-000000000000"]]) + + def test_async_query_client_does_not_leak(self): + query = build_query("SELECT 1+1") + team_id = self.team_id + wrong_team = 5 + query_id = client.enqueue_process_query_task(team_id, query, bypass_celery=True) + + try: + client.get_query_status(wrong_team, query_id) + except Exception as e: + self.assertEqual(str(e), f"Query {query_id} not found for team {wrong_team}") + + @patch("posthog.clickhouse.client.execute_async.process_query_task") + def test_async_query_client_is_lazy(self, execute_sync_mock): + query = build_query("SELECT 4 + 4") + query_id = uuid.uuid4().hex + team_id = self.team_id + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Try the same query again + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Try the same query again (for good measure!) + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Assert that we only called clickhouse once + execute_sync_mock.assert_called_once() + + @patch("posthog.clickhouse.client.execute_async.process_query_task") + def test_async_query_client_is_lazy_but_not_too_lazy(self, execute_sync_mock): + query = build_query("SELECT 8 + 8") + query_id = uuid.uuid4().hex + team_id = self.team_id + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Try the same query again, but with force + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True, force=True) + + # Try the same query again (for good measure!) + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Assert that we called clickhouse twice + self.assertEqual(execute_sync_mock.call_count, 2) + + @patch("posthog.clickhouse.client.execute_async.process_query_task") + def test_async_query_client_manual_query_uuid(self, execute_sync_mock): + # This is a unique test because technically in the test pattern `SELECT 8 + 8` is already + # in redis. This tests to make sure it is treated as a unique run of that query + query = build_query("SELECT 8 + 8") + team_id = self.team_id + query_id = "I'm so unique" + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Try the same query again, but with force + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True, force=True) + + # Try the same query again (for good measure!) + client.enqueue_process_query_task(team_id, query, query_id=query_id, bypass_celery=True) + + # Assert that we called clickhouse twice + self.assertEqual(execute_sync_mock.call_count, 2) + + def test_client_strips_comments_from_request(self): + """ + To ensure we can easily copy queries from `system.query_log` in e.g. + Metabase, we strip comments from the query we send. Metabase doesn't + display multilined output. + + See https://github.com/metabase/metabase/issues/14253 + + Note I'm not really testing much complexity, I trust that those will + come out as failures in other tests. + """ + from posthog.clickhouse.query_tagging import tag_queries + + # First add in the request information that should be added to the sql. + # We check this to make sure it is not removed by the comment stripping + with self.capture_select_queries() as sqls: + tag_queries(kind="request", id="1") + sync_execute( + query=""" + -- this request returns 1 + SELECT 1 + """ + ) + self.assertEqual(len(sqls), 1) + first_query = sqls[0] + self.assertIn(f"SELECT 1", first_query) + self.assertNotIn("this request returns", first_query) + + # Make sure it still includes the "annotation" comment that includes + # request routing information for debugging purposes + self.assertIn("/* request:1 */", first_query) diff --git a/posthog/clickhouse/plugin_log_entries.py b/posthog/clickhouse/plugin_log_entries.py index 1f4f7c70d7146..1ac1cb0759ce7 100644 --- a/posthog/clickhouse/plugin_log_entries.py +++ b/posthog/clickhouse/plugin_log_entries.py @@ -25,7 +25,7 @@ PLUGIN_LOG_ENTRIES_TABLE_ENGINE = lambda: ReplacingMergeTree(PLUGIN_LOG_ENTRIES_TABLE, ver="_timestamp") PLUGIN_LOG_ENTRIES_TABLE_SQL = lambda: ( PLUGIN_LOG_ENTRIES_TABLE_BASE_SQL - + """PARTITION BY plugin_id ORDER BY (team_id, id) + + """PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) {ttl_period} SETTINGS index_granularity=512 """ diff --git a/posthog/clickhouse/test/__snapshots__/test_schema.ambr b/posthog/clickhouse/test/__snapshots__/test_schema.ambr index b260abb7ce1d0..cd975ff0f823c 100644 --- a/posthog/clickhouse/test/__snapshots__/test_schema.ambr +++ b/posthog/clickhouse/test/__snapshots__/test_schema.ambr @@ -1367,7 +1367,7 @@ , _offset UInt64 ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY plugin_id ORDER BY (team_id, id) + PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) SETTINGS index_granularity=512 @@ -2166,7 +2166,7 @@ , _offset UInt64 ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY plugin_id ORDER BY (team_id, id) + PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) SETTINGS index_granularity=512 diff --git a/posthog/event_usage.py b/posthog/event_usage.py index fa69f0c23662b..7cd1945d37df4 100644 --- a/posthog/event_usage.py +++ b/posthog/event_usage.py @@ -196,6 +196,26 @@ def report_bulk_invited( ) +def report_user_organization_membership_level_changed( + user: User, + organization: Organization, + new_level: int, + previous_level: int, +) -> None: + """ + Triggered after a user's membership level in an organization is changed. + """ + posthoganalytics.capture( + user.distinct_id, + "membership level changed", + properties={ + "new_level": new_level, + "previous_level": previous_level, + }, + groups=groups(organization), + ) + + def report_user_action(user: User, event: str, properties: Dict = {}): posthoganalytics.capture( user.distinct_id, diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index 59580ccc6d8e7..c45b4ca4caa8b 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -90,7 +90,7 @@ class Database(BaseModel): _timezone: Optional[str] _week_start_day: Optional[WeekStartDay] - def __init__(self, timezone: Optional[str], week_start_day: Optional[WeekStartDay]): + def __init__(self, timezone: Optional[str] = None, week_start_day: Optional[WeekStartDay] = None): super().__init__() try: self._timezone = str(ZoneInfo(timezone)) if timezone else None diff --git a/posthog/hogql/database/schema/persons.py b/posthog/hogql/database/schema/persons.py index 1a1d79123436d..f823f1ce3c9f4 100644 --- a/posthog/hogql/database/schema/persons.py +++ b/posthog/hogql/database/schema/persons.py @@ -53,7 +53,7 @@ def select_from_persons_table(requested_fields: Dict[str, List[str]], modifiers: SELECT id, max(version) as version FROM raw_persons GROUP BY id - HAVING ifNull(equals(argMax(raw_persons.is_deleted, raw_persons.version), 0), 0) + HAVING equals(argMax(raw_persons.is_deleted, raw_persons.version), 0) ) """ ) diff --git a/posthog/hogql/filters.py b/posthog/hogql/filters.py index c900ac1bc5ea6..32ce707d0c647 100644 --- a/posthog/hogql/filters.py +++ b/posthog/hogql/filters.py @@ -63,6 +63,7 @@ def visit_placeholder(self, node): parse_expr( "timestamp < {timestamp}", {"timestamp": ast.Constant(value=parsed_date)}, + start=None, # do not add location information for "timestamp" to the metadata ) ) @@ -77,6 +78,7 @@ def visit_placeholder(self, node): parse_expr( "timestamp >= {timestamp}", {"timestamp": ast.Constant(value=parsed_date)}, + start=None, # do not add location information for "timestamp" to the metadata ) ) diff --git a/posthog/hogql/functions/mapping.py b/posthog/hogql/functions/mapping.py index 8d8fca037f21a..018ddc23b49b4 100644 --- a/posthog/hogql/functions/mapping.py +++ b/posthog/hogql/functions/mapping.py @@ -48,6 +48,21 @@ class HogQLFunctionMeta: """Whether the function is timezone-aware. This means the project timezone will be appended as the last arg.""" +HOGQL_COMPARISON_MAPPING: Dict[str, ast.CompareOperationOp] = { + "equals": ast.CompareOperationOp.Eq, + "notEquals": ast.CompareOperationOp.NotEq, + "less": ast.CompareOperationOp.Lt, + "greater": ast.CompareOperationOp.Gt, + "lessOrEquals": ast.CompareOperationOp.LtEq, + "greaterOrEquals": ast.CompareOperationOp.GtEq, + "like": ast.CompareOperationOp.Like, + "ilike": ast.CompareOperationOp.ILike, + "notLike": ast.CompareOperationOp.NotLike, + "notILike": ast.CompareOperationOp.NotILike, + "in": ast.CompareOperationOp.In, + "notIn": ast.CompareOperationOp.NotIn, +} + HOGQL_CLICKHOUSE_FUNCTIONS: Dict[str, HogQLFunctionMeta] = { # arithmetic "plus": HogQLFunctionMeta("plus", 2, 2), diff --git a/posthog/hogql/modifiers.py b/posthog/hogql/modifiers.py index 0643deefcc6fa..8884f197afcf6 100644 --- a/posthog/hogql/modifiers.py +++ b/posthog/hogql/modifiers.py @@ -1,7 +1,7 @@ from typing import Optional from posthog.models import Team -from posthog.schema import HogQLQueryModifiers +from posthog.schema import HogQLQueryModifiers, MaterializationMode from posthog.utils import PersonOnEventsMode @@ -22,4 +22,7 @@ def create_default_modifiers_for_team( if modifiers.inCohortVia is None: modifiers.inCohortVia = "subquery" + if modifiers.materializationMode is None or modifiers.materializationMode == MaterializationMode.auto: + modifiers.materializationMode = MaterializationMode.legacy_null_as_null + return modifiers diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 3bb0139b4b6f1..f89614d0dc95a 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -29,7 +29,7 @@ escape_hogql_identifier, escape_hogql_string, ) -from posthog.hogql.functions.mapping import ALL_EXPOSED_FUNCTION_NAMES, validate_function_args +from posthog.hogql.functions.mapping import ALL_EXPOSED_FUNCTION_NAMES, validate_function_args, HOGQL_COMPARISON_MAPPING from posthog.hogql.resolver import ResolverException, resolve_types from posthog.hogql.resolver_utils import lookup_field_by_name from posthog.hogql.transforms.in_cohort import resolve_in_cohorts @@ -39,6 +39,7 @@ from posthog.models.property import PropertyName, TableColumn from posthog.models.team.team import WeekStartDay from posthog.models.utils import UUIDT +from posthog.schema import MaterializationMode from posthog.utils import PersonOnEventsMode @@ -556,7 +557,11 @@ def visit_compare_operation(self, node: ast.CompareOperation): return op # Special optimization for "Eq" operator - if node.op == ast.CompareOperationOp.Eq: + if ( + node.op == ast.CompareOperationOp.Eq + or node.op == ast.CompareOperationOp.Like + or node.op == ast.CompareOperationOp.ILike + ): if isinstance(node.right, ast.Constant): if node.right.value is None: return f"isNull({left})" @@ -568,7 +573,11 @@ def visit_compare_operation(self, node: ast.CompareOperation): return f"ifNull({op}, isNull({left}) and isNull({right}))" # Worse case performance, but accurate # Special optimization for "NotEq" operator - if node.op == ast.CompareOperationOp.NotEq: + if ( + node.op == ast.CompareOperationOp.NotEq + or node.op == ast.CompareOperationOp.NotLike + or node.op == ast.CompareOperationOp.NotILike + ): if isinstance(node.right, ast.Constant): if node.right.value is None: return f"isNotNull({left})" @@ -655,7 +664,19 @@ def visit_field(self, node: ast.Field): raise HogQLException(f"Unknown Type, can not print {type(node.type).__name__}") def visit_call(self, node: ast.Call): - if node.name in HOGQL_AGGREGATIONS: + if node.name in HOGQL_COMPARISON_MAPPING: + op = HOGQL_COMPARISON_MAPPING[node.name] + if len(node.args) != 2: + raise HogQLException(f"Comparison '{node.name}' requires exactly two arguments") + # We do "cleverer" logic with nullable types in visit_compare_operation + return self.visit_compare_operation( + ast.CompareOperation( + left=node.args[0], + right=node.args[1], + op=op, + ) + ) + elif node.name in HOGQL_AGGREGATIONS: func_meta = HOGQL_AGGREGATIONS[node.name] validate_function_args( @@ -887,47 +908,51 @@ def visit_property_type(self, type: ast.PropertyType): while isinstance(table, ast.TableAliasType): table = table.table_type - # find a materialized property for the first part of the chain - materialized_property_sql: Optional[str] = None - if isinstance(table, ast.TableType): - if self.dialect == "clickhouse": - table_name = table.table.to_printed_clickhouse(self.context) - else: - table_name = table.table.to_printed_hogql() - if field is None: - raise HogQLException(f"Can't resolve field {field_type.name} on table {table_name}") - field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) - - materialized_column = self._get_materialized_column(table_name, type.chain[0], field_name) - if materialized_column: - property_sql = self._print_identifier(materialized_column) - property_sql = f"{self.visit(field_type.table_type)}.{property_sql}" - materialized_property_sql = property_sql - elif ( - self.context.within_non_hogql_query - and (isinstance(table, ast.SelectQueryAliasType) and table.alias == "events__pdi__person") - or (isinstance(table, ast.VirtualTableType) and table.field == "poe") - ): - # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. - if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: - materialized_column = self._get_materialized_column("events", type.chain[0], "person_properties") - else: - materialized_column = self._get_materialized_column("person", type.chain[0], "properties") - if materialized_column: - materialized_property_sql = self._print_identifier(materialized_column) - args: List[str] = [] - if materialized_property_sql is not None: - # When reading materialized columns, treat the values "" and "null" as NULL-s. - # TODO: rematerialize all columns to support empty strings and "null" string values. - materialized_property_sql = f"nullIf(nullIf({materialized_property_sql}, ''), 'null')" - if len(type.chain) == 1: - return materialized_property_sql - else: - for name in type.chain[1:]: - args.append(self.context.add_value(name)) - return self._unsafe_json_extract_trim_quotes(materialized_property_sql, args) + if self.context.modifiers.materializationMode != "disabled": + # find a materialized property for the first part of the chain + materialized_property_sql: Optional[str] = None + if isinstance(table, ast.TableType): + if self.dialect == "clickhouse": + table_name = table.table.to_printed_clickhouse(self.context) + else: + table_name = table.table.to_printed_hogql() + if field is None: + raise HogQLException(f"Can't resolve field {field_type.name} on table {table_name}") + field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) + + materialized_column = self._get_materialized_column(table_name, type.chain[0], field_name) + if materialized_column: + property_sql = self._print_identifier(materialized_column) + property_sql = f"{self.visit(field_type.table_type)}.{property_sql}" + materialized_property_sql = property_sql + elif ( + self.context.within_non_hogql_query + and (isinstance(table, ast.SelectQueryAliasType) and table.alias == "events__pdi__person") + or (isinstance(table, ast.VirtualTableType) and table.field == "poe") + ): + # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. + if self.context.modifiers.personsOnEventsMode != PersonOnEventsMode.DISABLED: + materialized_column = self._get_materialized_column("events", type.chain[0], "person_properties") + else: + materialized_column = self._get_materialized_column("person", type.chain[0], "properties") + if materialized_column: + materialized_property_sql = self._print_identifier(materialized_column) + + if materialized_property_sql is not None: + # TODO: rematerialize all columns to properly support empty strings and "null" string values. + if self.context.modifiers.materializationMode == MaterializationMode.legacy_null_as_string: + materialized_property_sql = f"nullIf({materialized_property_sql}, '')" + else: # MaterializationMode.auto.legacy_null_as_null + materialized_property_sql = f"nullIf(nullIf({materialized_property_sql}, ''), 'null')" + + if len(type.chain) == 1: + return materialized_property_sql + else: + for name in type.chain[1:]: + args.append(self.context.add_value(name)) + return self._unsafe_json_extract_trim_quotes(materialized_property_sql, args) for name in type.chain: args.append(self.context.add_value(name)) diff --git a/posthog/hogql/property.py b/posthog/hogql/property.py index 9d619c23175b6..5695a0d0be2e5 100644 --- a/posthog/hogql/property.py +++ b/posthog/hogql/property.py @@ -126,26 +126,35 @@ def property_to_expr( elif len(value) == 1: value = value[0] else: - exprs = [ - property_to_expr( - Property( - type=property.type, - key=property.key, - operator=property.operator, - value=v, - ), - team, - scope, + if operator in [PropertyOperator.exact, PropertyOperator.is_not]: + op = ( + ast.CompareOperationOp.In + if operator == PropertyOperator.exact + else ast.CompareOperationOp.NotIn ) - for v in value - ] - if ( - operator == PropertyOperator.is_not - or operator == PropertyOperator.not_icontains - or operator == PropertyOperator.not_regex - ): - return ast.And(exprs=exprs) - return ast.Or(exprs=exprs) + + return ast.CompareOperation( + op=op, + left=ast.Field(chain=["properties", property.key]), + right=ast.Tuple(exprs=[ast.Constant(value=v) for v in value]), + ) + else: + exprs = [ + property_to_expr( + Property( + type=property.type, + key=property.key, + operator=property.operator, + value=v, + ), + team, + scope, + ) + for v in value + ] + if operator == PropertyOperator.not_icontains or operator == PropertyOperator.not_regex: + return ast.And(exprs=exprs) + return ast.Or(exprs=exprs) chain = ["person", "properties"] if property.type == "person" and scope != "person" else ["properties"] field = ast.Field(chain=chain + [property.key]) diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index c7e8c82713b15..751b9fb46b860 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -22,6 +22,8 @@ from posthog.client import sync_execute from posthog.schema import HogQLQueryResponse, HogQLFilters, HogQLQueryModifiers +EXPORT_CONTEXT_MAX_EXECUTION_TIME = 600 + def execute_hogql_query( query: Union[str, ast.SelectQuery, ast.SelectUnionQuery], @@ -119,6 +121,10 @@ def execute_hogql_query( ) ) + settings = settings or HogQLGlobalSettings() + if in_export_context: + settings.max_execution_time = EXPORT_CONTEXT_MAX_EXECUTION_TIME + # Print the ClickHouse SQL query with timings.measure("print_ast"): clickhouse_context = HogQLContext( @@ -131,7 +137,7 @@ def execute_hogql_query( select_query, context=clickhouse_context, dialect="clickhouse", - settings=settings or HogQLGlobalSettings(), + settings=settings, ) timings_dict = timings.to_dict() diff --git a/posthog/hogql/test/__snapshots__/test_resolver.ambr b/posthog/hogql/test/__snapshots__/test_resolver.ambr new file mode 100644 index 0000000000000..78223c03c2b66 --- /dev/null +++ b/posthog/hogql/test/__snapshots__/test_resolver.ambr @@ -0,0 +1,2984 @@ +# name: TestResolver.test_asterisk_expander_from_subquery_table + ' + { + select: [ + { + chain: [ + "uuid" + ] + type: { + name: "uuid" + table_type: { + aliases: {} + anonymous_tables: [] + columns: { + $group_0: { + name: "$group_0" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + }, + $group_1: { + name: "$group_1" + table_type: + }, + $group_2: { + name: "$group_2" + table_type: + }, + $group_3: { + name: "$group_3" + table_type: + }, + $group_4: { + name: "$group_4" + table_type: + }, + $session_id: { + name: "$session_id" + table_type: + }, + created_at: { + name: "created_at" + table_type: + }, + distinct_id: { + name: "distinct_id" + table_type: + }, + elements_chain: { + name: "elements_chain" + table_type: + }, + event: { + name: "event" + table_type: + }, + properties: { + name: "properties" + table_type: + }, + timestamp: { + name: "timestamp" + table_type: + }, + uuid: { + name: "uuid" + table_type: + } + } + ctes: {} + tables: { + events: + } + } + } + }, + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: + } + }, + { + chain: [ + "properties" + ] + type: { + name: "properties" + table_type: + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + }, + { + chain: [ + "distinct_id" + ] + type: { + name: "distinct_id" + table_type: + } + }, + { + chain: [ + "elements_chain" + ] + type: { + name: "elements_chain" + table_type: + } + }, + { + chain: [ + "created_at" + ] + type: { + name: "created_at" + table_type: + } + }, + { + chain: [ + "$session_id" + ] + type: { + name: "$session_id" + table_type: + } + }, + { + chain: [ + "$group_0" + ] + type: { + name: "$group_0" + table_type: + } + }, + { + chain: [ + "$group_1" + ] + type: { + name: "$group_1" + table_type: + } + }, + { + chain: [ + "$group_2" + ] + type: { + name: "$group_2" + table_type: + } + }, + { + chain: [ + "$group_3" + ] + type: { + name: "$group_3" + table_type: + } + }, + { + chain: [ + "$group_4" + ] + type: { + name: "$group_4" + table_type: + } + } + ] + select_from: { + table: { + select: [ + { + chain: [ + "uuid" + ] + type: + }, + { + chain: [ + "event" + ] + type: + }, + { + chain: [ + "properties" + ] + type: + }, + { + chain: [ + "timestamp" + ] + type: + }, + { + chain: [ + "distinct_id" + ] + type: + }, + { + chain: [ + "elements_chain" + ] + type: + }, + { + chain: [ + "created_at" + ] + type: + }, + { + chain: [ + "$session_id" + ] + type: + }, + { + chain: [ + "$group_0" + ] + type: + }, + { + chain: [ + "$group_1" + ] + type: + }, + { + chain: [ + "$group_2" + ] + type: + }, + { + chain: [ + "$group_3" + ] + type: + }, + { + chain: [ + "$group_4" + ] + type: + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [ + + ] + columns: { + $group_0: , + $group_1: , + $group_2: , + $group_3: , + $group_4: , + $session_id: , + created_at: , + distinct_id: , + elements_chain: , + event: , + properties: , + timestamp: , + uuid: + } + ctes: {} + tables: {} + } + } + ' +--- +# name: TestResolver.test_asterisk_expander_select_union + ' + { + select: [ + { + chain: [ + "uuid" + ] + type: { + name: "uuid" + table_type: { + types: [ + { + aliases: {} + anonymous_tables: [] + columns: { + $group_0: { + name: "$group_0" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + }, + $group_1: { + name: "$group_1" + table_type: + }, + $group_2: { + name: "$group_2" + table_type: + }, + $group_3: { + name: "$group_3" + table_type: + }, + $group_4: { + name: "$group_4" + table_type: + }, + $session_id: { + name: "$session_id" + table_type: + }, + created_at: { + name: "created_at" + table_type: + }, + distinct_id: { + name: "distinct_id" + table_type: + }, + elements_chain: { + name: "elements_chain" + table_type: + }, + event: { + name: "event" + table_type: + }, + properties: { + name: "properties" + table_type: + }, + timestamp: { + name: "timestamp" + table_type: + }, + uuid: { + name: "uuid" + table_type: + } + } + ctes: {} + tables: { + events: + } + }, + { + aliases: {} + anonymous_tables: [] + columns: { + $group_0: { + name: "$group_0" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + }, + $group_1: { + name: "$group_1" + table_type: + }, + $group_2: { + name: "$group_2" + table_type: + }, + $group_3: { + name: "$group_3" + table_type: + }, + $group_4: { + name: "$group_4" + table_type: + }, + $session_id: { + name: "$session_id" + table_type: + }, + created_at: { + name: "created_at" + table_type: + }, + distinct_id: { + name: "distinct_id" + table_type: + }, + elements_chain: { + name: "elements_chain" + table_type: + }, + event: { + name: "event" + table_type: + }, + properties: { + name: "properties" + table_type: + }, + timestamp: { + name: "timestamp" + table_type: + }, + uuid: { + name: "uuid" + table_type: + } + } + ctes: {} + tables: { + events: + } + } + ] + } + } + }, + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: + } + }, + { + chain: [ + "properties" + ] + type: { + name: "properties" + table_type: + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + }, + { + chain: [ + "distinct_id" + ] + type: { + name: "distinct_id" + table_type: + } + }, + { + chain: [ + "elements_chain" + ] + type: { + name: "elements_chain" + table_type: + } + }, + { + chain: [ + "created_at" + ] + type: { + name: "created_at" + table_type: + } + }, + { + chain: [ + "$session_id" + ] + type: { + name: "$session_id" + table_type: + } + }, + { + chain: [ + "$group_0" + ] + type: { + name: "$group_0" + table_type: + } + }, + { + chain: [ + "$group_1" + ] + type: { + name: "$group_1" + table_type: + } + }, + { + chain: [ + "$group_2" + ] + type: { + name: "$group_2" + table_type: + } + }, + { + chain: [ + "$group_3" + ] + type: { + name: "$group_3" + table_type: + } + }, + { + chain: [ + "$group_4" + ] + type: { + name: "$group_4" + table_type: + } + } + ] + select_from: { + table: { + select_queries: [ + { + select: [ + { + chain: [ + "uuid" + ] + type: + }, + { + chain: [ + "event" + ] + type: + }, + { + chain: [ + "properties" + ] + type: + }, + { + chain: [ + "timestamp" + ] + type: + }, + { + chain: [ + "distinct_id" + ] + type: + }, + { + chain: [ + "elements_chain" + ] + type: + }, + { + chain: [ + "created_at" + ] + type: + }, + { + chain: [ + "$session_id" + ] + type: + }, + { + chain: [ + "$group_0" + ] + type: + }, + { + chain: [ + "$group_1" + ] + type: + }, + { + chain: [ + "$group_2" + ] + type: + }, + { + chain: [ + "$group_3" + ] + type: + }, + { + chain: [ + "$group_4" + ] + type: + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: + }, + { + select: [ + { + chain: [ + "uuid" + ] + type: + }, + { + chain: [ + "event" + ] + type: + }, + { + chain: [ + "properties" + ] + type: + }, + { + chain: [ + "timestamp" + ] + type: + }, + { + chain: [ + "distinct_id" + ] + type: + }, + { + chain: [ + "elements_chain" + ] + type: + }, + { + chain: [ + "created_at" + ] + type: + }, + { + chain: [ + "$session_id" + ] + type: + }, + { + chain: [ + "$group_0" + ] + type: + }, + { + chain: [ + "$group_1" + ] + type: + }, + { + chain: [ + "$group_2" + ] + type: + }, + { + chain: [ + "$group_3" + ] + type: + }, + { + chain: [ + "$group_4" + ] + type: + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: + } + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [ + + ] + columns: { + $group_0: , + $group_1: , + $group_2: , + $group_3: , + $group_4: , + $session_id: , + created_at: , + distinct_id: , + elements_chain: , + event: , + properties: , + timestamp: , + uuid: + } + ctes: {} + tables: {} + } + } + ' +--- +# name: TestResolver.test_asterisk_expander_subquery + ' + { + select: [ + { + chain: [ + "a" + ] + type: { + name: "a" + table_type: { + aliases: { + a: { + alias: "a" + type: { + data_type: "int" + } + }, + b: { + alias: "b" + type: { + data_type: "int" + } + } + } + anonymous_tables: [] + columns: { + a: , + b: + } + ctes: {} + tables: {} + } + } + }, + { + chain: [ + "b" + ] + type: { + name: "b" + table_type: + } + } + ] + select_from: { + table: { + select: [ + { + alias: "a" + expr: { + type: + value: 1 + } + type: + }, + { + alias: "b" + expr: { + type: + value: 2 + } + type: + } + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [ + + ] + columns: { + a: , + b: + } + ctes: {} + tables: {} + } + } + ' +--- +# name: TestResolver.test_asterisk_expander_subquery_alias + ' + { + select: [ + { + chain: [ + "a" + ] + type: { + name: "a" + table_type: { + alias: "x" + select_query_type: { + aliases: { + a: { + alias: "a" + type: { + data_type: "int" + } + }, + b: { + alias: "b" + type: { + data_type: "int" + } + } + } + anonymous_tables: [] + columns: { + a: , + b: + } + ctes: {} + tables: {} + } + } + } + }, + { + chain: [ + "b" + ] + type: { + name: "b" + table_type: + } + } + ] + select_from: { + alias: "x" + table: { + select: [ + { + alias: "a" + expr: { + type: + value: 1 + } + type: + }, + { + alias: "b" + expr: { + type: + value: 2 + } + type: + } + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + a: , + b: + } + ctes: {} + tables: { + x: + } + } + } + ' +--- +# name: TestResolver.test_asterisk_expander_table + ' + { + select: [ + { + chain: [ + "uuid" + ] + type: { + name: "uuid" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: + } + }, + { + chain: [ + "properties" + ] + type: { + name: "properties" + table_type: + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + }, + { + chain: [ + "distinct_id" + ] + type: { + name: "distinct_id" + table_type: + } + }, + { + chain: [ + "elements_chain" + ] + type: { + name: "elements_chain" + table_type: + } + }, + { + chain: [ + "created_at" + ] + type: { + name: "created_at" + table_type: + } + }, + { + chain: [ + "$session_id" + ] + type: { + name: "$session_id" + table_type: + } + }, + { + chain: [ + "$group_0" + ] + type: { + name: "$group_0" + table_type: + } + }, + { + chain: [ + "$group_1" + ] + type: { + name: "$group_1" + table_type: + } + }, + { + chain: [ + "$group_2" + ] + type: { + name: "$group_2" + table_type: + } + }, + { + chain: [ + "$group_3" + ] + type: { + name: "$group_3" + table_type: + } + }, + { + chain: [ + "$group_4" + ] + type: { + name: "$group_4" + table_type: + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + $group_0: , + $group_1: , + $group_2: , + $group_3: , + $group_4: , + $session_id: , + created_at: , + distinct_id: , + elements_chain: , + event: , + properties: , + timestamp: , + uuid: + } + ctes: {} + tables: { + events: + } + } + } + ' +--- +# name: TestResolver.test_asterisk_expander_table_alias + ' + { + select: [ + { + chain: [ + "uuid" + ] + type: { + name: "uuid" + table_type: { + alias: "e" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + }, + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: + } + }, + { + chain: [ + "properties" + ] + type: { + name: "properties" + table_type: + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + }, + { + chain: [ + "distinct_id" + ] + type: { + name: "distinct_id" + table_type: + } + }, + { + chain: [ + "elements_chain" + ] + type: { + name: "elements_chain" + table_type: + } + }, + { + chain: [ + "created_at" + ] + type: { + name: "created_at" + table_type: + } + }, + { + chain: [ + "$session_id" + ] + type: { + name: "$session_id" + table_type: + } + }, + { + chain: [ + "$group_0" + ] + type: { + name: "$group_0" + table_type: + } + }, + { + chain: [ + "$group_1" + ] + type: { + name: "$group_1" + table_type: + } + }, + { + chain: [ + "$group_2" + ] + type: { + name: "$group_2" + table_type: + } + }, + { + chain: [ + "$group_3" + ] + type: { + name: "$group_3" + table_type: + } + }, + { + chain: [ + "$group_4" + ] + type: { + name: "$group_4" + table_type: + } + } + ] + select_from: { + alias: "e" + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + $group_0: , + $group_1: , + $group_2: , + $group_3: , + $group_4: , + $session_id: , + created_at: , + distinct_id: , + elements_chain: , + event: , + properties: , + timestamp: , + uuid: + } + ctes: {} + tables: { + e: + } + } + } + ' +--- +# name: TestResolver.test_call_type + ' + { + select: [ + { + args: [ + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + ] + distinct: False + name: "max" + type: { + arg_types: [ + { + data_type: "datetime" + } + ] + name: "max" + return_type: { + data_type: "unknown" + } + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: {} + ctes: {} + tables: { + events: + } + } + } + ' +--- +# name: TestResolver.test_resolve_boolean_operation_types + ' + { + select: [ + { + exprs: [ + { + type: { + data_type: "int" + } + value: 1 + }, + { + type: { + data_type: "int" + } + value: 1 + } + ] + type: { + data_type: "bool" + } + }, + { + exprs: [ + { + type: { + data_type: "int" + } + value: 1 + }, + { + type: { + data_type: "int" + } + value: 1 + } + ] + type: { + data_type: "bool" + } + }, + { + expr: { + type: { + data_type: "bool" + } + value: True + } + type: { + data_type: "bool" + } + } + ] + type: { + aliases: {} + anonymous_tables: [] + columns: {} + ctes: {} + tables: {} + } + } + ' +--- +# name: TestResolver.test_resolve_constant_type + ' + { + select: [ + { + type: { + data_type: "int" + } + value: 1 + }, + { + type: { + data_type: "str" + } + value: "boo" + }, + { + type: { + data_type: "bool" + } + value: True + }, + { + type: { + data_type: "float" + } + value: 1.1232 + }, + { + type: { + data_type: "unknown" + } + }, + { + type: { + data_type: "date" + } + value: 2020-01-10 + }, + { + type: { + data_type: "datetime" + } + value: 2020-01-10 00:00:00+00:00 + }, + { + type: { + data_type: "uuid" + } + value: 00000000-0000-4000-8000-000000000000 + }, + { + type: { + data_type: "array" + item_type: { + data_type: "unknown" + } + } + value: [] + }, + { + type: { + data_type: "array" + item_type: { + data_type: "int" + } + } + value: [ + 1, + 2 + ] + }, + { + type: { + data_type: "tuple" + item_types: [ + { + data_type: "int" + }, + { + data_type: "int" + }, + { + data_type: "int" + } + ] + } + value: (1, 2, 3) + } + ] + type: { + aliases: {} + anonymous_tables: [] + columns: {} + ctes: {} + tables: {} + } + } + ' +--- +# name: TestResolver.test_resolve_events_table + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "events", + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + timestamp: + } + ctes: {} + tables: { + events: + } + } + where: { + left: { + chain: [ + "events", + "event" + ] + type: { + name: "event" + table_type: + } + } + op: "==" + right: { + type: { + data_type: "str" + } + value: "test" + } + type: { + data_type: "bool" + } + } + } + ' +--- +# name: TestResolver.test_resolve_events_table_alias + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + alias: "e" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + }, + { + chain: [ + "e", + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + } + ] + select_from: { + alias: "e" + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + timestamp: + } + ctes: {} + tables: { + e: + } + } + where: { + left: { + chain: [ + "e", + "event" + ] + type: { + name: "event" + table_type: + } + } + op: "==" + right: { + type: { + data_type: "str" + } + value: "test" + } + type: { + data_type: "bool" + } + } + } + ' +--- +# name: TestResolver.test_resolve_events_table_column_alias + ' + { + select: [ + { + alias: "ee" + expr: { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + alias: "e" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + } + type: { + alias: "ee" + type: + } + }, + { + chain: [ + "ee" + ] + type: + }, + { + alias: "e" + expr: { + chain: [ + "ee" + ] + type: + } + type: { + alias: "e" + type: + } + }, + { + chain: [ + "e", + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + } + ] + select_from: { + alias: "e" + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: { + e: , + ee: + } + anonymous_tables: [] + columns: { + e: , + ee: , + timestamp: + } + ctes: {} + tables: { + e: + } + } + where: { + left: { + chain: [ + "e", + "event" + ] + type: { + name: "event" + table_type: + } + } + op: "==" + right: { + type: { + data_type: "str" + } + value: "test" + } + type: { + data_type: "bool" + } + } + } + ' +--- +# name: TestResolver.test_resolve_events_table_column_alias_inside_subquery + ' + { + select: [ + { + chain: [ + "b" + ] + type: { + name: "b" + table_type: { + alias: "e" + select_query_type: { + aliases: { + b: { + alias: "b" + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + c: { + alias: "c" + type: { + name: "timestamp" + table_type: + } + } + } + anonymous_tables: [] + columns: { + b: , + c: + } + ctes: {} + tables: { + events: + } + } + } + } + } + ] + select_from: { + alias: "e" + table: { + select: [ + { + alias: "b" + expr: { + chain: [ + "event" + ] + type: + } + type: + }, + { + alias: "c" + expr: { + chain: [ + "timestamp" + ] + type: + } + type: + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + b: + } + ctes: {} + tables: { + e: + } + } + where: { + left: { + chain: [ + "e", + "b" + ] + type: { + name: "b" + table_type: + } + } + op: "==" + right: { + type: { + data_type: "str" + } + value: "test" + } + type: { + data_type: "bool" + } + } + } + ' +--- +# name: TestResolver.test_resolve_lazy_events_pdi_person_table + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "pdi", + "person", + "id" + ] + type: { + name: "id" + table_type: { + field: "person" + lazy_join: { + from_field: "person_id", + join_function: , + join_table: { + fields: { + created_at: {}, + id: {}, + is_identified: {}, + pdi: {}, + properties: {}, + team_id: {} + } + } + } + table_type: { + field: "pdi" + lazy_join: { + from_field: "distinct_id", + join_function: , + join_table: { + fields: { + distinct_id: {}, + person: {}, + person_id: {}, + team_id: {} + } + } + } + table_type: + } + } + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + id: + } + ctes: {} + tables: { + events: + } + } + } + ' +--- +# name: TestResolver.test_resolve_lazy_events_pdi_person_table_aliased + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + alias: "e" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + }, + { + chain: [ + "e", + "pdi", + "person", + "id" + ] + type: { + name: "id" + table_type: { + field: "person" + lazy_join: { + from_field: "person_id", + join_function: , + join_table: { + fields: { + created_at: {}, + id: {}, + is_identified: {}, + pdi: {}, + properties: {}, + team_id: {} + } + } + } + table_type: { + field: "pdi" + lazy_join: { + from_field: "distinct_id", + join_function: , + join_table: { + fields: { + distinct_id: {}, + person: {}, + person_id: {}, + team_id: {} + } + } + } + table_type: + } + } + } + } + ] + select_from: { + alias: "e" + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + id: + } + ctes: {} + tables: { + e: + } + } + } + ' +--- +# name: TestResolver.test_resolve_lazy_events_pdi_table + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "pdi", + "person_id" + ] + type: { + name: "person_id" + table_type: { + field: "pdi" + lazy_join: { + from_field: "distinct_id", + join_function: , + join_table: { + fields: { + distinct_id: {}, + person: {}, + person_id: {}, + team_id: {} + } + } + } + table_type: + } + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + person_id: + } + ctes: {} + tables: { + events: + } + } + } + ' +--- +# name: TestResolver.test_resolve_lazy_events_pdi_table_aliased + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + alias: "e" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + } + }, + { + chain: [ + "e", + "pdi", + "person_id" + ] + type: { + name: "person_id" + table_type: { + field: "pdi" + lazy_join: { + from_field: "distinct_id", + join_function: , + join_table: { + fields: { + distinct_id: {}, + person: {}, + person_id: {}, + team_id: {} + } + } + } + table_type: + } + } + } + ] + select_from: { + alias: "e" + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + person_id: + } + ctes: {} + tables: { + e: + } + } + } + ' +--- +# name: TestResolver.test_resolve_lazy_pdi_person_table + ' + { + select: [ + { + chain: [ + "distinct_id" + ] + type: { + name: "distinct_id" + table_type: { + table: { + fields: { + distinct_id: {}, + person: {}, + person_id: {}, + team_id: {} + } + } + } + } + }, + { + chain: [ + "person", + "id" + ] + type: { + name: "id" + table_type: { + field: "person" + lazy_join: { + from_field: "person_id", + join_function: , + join_table: { + fields: { + created_at: {}, + id: {}, + is_identified: {}, + pdi: {}, + properties: {}, + team_id: {} + } + } + } + table_type: + } + } + } + ] + select_from: { + table: { + chain: [ + "person_distinct_ids" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + distinct_id: , + id: + } + ctes: {} + tables: { + person_distinct_ids: + } + } + } + ' +--- +# name: TestResolver.test_resolve_union_all + ' + { + select_queries: [ + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + timestamp: + } + ctes: {} + tables: { + events: + } + } + }, + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "timestamp" + ] + type: { + name: "timestamp" + table_type: + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + timestamp: + } + ctes: {} + tables: { + events: + } + } + } + ] + type: { + types: [ + , + + ] + } + } + ' +--- +# name: TestResolver.test_resolve_virtual_events_poe + ' + { + select: [ + { + chain: [ + "event" + ] + type: { + name: "event" + table_type: { + table: { + fields: { + $group_0: {}, + $group_1: {}, + $group_2: {}, + $group_3: {}, + $group_4: {}, + $session_id: {}, + created_at: {}, + distinct_id: {}, + elements_chain: {}, + event: {}, + goe_0: {}, + goe_1: {}, + goe_2: {}, + goe_3: {}, + goe_4: {}, + group_0: {}, + group_1: {}, + group_2: {}, + group_3: {}, + group_4: {}, + override: {}, + override_person_id: {}, + pdi: {}, + person: {}, + person_id: {}, + poe: {}, + properties: {}, + session: {}, + team_id: {}, + timestamp: {}, + uuid: {} + } + } + } + } + }, + { + chain: [ + "poe", + "id" + ] + type: { + name: "id" + table_type: { + field: "poe" + table_type: + virtual_table: { + fields: { + created_at: {}, + id: {}, + properties: {} + } + } + } + } + } + ] + select_from: { + table: { + chain: [ + "events" + ] + type: + } + type: + } + type: { + aliases: {} + anonymous_tables: [] + columns: { + event: , + id: + } + ctes: {} + tables: { + events: + } + } + } + ' +--- diff --git a/posthog/hogql/test/test_modifiers.py b/posthog/hogql/test/test_modifiers.py index ba5ed58e84882..4296213727f37 100644 --- a/posthog/hogql/test/test_modifiers.py +++ b/posthog/hogql/test/test_modifiers.py @@ -1,7 +1,7 @@ from posthog.hogql.modifiers import create_default_modifiers_for_team from posthog.hogql.query import execute_hogql_query from posthog.models import Cohort -from posthog.schema import HogQLQueryModifiers, PersonsOnEventsMode +from posthog.schema import HogQLQueryModifiers, PersonsOnEventsMode, MaterializationMode from posthog.test.base import BaseTest from django.test import override_settings @@ -144,3 +144,43 @@ def test_modifiers_in_cohort_join(self): modifiers=HogQLQueryModifiers(inCohortVia="leftjoin"), ) assert "LEFT JOIN" in response.clickhouse + + def test_modifiers_materialization_mode(self): + try: + from ee.clickhouse.materialized_columns.analyze import materialize + except ModuleNotFoundError: + # EE not available? Assume we're good + self.assertEqual(1 + 2, 3) + return + materialize("events", "$browser") + + response = execute_hogql_query( + "SELECT properties.$browser FROM events", + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.auto), + ) + assert "SELECT nullIf(nullIf(events.`mat_$browser`, ''), 'null') FROM events" in response.clickhouse + + response = execute_hogql_query( + "SELECT properties.$browser FROM events", + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_null), + ) + assert "SELECT nullIf(nullIf(events.`mat_$browser`, ''), 'null') FROM events" in response.clickhouse + + response = execute_hogql_query( + "SELECT properties.$browser FROM events", + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_string), + ) + assert "SELECT nullIf(events.`mat_$browser`, '') FROM events" in response.clickhouse + + response = execute_hogql_query( + "SELECT properties.$browser FROM events", + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.disabled), + ) + assert ( + "SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, %(hogql_val_0)s), ''), 'null'), '^\"|\"$', '') FROM events" + in response.clickhouse + ) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 75f182a618f54..53f31749c85d2 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -7,7 +7,7 @@ from posthog.hogql.constants import HogQLQuerySettings, HogQLGlobalSettings from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import Database -from posthog.hogql.database.models import DateDatabaseField +from posthog.hogql.database.models import DateDatabaseField, StringDatabaseField from posthog.hogql.errors import HogQLException from posthog.hogql.hogql import translate_hogql from posthog.hogql.parser import parse_select @@ -964,10 +964,36 @@ def test_functions_expecting_datetime_arg(self): ) def test_field_nullable_equals(self): - generated_sql_statements = self._select( - "SELECT min_first_timestamp = toStartOfMonth(now()), now() = now(), 1 = now(), now() = 1, 1 = 1, click_count = 1, 1 = click_count, click_count = keypress_count, click_count = null, null = click_count FROM session_replay_events" + generated_sql_statements1 = self._select( + "SELECT " + "min_first_timestamp = toStartOfMonth(now()), " + "now() = now(), " + "1 = now(), " + "now() = 1, " + "1 = 1, " + "click_count = 1, " + "1 = click_count, " + "click_count = keypress_count, " + "click_count = null, " + "null = click_count " + "FROM session_replay_events" + ) + generated_sql_statements2 = self._select( + "SELECT " + "equals(min_first_timestamp, toStartOfMonth(now())), " + "equals(now(), now()), " + "equals(1, now()), " + "equals(now(), 1), " + "equals(1, 1), " + "equals(click_count, 1), " + "equals(1, click_count), " + "equals(click_count, keypress_count), " + "equals(click_count, null), " + "equals(null, click_count) " + "FROM session_replay_events" ) - assert generated_sql_statements == ( + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( f"SELECT " # min_first_timestamp = toStartOfMonth(now()) # (the return of toStartOfMonth() is treated as "potentially nullable" since we yet have full typing support) @@ -996,12 +1022,18 @@ def test_field_nullable_equals(self): ) def test_field_nullable_not_equals(self): - generated_sql = self._select( + generated_sql1 = self._select( "SELECT min_first_timestamp != toStartOfMonth(now()), now() != now(), 1 != now(), now() != 1, 1 != 1, " "click_count != 1, 1 != click_count, click_count != keypress_count, click_count != null, null != click_count " "FROM session_replay_events" ) - assert generated_sql == ( + generated_sql2 = self._select( + "SELECT notEquals(min_first_timestamp, toStartOfMonth(now())), notEquals(now(), now()), notEquals(1, now()), notEquals(now(), 1), notEquals(1, 1), " + "notEquals(click_count, 1), notEquals(1, click_count), notEquals(click_count, keypress_count), notEquals(click_count, null), notEquals(null, click_count) " + "FROM session_replay_events" + ) + assert generated_sql1 == generated_sql2 + assert generated_sql1 == ( f"SELECT " # min_first_timestamp = toStartOfMonth(now()) # (the return of toStartOfMonth() is treated as "potentially nullable" since we yet have full typing support) @@ -1029,6 +1061,98 @@ def test_field_nullable_not_equals(self): f"FROM (SELECT session_replay_events.min_first_timestamp AS min_first_timestamp, sum(session_replay_events.click_count) AS click_count, sum(session_replay_events.keypress_count) AS keypress_count FROM session_replay_events WHERE equals(session_replay_events.team_id, {self.team.pk}) GROUP BY session_replay_events.min_first_timestamp) AS session_replay_events LIMIT 10000" ) + def test_field_nullable_like(self): + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements1 = self._select( + "SELECT " + "nullable_field like 'a', " + "nullable_field like null, " + "null like nullable_field, " + "null like 'a', " + "'a' like nullable_field, " + "'a' like null " + "FROM events", + context, + ) + + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements2 = self._select( + "SELECT " + "like(nullable_field, 'a'), " + "like(nullable_field, null), " + "like(null, nullable_field), " + "like(null, 'a'), " + "like('a', nullable_field), " + "like('a', null) " + "FROM events", + context, + ) + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( + f"SELECT " + # event like 'a', + "ifNull(like(events.nullable_field, %(hogql_val_0)s), 0), " + # event like null, + "isNull(events.nullable_field), " + # null like event, + "isNull(events.nullable_field), " + # null like 'a', + "ifNull(like(NULL, %(hogql_val_1)s), 0), " + # 'a' like event, + "ifNull(like(%(hogql_val_2)s, events.nullable_field), 0), " + # 'a' like null + "isNull(%(hogql_val_3)s) " + f"FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT 10000" + ) + + def test_field_nullable_not_like(self): + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements1 = self._select( + "SELECT " + "nullable_field not like 'a', " + "nullable_field not like null, " + "null not like nullable_field, " + "null not like 'a', " + "'a' not like nullable_field, " + "'a' not like null " + "FROM events", + context, + ) + + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements2 = self._select( + "SELECT " + "notLike(nullable_field, 'a'), " + "notLike(nullable_field, null), " + "notLike(null, nullable_field), " + "notLike(null, 'a'), " + "notLike('a', nullable_field), " + "notLike('a', null) " + "FROM events", + context, + ) + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( + f"SELECT " + # event like 'a', + "ifNull(notLike(events.nullable_field, %(hogql_val_0)s), 1), " + # event like null, + "isNotNull(events.nullable_field), " + # null like event, + "isNotNull(events.nullable_field), " + # null like 'a', + "ifNull(notLike(NULL, %(hogql_val_1)s), 1), " + # 'a' like event, + "ifNull(notLike(%(hogql_val_2)s, events.nullable_field), 1), " + # 'a' like null + "isNotNull(%(hogql_val_3)s) " + f"FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT 10000" + ) + def test_print_global_settings(self): query = parse_select("SELECT 1 FROM events") printed = print_ast( diff --git a/posthog/hogql/test/test_property.py b/posthog/hogql/test/test_property.py index c0ed528ea4da9..ecdfecee28671 100644 --- a/posthog/hogql/test/test_property.py +++ b/posthog/hogql/test/test_property.py @@ -163,7 +163,7 @@ def test_property_to_expr_event_list(self): # positive self.assertEqual( self._property_to_expr({"type": "event", "key": "a", "value": ["b", "c"], "operator": "exact"}), - self._parse_expr("properties.a = 'b' or properties.a = 'c'"), + self._parse_expr("properties.a IN ('b', 'c')"), ) self.assertEqual( self._property_to_expr( @@ -183,7 +183,7 @@ def test_property_to_expr_event_list(self): # negative self.assertEqual( self._property_to_expr({"type": "event", "key": "a", "value": ["b", "c"], "operator": "is_not"}), - self._parse_expr("properties.a != 'b' and properties.a != 'c'"), + self._parse_expr("properties.a NOT IN ('b', 'c')"), ) self.assertEqual( self._property_to_expr( diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index f2ee1d812ea65..069e633e0a457 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -1,6 +1,6 @@ from datetime import timezone, datetime, date from typing import Optional, Dict, cast - +import pytest from django.test import override_settings from uuid import UUID @@ -10,12 +10,12 @@ from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import create_hogql_database from posthog.hogql.database.models import ( - LazyJoin, FieldTraverser, StringJSONDatabaseField, StringDatabaseField, DateTimeDatabaseField, ) +from posthog.hogql.test.utils import pretty_dataclasses from posthog.hogql.visitor import clone_expr from posthog.hogql.parser import parse_select from posthog.hogql.printer import print_ast, print_prepared_ast @@ -44,42 +44,11 @@ def setUp(self): self.database = create_hogql_database(self.team.pk) self.context = HogQLContext(database=self.database, team_id=self.team.pk) + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_events_table(self): expr = self._select("SELECT event, events.timestamp FROM events WHERE events.event = 'test'") expr = resolve_types(expr, self.context) - - events_table_type = ast.TableType(table=self.database.events) - event_field_type = ast.FieldType(name="event", table_type=events_table_type) - timestamp_field_type = ast.FieldType(name="timestamp", table_type=events_table_type) - select_query_type = ast.SelectQueryType( - columns={"event": event_field_type, "timestamp": timestamp_field_type}, - tables={"events": events_table_type}, - ) - - expected = ast.SelectQuery( - select=[ - ast.Field(chain=["event"], type=event_field_type), - ast.Field(chain=["events", "timestamp"], type=timestamp_field_type), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - type=events_table_type, - ), - where=ast.CompareOperation( - left=ast.Field(chain=["events", "event"], type=event_field_type), - op=ast.CompareOperationOp.Eq, - right=ast.Constant(value="test", type=ast.StringType()), - type=ast.BooleanType(), - ), - type=select_query_type, - ) - - # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot def test_will_not_run_twice(self): expr = self._select("SELECT event, events.timestamp FROM events WHERE events.event = 'test'") @@ -91,186 +60,23 @@ def test_will_not_run_twice(self): "Type already resolved for SelectQuery (SelectQueryType). Can't run again.", ) + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_events_table_alias(self): expr = self._select("SELECT event, e.timestamp FROM events e WHERE e.event = 'test'") expr = resolve_types(expr, self.context) + assert pretty_dataclasses(expr) == self.snapshot - events_table_type = ast.TableType(table=self.database.events) - events_table_alias_type = ast.TableAliasType(alias="e", table_type=events_table_type) - event_field_type = ast.FieldType(name="event", table_type=events_table_alias_type) - timestamp_field_type = ast.FieldType(name="timestamp", table_type=events_table_alias_type) - select_query_type = ast.SelectQueryType( - columns={"event": event_field_type, "timestamp": timestamp_field_type}, - tables={"e": events_table_alias_type}, - ) - - expected = ast.SelectQuery( - select=[ - ast.Field(chain=["event"], type=event_field_type), - ast.Field(chain=["e", "timestamp"], type=timestamp_field_type), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - alias="e", - type=events_table_alias_type, - ), - where=ast.CompareOperation( - left=ast.Field(chain=["e", "event"], type=event_field_type), - op=ast.CompareOperationOp.Eq, - right=ast.Constant(value="test", type=ast.StringType()), - type=ast.BooleanType(), - ), - type=select_query_type, - ) - - # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) - + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_events_table_column_alias(self): expr = self._select("SELECT event as ee, ee, ee as e, e.timestamp FROM events e WHERE e.event = 'test'") expr = resolve_types(expr, self.context) + assert pretty_dataclasses(expr) == self.snapshot - events_table_type = ast.TableType(table=self.database.events) - events_table_alias_type = ast.TableAliasType(alias="e", table_type=events_table_type) - event_field_type = ast.FieldType(name="event", table_type=events_table_alias_type) - timestamp_field_type = ast.FieldType(name="timestamp", table_type=events_table_alias_type) - - select_query_type = ast.SelectQueryType( - aliases={ - "ee": ast.FieldAliasType(alias="ee", type=event_field_type), - "e": ast.FieldAliasType( - alias="e", - type=ast.FieldAliasType(alias="ee", type=event_field_type), - ), - }, - columns={ - "ee": ast.FieldAliasType(alias="ee", type=event_field_type), - "e": ast.FieldAliasType( - alias="e", - type=ast.FieldAliasType(alias="ee", type=event_field_type), - ), - "timestamp": timestamp_field_type, - }, - tables={"e": events_table_alias_type}, - ) - - expected = ast.SelectQuery( - select=[ - ast.Alias( - alias="ee", - expr=ast.Field(chain=["event"], type=event_field_type), - type=select_query_type.aliases["ee"], - ), - ast.Field(chain=["ee"], type=select_query_type.aliases["ee"]), - ast.Alias( - alias="e", - expr=ast.Field(chain=["ee"], type=select_query_type.aliases["ee"]), - type=select_query_type.aliases["e"], # is ee ? - ), - ast.Field(chain=["e", "timestamp"], type=timestamp_field_type), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - alias="e", - type=select_query_type.tables["e"], - ), - where=ast.CompareOperation( - left=ast.Field(chain=["e", "event"], type=event_field_type), - op=ast.CompareOperationOp.Eq, - right=ast.Constant(value="test", type=ast.StringType()), - type=ast.BooleanType(), - ), - type=select_query_type, - ) - # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) - + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_events_table_column_alias_inside_subquery(self): expr = self._select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") expr = resolve_types(expr, self.context) - inner_events_table_type = ast.TableType(table=self.database.events) - inner_event_field_type = ast.FieldAliasType( - alias="b", - type=ast.FieldType(name="event", table_type=inner_events_table_type), - ) - timestamp_field_type = ast.FieldType(name="timestamp", table_type=inner_events_table_type) - timstamp_alias_type = ast.FieldAliasType(alias="c", type=timestamp_field_type) - inner_select_type = ast.SelectQueryType( - aliases={ - "b": inner_event_field_type, - "c": ast.FieldAliasType(alias="c", type=timestamp_field_type), - }, - columns={ - "b": inner_event_field_type, - "c": ast.FieldAliasType(alias="c", type=timestamp_field_type), - }, - tables={ - "events": inner_events_table_type, - }, - ) - select_alias_type = ast.SelectQueryAliasType(alias="e", select_query_type=inner_select_type) - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["b"], - type=ast.FieldType( - name="b", - table_type=ast.SelectQueryAliasType(alias="e", select_query_type=inner_select_type), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.SelectQuery( - select=[ - ast.Alias( - alias="b", - expr=ast.Field(chain=["event"], type=inner_event_field_type.type), - type=inner_event_field_type, - ), - ast.Alias( - alias="c", - expr=ast.Field(chain=["timestamp"], type=timestamp_field_type), - type=timstamp_alias_type, - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=inner_events_table_type), - type=inner_events_table_type, - ), - type=inner_select_type, - ), - alias="e", - type=select_alias_type, - ), - where=ast.CompareOperation( - left=ast.Field( - chain=["e", "b"], - type=ast.FieldType(name="b", table_type=select_alias_type), - ), - op=ast.CompareOperationOp.Eq, - right=ast.Constant(value="test", type=ast.StringType()), - type=ast.BooleanType(), - ), - type=ast.SelectQueryType( - aliases={}, - columns={"b": ast.FieldType(name="b", table_type=select_alias_type)}, - tables={"e": select_alias_type}, - ), - ) - # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot def test_resolve_subquery_no_field_access(self): # From ClickHouse's GitHub: "Aliases defined outside of subquery are not visible in subqueries (but see below)." @@ -281,6 +87,7 @@ def test_resolve_subquery_no_field_access(self): expr = resolve_types(expr, self.context) self.assertEqual(str(e.exception), "Unable to resolve field: e") + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_constant_type(self): with freeze_time("2020-01-10 00:00:00"): expr = self._select( @@ -295,66 +102,13 @@ def test_resolve_constant_type(self): }, ) expr = resolve_types(expr, self.context) - expected = ast.SelectQuery( - select=[ - ast.Constant(value=1, type=ast.IntegerType()), - ast.Constant(value="boo", type=ast.StringType()), - ast.Constant(value=True, type=ast.BooleanType()), - ast.Constant(value=1.1232, type=ast.FloatType()), - ast.Constant(value=None, type=ast.UnknownType()), - ast.Constant(value=date(2020, 1, 10), type=ast.DateType()), - ast.Constant( - value=datetime(2020, 1, 10, 0, 0, 0, tzinfo=timezone.utc), - type=ast.DateTimeType(), - ), - ast.Constant( - value=UUID("00000000-0000-4000-8000-000000000000"), - type=ast.UUIDType(), - ), - ast.Constant(value=[], type=ast.ArrayType(item_type=ast.UnknownType())), - ast.Constant(value=[1, 2], type=ast.ArrayType(item_type=ast.IntegerType())), - ast.Constant( - value=(1, 2, 3), - type=ast.TupleType( - item_types=[ - ast.IntegerType(), - ast.IntegerType(), - ast.IntegerType(), - ] - ), - ), - ], - type=ast.SelectQueryType(aliases={}, columns={}, tables={}), - ) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_boolean_operation_types(self): expr = self._select("SELECT 1 and 1, 1 or 1, not true") expr = resolve_types(expr, self.context) - expected = ast.SelectQuery( - select=[ - ast.And( - exprs=[ - ast.Constant(value=1, type=ast.IntegerType()), - ast.Constant(value=1, type=ast.IntegerType()), - ], - type=ast.BooleanType(), - ), - ast.Or( - exprs=[ - ast.Constant(value=1, type=ast.IntegerType()), - ast.Constant(value=1, type=ast.IntegerType()), - ], - type=ast.BooleanType(), - ), - ast.Not( - expr=ast.Constant(value=True, type=ast.BooleanType()), - type=ast.BooleanType(), - ), - ], - type=ast.SelectQueryType(aliases={}, columns={}, tables={}), - ) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot def test_resolve_errors(self): queries = [ @@ -369,388 +123,53 @@ def test_resolve_errors(self): resolve_types(self._select(query), self.context) self.assertIn("Unable to resolve field:", str(e.exception)) + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_lazy_pdi_person_table(self): expr = self._select("select distinct_id, person.id from person_distinct_ids") expr = resolve_types(expr, self.context) - pdi_table_type = ast.LazyTableType(table=self.database.person_distinct_ids) - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["distinct_id"], - type=ast.FieldType(name="distinct_id", table_type=pdi_table_type), - ), - ast.Field( - chain=["person", "id"], - type=ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=pdi_table_type, - field="person", - lazy_join=self.database.person_distinct_ids.fields.get("person"), - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["person_distinct_ids"], type=pdi_table_type), - type=pdi_table_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "distinct_id": ast.FieldType(name="distinct_id", table_type=pdi_table_type), - "id": ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=pdi_table_type, - lazy_join=self.database.person_distinct_ids.fields.get("person"), - field="person", - ), - ), - }, - tables={"person_distinct_ids": pdi_table_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_lazy_events_pdi_table(self): expr = self._select("select event, pdi.person_id from events") expr = resolve_types(expr, self.context) - events_table_type = ast.TableType(table=self.database.events) - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["pdi", "person_id"], - type=ast.FieldType( - name="person_id", - table_type=ast.LazyJoinType( - table_type=events_table_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - type=events_table_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldType(name="event", table_type=events_table_type), - "person_id": ast.FieldType( - name="person_id", - table_type=ast.LazyJoinType( - table_type=events_table_type, - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - field="pdi", - ), - ), - }, - tables={"events": events_table_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_lazy_events_pdi_table_aliased(self): expr = self._select("select event, e.pdi.person_id from events e") expr = resolve_types(expr, self.context) - events_table_type = ast.TableType(table=self.database.events) - events_table_alias_type = ast.TableAliasType(table_type=events_table_type, alias="e") - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_alias_type), - ), - ast.Field( - chain=["e", "pdi", "person_id"], - type=ast.FieldType( - name="person_id", - table_type=ast.LazyJoinType( - table_type=events_table_alias_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - alias="e", - type=events_table_alias_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldType(name="event", table_type=events_table_alias_type), - "person_id": ast.FieldType( - name="person_id", - table_type=ast.LazyJoinType( - table_type=events_table_alias_type, - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - field="pdi", - ), - ), - }, - tables={"e": events_table_alias_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_lazy_events_pdi_person_table(self): expr = self._select("select event, pdi.person.id from events") expr = resolve_types(expr, self.context) - events_table_type = ast.TableType(table=self.database.events) - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["pdi", "person", "id"], - type=ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=ast.LazyJoinType( - table_type=events_table_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - field="person", - lazy_join=cast( - LazyJoin, - cast(LazyJoin, self.database.events.fields.get("pdi")).join_table.fields.get("person"), - ), - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - type=events_table_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldType(name="event", table_type=events_table_type), - "id": ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=ast.LazyJoinType( - table_type=events_table_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - field="person", - lazy_join=cast( - LazyJoin, - cast(LazyJoin, self.database.events.fields.get("pdi")).join_table.fields.get("person"), - ), - ), - ), - }, - tables={"events": events_table_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_lazy_events_pdi_person_table_aliased(self): expr = self._select("select event, e.pdi.person.id from events e") expr = resolve_types(expr, self.context) - events_table_type = ast.TableType(table=self.database.events) - events_table_alias_type = ast.TableAliasType(table_type=events_table_type, alias="e") - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_alias_type), - ), - ast.Field( - chain=["e", "pdi", "person", "id"], - type=ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=ast.LazyJoinType( - table_type=events_table_alias_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - field="person", - lazy_join=cast( - LazyJoin, - cast(LazyJoin, self.database.events.fields.get("pdi")).join_table.fields.get("person"), - ), - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - alias="e", - type=events_table_alias_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldType(name="event", table_type=events_table_alias_type), - "id": ast.FieldType( - name="id", - table_type=ast.LazyJoinType( - table_type=ast.LazyJoinType( - table_type=events_table_alias_type, - field="pdi", - lazy_join=cast(LazyJoin, self.database.events.fields.get("pdi")), - ), - field="person", - lazy_join=cast( - LazyJoin, - cast(LazyJoin, self.database.events.fields.get("pdi")).join_table.fields.get("person"), - ), - ), - ), - }, - tables={"e": events_table_alias_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_virtual_events_poe(self): expr = self._select("select event, poe.id from events") expr = resolve_types(expr, self.context) - events_table_type = ast.TableType(table=self.database.events) - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["poe", "id"], - type=ast.FieldType( - name="id", - table_type=ast.VirtualTableType( - table_type=events_table_type, - field="poe", - virtual_table=self.database.events.fields["poe"], - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], type=events_table_type), - type=events_table_type, - ), - type=ast.SelectQueryType( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldType(name="event", table_type=events_table_type), - "id": ast.FieldType( - name="id", - table_type=ast.VirtualTableType( - table_type=events_table_type, - field="poe", - virtual_table=self.database.events.fields.get("poe"), - ), - ), - }, - tables={"events": events_table_type}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.type, expected.type) - self.assertEqual(expr, expected) + assert pretty_dataclasses(expr) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_resolve_union_all(self): node = self._select("select event, timestamp from events union all select event, timestamp from events") node = resolve_types(node, self.context) + assert pretty_dataclasses(node) == self.snapshot - events_table_type = ast.TableType(table=self.database.events) - self.assertEqual( - node.select_queries[0].select, - [ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=events_table_type), - ), - ], - ) - self.assertEqual( - node.select_queries[1].select, - [ - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=events_table_type), - ), - ], - ) - + @pytest.mark.usefixtures("unittest_snapshot") def test_call_type(self): node = self._select("select max(timestamp) from events") node = resolve_types(node, self.context) - expected = [ - ast.Call( - name="max", - # NB! timestamp was resolved to a DateTimeType for the Call's arg type. - type=ast.CallType( - name="max", - arg_types=[ast.DateTimeType()], - return_type=ast.UnknownType(), - ), - args=[ - ast.Field( - chain=["timestamp"], - type=ast.FieldType( - name="timestamp", - table_type=ast.TableType(table=self.database.events), - ), - ) - ], - ), - ] - self.assertEqual(node.select, expected) + assert pretty_dataclasses(node) == self.snapshot def test_ctes_loop(self): with self.assertRaises(ResolverException) as e: @@ -760,10 +179,7 @@ def test_ctes_loop(self): def test_ctes_basic_column(self): expr = self._print_hogql("with 1 as cte select cte from events") expected = self._print_hogql("select 1 from events") - self.assertEqual( - expr, - expected, - ) + self.assertEqual(expr, expected) def test_ctes_recursive_column(self): self.assertEqual( @@ -815,282 +231,40 @@ def test_ctes_subquery_recursion(self): ) @override_settings(PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False) + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_table(self): self.setUp() # rebuild self.database with PERSON_ON_EVENTS_OVERRIDE=False node = self._select("select * from events") node = resolve_types(node, self.context) - - events_table_type = ast.TableType(table=self.database.events) - self.assertEqual( - node.select, - [ - ast.Field( - chain=["uuid"], - type=ast.FieldType(name="uuid", table_type=events_table_type), - ), - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_type), - ), - ast.Field( - chain=["properties"], - type=ast.FieldType(name="properties", table_type=events_table_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=events_table_type), - ), - ast.Field( - chain=["distinct_id"], - type=ast.FieldType(name="distinct_id", table_type=events_table_type), - ), - ast.Field( - chain=["elements_chain"], - type=ast.FieldType(name="elements_chain", table_type=events_table_type), - ), - ast.Field( - chain=["created_at"], - type=ast.FieldType(name="created_at", table_type=events_table_type), - ), - ast.Field( - chain=["$session_id"], - type=ast.FieldType(name="$session_id", table_type=events_table_type), - ), - ast.Field( - chain=["$group_0"], - type=ast.FieldType(name="$group_0", table_type=events_table_type), - ), - ast.Field( - chain=["$group_1"], - type=ast.FieldType(name="$group_1", table_type=events_table_type), - ), - ast.Field( - chain=["$group_2"], - type=ast.FieldType(name="$group_2", table_type=events_table_type), - ), - ast.Field( - chain=["$group_3"], - type=ast.FieldType(name="$group_3", table_type=events_table_type), - ), - ast.Field( - chain=["$group_4"], - type=ast.FieldType(name="$group_4", table_type=events_table_type), - ), - ], - ) + assert pretty_dataclasses(node) == self.snapshot @override_settings(PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False) + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_table_alias(self): self.setUp() # rebuild self.database with PERSON_ON_EVENTS_OVERRIDE=False node = self._select("select * from events e") node = resolve_types(node, self.context) + assert pretty_dataclasses(node) == self.snapshot - events_table_type = ast.TableType(table=self.database.events) - events_table_alias_type = ast.TableAliasType(table_type=events_table_type, alias="e") - self.assertEqual( - node.select, - [ - ast.Field( - chain=["uuid"], - type=ast.FieldType(name="uuid", table_type=events_table_alias_type), - ), - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=events_table_alias_type), - ), - ast.Field( - chain=["properties"], - type=ast.FieldType(name="properties", table_type=events_table_alias_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=events_table_alias_type), - ), - ast.Field( - chain=["distinct_id"], - type=ast.FieldType(name="distinct_id", table_type=events_table_alias_type), - ), - ast.Field( - chain=["elements_chain"], - type=ast.FieldType(name="elements_chain", table_type=events_table_alias_type), - ), - ast.Field( - chain=["created_at"], - type=ast.FieldType(name="created_at", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$session_id"], - type=ast.FieldType(name="$session_id", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$group_0"], - type=ast.FieldType(name="$group_0", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$group_1"], - type=ast.FieldType(name="$group_1", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$group_2"], - type=ast.FieldType(name="$group_2", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$group_3"], - type=ast.FieldType(name="$group_3", table_type=events_table_alias_type), - ), - ast.Field( - chain=["$group_4"], - type=ast.FieldType(name="$group_4", table_type=events_table_alias_type), - ), - ], - ) - + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_subquery(self): node = self._select("select * from (select 1 as a, 2 as b)") node = resolve_types(node, self.context) - select_subquery_type = ast.SelectQueryType( - aliases={ - "a": ast.FieldAliasType(alias="a", type=ast.IntegerType()), - "b": ast.FieldAliasType(alias="b", type=ast.IntegerType()), - }, - columns={ - "a": ast.FieldAliasType(alias="a", type=ast.IntegerType()), - "b": ast.FieldAliasType(alias="b", type=ast.IntegerType()), - }, - tables={}, - anonymous_tables=[], - ) - self.assertEqual( - node.select, - [ - ast.Field( - chain=["a"], - type=ast.FieldType(name="a", table_type=select_subquery_type), - ), - ast.Field( - chain=["b"], - type=ast.FieldType(name="b", table_type=select_subquery_type), - ), - ], - ) + assert pretty_dataclasses(node) == self.snapshot + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_subquery_alias(self): node = self._select("select x.* from (select 1 as a, 2 as b) x") node = resolve_types(node, self.context) - select_subquery_type = ast.SelectQueryAliasType( - alias="x", - select_query_type=ast.SelectQueryType( - aliases={ - "a": ast.FieldAliasType(alias="a", type=ast.IntegerType()), - "b": ast.FieldAliasType(alias="b", type=ast.IntegerType()), - }, - columns={ - "a": ast.FieldAliasType(alias="a", type=ast.IntegerType()), - "b": ast.FieldAliasType(alias="b", type=ast.IntegerType()), - }, - tables={}, - anonymous_tables=[], - ), - ) - self.assertEqual( - node.select, - [ - ast.Field( - chain=["a"], - type=ast.FieldType(name="a", table_type=select_subquery_type), - ), - ast.Field( - chain=["b"], - type=ast.FieldType(name="b", table_type=select_subquery_type), - ), - ], - ) + assert pretty_dataclasses(node) == self.snapshot @override_settings(PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False) + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_from_subquery_table(self): self.setUp() # rebuild self.database with PERSON_ON_EVENTS_OVERRIDE=False node = self._select("select * from (select * from events)") node = resolve_types(node, self.context) - - events_table_type = ast.TableType(table=self.database.events) - inner_select_type = ast.SelectQueryType( - tables={"events": events_table_type}, - anonymous_tables=[], - aliases={}, - columns={ - "uuid": ast.FieldType(name="uuid", table_type=events_table_type), - "event": ast.FieldType(name="event", table_type=events_table_type), - "properties": ast.FieldType(name="properties", table_type=events_table_type), - "timestamp": ast.FieldType(name="timestamp", table_type=events_table_type), - "distinct_id": ast.FieldType(name="distinct_id", table_type=events_table_type), - "elements_chain": ast.FieldType(name="elements_chain", table_type=events_table_type), - "created_at": ast.FieldType(name="created_at", table_type=events_table_type), - "$session_id": ast.FieldType(name="$session_id", table_type=events_table_type), - "$group_0": ast.FieldType(name="$group_0", table_type=events_table_type), - "$group_1": ast.FieldType(name="$group_1", table_type=events_table_type), - "$group_2": ast.FieldType(name="$group_2", table_type=events_table_type), - "$group_3": ast.FieldType(name="$group_3", table_type=events_table_type), - "$group_4": ast.FieldType(name="$group_4", table_type=events_table_type), - }, - ) - - self.assertEqual( - node.select, - [ - ast.Field( - chain=["uuid"], - type=ast.FieldType(name="uuid", table_type=inner_select_type), - ), - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=inner_select_type), - ), - ast.Field( - chain=["properties"], - type=ast.FieldType(name="properties", table_type=inner_select_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=inner_select_type), - ), - ast.Field( - chain=["distinct_id"], - type=ast.FieldType(name="distinct_id", table_type=inner_select_type), - ), - ast.Field( - chain=["elements_chain"], - type=ast.FieldType(name="elements_chain", table_type=inner_select_type), - ), - ast.Field( - chain=["created_at"], - type=ast.FieldType(name="created_at", table_type=inner_select_type), - ), - ast.Field( - chain=["$session_id"], - type=ast.FieldType(name="$session_id", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_0"], - type=ast.FieldType(name="$group_0", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_1"], - type=ast.FieldType(name="$group_1", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_2"], - type=ast.FieldType(name="$group_2", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_3"], - type=ast.FieldType(name="$group_3", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_4"], - type=ast.FieldType(name="$group_4", table_type=inner_select_type), - ), - ], - ) + assert pretty_dataclasses(node) == self.snapshot def test_asterisk_expander_multiple_table_error(self): node = self._select("select * from (select 1 as a, 2 as b) x left join (select 1 as a, 2 as b) y on x.a = y.a") @@ -1102,95 +276,12 @@ def test_asterisk_expander_multiple_table_error(self): ) @override_settings(PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False) + @pytest.mark.usefixtures("unittest_snapshot") def test_asterisk_expander_select_union(self): self.setUp() # rebuild self.database with PERSON_ON_EVENTS_OVERRIDE=False node = self._select("select * from (select * from events union all select * from events)") node = resolve_types(node, self.context) - - events_table_type = ast.TableType(table=self.database.events) - inner_select_type = ast.SelectUnionQueryType( - types=[ - ast.SelectQueryType( - tables={"events": events_table_type}, - anonymous_tables=[], - aliases={}, - columns={ - "uuid": ast.FieldType(name="uuid", table_type=events_table_type), - "event": ast.FieldType(name="event", table_type=events_table_type), - "properties": ast.FieldType(name="properties", table_type=events_table_type), - "timestamp": ast.FieldType(name="timestamp", table_type=events_table_type), - "distinct_id": ast.FieldType(name="distinct_id", table_type=events_table_type), - "elements_chain": ast.FieldType(name="elements_chain", table_type=events_table_type), - "created_at": ast.FieldType(name="created_at", table_type=events_table_type), - "$session_id": ast.FieldType(name="$session_id", table_type=events_table_type), - "$group_0": ast.FieldType(name="$group_0", table_type=events_table_type), - "$group_1": ast.FieldType(name="$group_1", table_type=events_table_type), - "$group_2": ast.FieldType(name="$group_2", table_type=events_table_type), - "$group_3": ast.FieldType(name="$group_3", table_type=events_table_type), - "$group_4": ast.FieldType(name="$group_4", table_type=events_table_type), - }, - ) - ] - * 2 - ) - - self.assertEqual( - node.select, - [ - ast.Field( - chain=["uuid"], - type=ast.FieldType(name="uuid", table_type=inner_select_type), - ), - ast.Field( - chain=["event"], - type=ast.FieldType(name="event", table_type=inner_select_type), - ), - ast.Field( - chain=["properties"], - type=ast.FieldType(name="properties", table_type=inner_select_type), - ), - ast.Field( - chain=["timestamp"], - type=ast.FieldType(name="timestamp", table_type=inner_select_type), - ), - ast.Field( - chain=["distinct_id"], - type=ast.FieldType(name="distinct_id", table_type=inner_select_type), - ), - ast.Field( - chain=["elements_chain"], - type=ast.FieldType(name="elements_chain", table_type=inner_select_type), - ), - ast.Field( - chain=["created_at"], - type=ast.FieldType(name="created_at", table_type=inner_select_type), - ), - ast.Field( - chain=["$session_id"], - type=ast.FieldType(name="$session_id", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_0"], - type=ast.FieldType(name="$group_0", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_1"], - type=ast.FieldType(name="$group_1", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_2"], - type=ast.FieldType(name="$group_2", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_3"], - type=ast.FieldType(name="$group_3", table_type=inner_select_type), - ), - ast.Field( - chain=["$group_4"], - type=ast.FieldType(name="$group_4", table_type=inner_select_type), - ), - ], - ) + assert pretty_dataclasses(node) == self.snapshot def test_lambda_parent_scope(self): # does not raise @@ -1234,7 +325,8 @@ def test_visit_hogqlx_tag(self): node = cast(ast.SelectQuery, resolve_types(node, self.context)) table_node = cast(ast.SelectQuery, node).select_from.table expected = ast.SelectQuery( - select=[ast.Field(chain=["event"])], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])) + select=[ast.Field(chain=["event"])], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), ) assert clone_expr(table_node, clear_types=True) == expected diff --git a/posthog/hogql/test/utils.py b/posthog/hogql/test/utils.py index 8e5fc45313a0f..7e46c620c997a 100644 --- a/posthog/hogql/test/utils.py +++ b/posthog/hogql/test/utils.py @@ -1,3 +1,8 @@ +import dataclasses +import json +from pydantic import BaseModel + + def pretty_print_in_tests(query: str, team_id: int) -> str: return ( query.replace("SELECT", "\nSELECT") @@ -9,3 +14,52 @@ def pretty_print_in_tests(query: str, team_id: int) -> str: .replace("SETTINGS", "\nSETTINGS") .replace(f"team_id, {team_id})", "team_id, 420)") ) + + +def pretty_dataclasses(obj, seen=None, indent=0): + if seen is None: + seen = set() + + indent_space = " " * indent + next_indent = " " * (indent + 2) + + if isinstance(obj, BaseModel): + obj = obj.model_dump() + + if dataclasses.is_dataclass(obj): + obj_id = id(obj) + if obj_id in seen: + return "" + seen.add(obj_id) + + field_strings = [] + fields = sorted(dataclasses.fields(obj), key=lambda f: f.name) + for f in fields: + value = getattr(obj, f.name) + if value is not None: + formatted_value = pretty_dataclasses(value, seen, indent + 2) + field_strings.append(f"{next_indent}{f.name}: {formatted_value}") + + return "{\n" + "\n".join(field_strings) + "\n" + indent_space + "}" + + elif isinstance(obj, list): + if len(obj) == 0: + return "[]" + elements = [pretty_dataclasses(item, seen, indent + 2) for item in obj] + return "[\n" + ",\n".join(next_indent + element for element in elements) + "\n" + indent_space + "]" + + elif isinstance(obj, dict): + if len(obj) == 0: + return "{}" + sorted_items = sorted(obj.items()) + key_value_pairs = [f"{k}: {pretty_dataclasses(v, seen, indent + 2)}" for k, v in sorted_items] + return "{\n" + ",\n".join(next_indent + pair for pair in key_value_pairs) + "\n" + indent_space + "}" + + elif isinstance(obj, str): + return json.dumps(obj) + + elif callable(obj): + return "" + + else: + return str(obj) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 48018cd789264..b2a9a7d12bf4d 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -7,7 +7,7 @@ from posthog.hogql.errors import HogQLException from posthog.hogql.resolver import resolve_types from posthog.hogql.resolver_utils import get_long_table_name -from posthog.hogql.visitor import TraversingVisitor +from posthog.hogql.visitor import TraversingVisitor, clone_expr def resolve_lazy_tables( @@ -180,6 +180,7 @@ def visit_select_query(self, node: ast.SelectQuery): # For all the collected tables, create the subqueries, and add them to the table. for table_name, table_to_add in tables_to_add.items(): subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed, self.context.modifiers) + subquery = cast(ast.SelectQuery, clone_expr(subquery, clear_locations=True)) subquery = cast(ast.SelectQuery, resolve_types(subquery, self.context, [node.type])) old_table_type = select_type.tables[table_name] select_type.tables[table_name] = ast.SelectQueryAliasType(alias=table_name, select_query_type=subquery.type) @@ -202,6 +203,7 @@ def visit_select_query(self, node: ast.SelectQuery): self.context, node, ) + join_to_add = cast(ast.JoinExpr, clone_expr(join_to_add, clear_locations=True)) join_to_add = cast(ast.JoinExpr, resolve_types(join_to_add, self.context, [node.type])) select_type.tables[to_table] = join_to_add.type diff --git a/posthog/hogql_queries/events_query_runner.py b/posthog/hogql_queries/events_query_runner.py index a1b6973995668..e7ec26a441ded 100644 --- a/posthog/hogql_queries/events_query_runner.py +++ b/posthog/hogql_queries/events_query_runner.py @@ -1,6 +1,6 @@ import json from datetime import timedelta -from typing import Dict, List, Optional, Any +from typing import Dict, List, Optional from dateutil.parser import isoparse from django.db.models import Prefetch @@ -15,7 +15,7 @@ from posthog.hogql.query import execute_hogql_query from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.query_runner import QueryRunner -from posthog.models import Action, Person, Team +from posthog.models import Action, Person from posthog.models.element import chain_to_elements from posthog.models.person.person import get_distinct_ids_for_subquery from posthog.models.person.util import get_persons_by_distinct_ids @@ -39,19 +39,6 @@ class EventsQueryRunner(QueryRunner): query: EventsQuery query_type = EventsQuery - def __init__( - self, - query: EventsQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - in_export_context: Optional[bool] = False, - ): - super().__init__(query, team, timings, in_export_context) - if isinstance(query, EventsQuery): - self.query = query - else: - self.query = EventsQuery.model_validate(query) - def to_query(self) -> ast.SelectQuery: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. if self.timings is None: @@ -199,6 +186,7 @@ def calculate(self) -> EventsQueryResponse: workload=Workload.ONLINE, query_type="EventsQuery", timings=self.timings, + modifiers=self.modifiers, in_export_context=self.in_export_context, ) diff --git a/posthog/hogql_queries/hogql_query_runner.py b/posthog/hogql_queries/hogql_query_runner.py index 4326a2ba7dbee..a79e875d14a73 100644 --- a/posthog/hogql_queries/hogql_query_runner.py +++ b/posthog/hogql_queries/hogql_query_runner.py @@ -1,5 +1,4 @@ from datetime import timedelta -from typing import Dict, Optional, Any from posthog.clickhouse.client.connection import Workload from posthog.hogql import ast @@ -9,7 +8,6 @@ from posthog.hogql.query import execute_hogql_query from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.query_runner import QueryRunner -from posthog.models import Team from posthog.schema import ( HogQLQuery, HogQLQueryResponse, @@ -23,19 +21,6 @@ class HogQLQueryRunner(QueryRunner): query: HogQLQuery query_type = HogQLQuery - def __init__( - self, - query: HogQLQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - in_export_context: Optional[bool] = False, - ): - super().__init__(query, team, timings, in_export_context) - if isinstance(query, HogQLQuery): - self.query = query - else: - self.query = HogQLQuery.model_validate(query) - def to_query(self) -> ast.SelectQuery: if self.timings is None: self.timings = HogQLTimings() @@ -60,7 +45,7 @@ def calculate(self) -> HogQLQueryResponse: query_type="HogQLQuery", query=self.to_query(), filters=self.query.filters, - modifiers=self.query.modifiers, + modifiers=self.query.modifiers or self.modifiers, team=self.team, workload=Workload.ONLINE, timings=self.timings, diff --git a/posthog/hogql_queries/insights/insight_persons_query_runner.py b/posthog/hogql_queries/insights/insight_persons_query_runner.py index a6bc08c0d0849..51cf792346992 100644 --- a/posthog/hogql_queries/insights/insight_persons_query_runner.py +++ b/posthog/hogql_queries/insights/insight_persons_query_runner.py @@ -1,13 +1,11 @@ from datetime import timedelta -from typing import Dict, Optional, Any, cast +from typing import cast from posthog.hogql import ast from posthog.hogql.query import execute_hogql_query -from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.insights.lifecycle_query_runner import LifecycleQueryRunner from posthog.hogql_queries.insights.trends.trends_query_runner import TrendsQueryRunner from posthog.hogql_queries.query_runner import QueryRunner, get_query_runner -from posthog.models import Team from posthog.models.filters.mixins.utils import cached_property from posthog.schema import InsightPersonsQuery, HogQLQueryResponse @@ -16,19 +14,6 @@ class InsightPersonsQueryRunner(QueryRunner): query: InsightPersonsQuery query_type = InsightPersonsQuery - def __init__( - self, - query: InsightPersonsQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - in_export_context: Optional[bool] = False, - ): - super().__init__(query, team, timings, in_export_context) - if isinstance(query, InsightPersonsQuery): - self.query = query - else: - self.query = InsightPersonsQuery.model_validate(query) - @cached_property def source_runner(self) -> QueryRunner: return get_query_runner(self.query.source, self.team, self.timings, self.in_export_context) @@ -54,6 +39,7 @@ def calculate(self) -> HogQLQueryResponse: query=self.to_query(), team=self.team, timings=self.timings, + modifiers=self.modifiers, ) def _is_stale(self, cached_result_package): diff --git a/posthog/hogql_queries/insights/lifecycle_query_runner.py b/posthog/hogql_queries/insights/lifecycle_query_runner.py index da088f11daac7..c8731994d9a8c 100644 --- a/posthog/hogql_queries/insights/lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/lifecycle_query_runner.py @@ -1,6 +1,6 @@ from datetime import timedelta from math import ceil -from typing import Optional, Any, Dict, List +from typing import Optional, List from django.utils.timezone import datetime from posthog.caching.insights_api import ( @@ -14,9 +14,8 @@ from posthog.hogql.printer import to_printed_hogql from posthog.hogql.property import property_to_expr, action_to_expr from posthog.hogql.query import execute_hogql_query -from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.query_runner import QueryRunner -from posthog.models import Team, Action +from posthog.models import Action from posthog.hogql_queries.utils.query_date_range import QueryDateRange from posthog.models.filters.mixins.utils import cached_property from posthog.schema import ( @@ -31,15 +30,6 @@ class LifecycleQueryRunner(QueryRunner): query: LifecycleQuery query_type = LifecycleQuery - def __init__( - self, - query: LifecycleQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - in_export_context: Optional[bool] = False, - ): - super().__init__(query, team, timings, in_export_context) - def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: if self.query.samplingFactor == 0: counts_with_sampling = ast.Constant(value=0) @@ -139,6 +129,7 @@ def calculate(self) -> LifecycleQueryResponse: query=query, team=self.team, timings=self.timings, + modifiers=self.modifiers, ) # TODO: can we move the data conversion part into the query as well? It would make it easier to swap diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index f6afdfd591e85..f7499741cd51e 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -420,10 +420,10 @@ def test_trends_breakdowns(self): assert len(response.results) == 4 assert breakdown_labels == ["Chrome", "Edge", "Firefox", "Safari"] - assert response.results[0]["label"] == f"$pageview - Chrome" - assert response.results[1]["label"] == f"$pageview - Edge" - assert response.results[2]["label"] == f"$pageview - Firefox" - assert response.results[3]["label"] == f"$pageview - Safari" + assert response.results[0]["label"] == f"Chrome" + assert response.results[1]["label"] == f"Edge" + assert response.results[2]["label"] == f"Firefox" + assert response.results[3]["label"] == f"Safari" assert response.results[0]["count"] == 6 assert response.results[1]["count"] == 1 assert response.results[2]["count"] == 2 @@ -479,11 +479,11 @@ def test_trends_breakdowns_histogram(self): "[32.5,40.01]", ] - assert response.results[0]["label"] == '$pageview - ["",""]' - assert response.results[1]["label"] == "$pageview - [10.0,17.5]" - assert response.results[2]["label"] == "$pageview - [17.5,25.0]" - assert response.results[3]["label"] == "$pageview - [25.0,32.5]" - assert response.results[4]["label"] == "$pageview - [32.5,40.01]" + assert response.results[0]["label"] == '["",""]' + assert response.results[1]["label"] == "[10.0,17.5]" + assert response.results[2]["label"] == "[17.5,25.0]" + assert response.results[3]["label"] == "[25.0,32.5]" + assert response.results[4]["label"] == "[32.5,40.01]" assert response.results[0]["data"] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] assert response.results[1]["data"] == [0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0] @@ -554,14 +554,47 @@ def test_trends_breakdowns_hogql(self): assert len(response.results) == 4 assert breakdown_labels == ["Chrome", "Edge", "Firefox", "Safari"] + assert response.results[0]["label"] == f"Chrome" + assert response.results[1]["label"] == f"Edge" + assert response.results[2]["label"] == f"Firefox" + assert response.results[3]["label"] == f"Safari" + assert response.results[0]["count"] == 6 + assert response.results[1]["count"] == 1 + assert response.results[2]["count"] == 2 + assert response.results[3]["count"] == 1 + + def test_trends_breakdowns_multiple_hogql(self): + self._create_test_events() + + response = self._run_trends_query( + "2020-01-09", + "2020-01-20", + IntervalType.day, + [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], + None, + BreakdownFilter(breakdown_type=BreakdownType.hogql, breakdown="properties.$browser"), + ) + + breakdown_labels = [result["breakdown_value"] for result in response.results] + + assert len(response.results) == 8 + assert breakdown_labels == ["Chrome", "Edge", "Firefox", "Safari", "Chrome", "Edge", "Firefox", "Safari"] assert response.results[0]["label"] == f"$pageview - Chrome" assert response.results[1]["label"] == f"$pageview - Edge" assert response.results[2]["label"] == f"$pageview - Firefox" assert response.results[3]["label"] == f"$pageview - Safari" + assert response.results[4]["label"] == f"$pageleave - Chrome" + assert response.results[5]["label"] == f"$pageleave - Edge" + assert response.results[6]["label"] == f"$pageleave - Firefox" + assert response.results[7]["label"] == f"$pageleave - Safari" assert response.results[0]["count"] == 6 assert response.results[1]["count"] == 1 assert response.results[2]["count"] == 2 assert response.results[3]["count"] == 1 + assert response.results[4]["count"] == 3 + assert response.results[5]["count"] == 1 + assert response.results[6]["count"] == 1 + assert response.results[7]["count"] == 1 def test_trends_breakdowns_and_compare(self): self._create_test_events() @@ -626,10 +659,10 @@ def test_trends_breakdown_and_aggregation_query_orchestration(self): assert len(response.results) == 4 assert breakdown_labels == ["Chrome", "Edge", "Firefox", "Safari"] - assert response.results[0]["label"] == f"$pageview - Chrome" - assert response.results[1]["label"] == f"$pageview - Edge" - assert response.results[2]["label"] == f"$pageview - Firefox" - assert response.results[3]["label"] == f"$pageview - Safari" + assert response.results[0]["label"] == f"Chrome" + assert response.results[1]["label"] == f"Edge" + assert response.results[2]["label"] == f"Firefox" + assert response.results[3]["label"] == f"Safari" assert response.results[0]["data"] == [ 0, diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index ff013658d021e..3aac186437f1c 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -35,6 +35,7 @@ HogQLQueryResponse, TrendsQuery, TrendsQueryResponse, + HogQLQueryModifiers, ) @@ -48,9 +49,10 @@ def __init__( query: TrendsQuery | Dict[str, Any], team: Team, timings: Optional[HogQLTimings] = None, + modifiers: Optional[HogQLQueryModifiers] = None, in_export_context: Optional[bool] = None, ): - super().__init__(query, team, timings, in_export_context) + super().__init__(query, team=team, timings=timings, modifiers=modifiers, in_export_context=in_export_context) self.series = self.setup_series() def _is_stale(self, cached_result_package): @@ -129,11 +131,12 @@ def calculate(self): query=query, team=self.team, timings=self.timings, + modifiers=self.modifiers, ) timings.extend(response.timings) - res.extend(self.build_series_response(response, series_with_extra)) + res.extend(self.build_series_response(response, series_with_extra, len(queries))) if ( self.query.trendsFilter is not None @@ -144,7 +147,7 @@ def calculate(self): return TrendsQueryResponse(results=res, timings=timings) - def build_series_response(self, response: HogQLQueryResponse, series: SeriesWithExtras): + def build_series_response(self, response: HogQLQueryResponse, series: SeriesWithExtras, series_count: int): if response.results is None: return [] @@ -243,7 +246,13 @@ def get_value(name: str, val: Any): series_object["label"] = "{} - {}".format(series_object["label"], cohort_name) series_object["breakdown_value"] = get_value("breakdown_value", val) else: - series_object["label"] = "{} - {}".format(series_object["label"], get_value("breakdown_value", val)) + # If there's multiple series, include the object label in the series label + if series_count > 1: + series_object["label"] = "{} - {}".format( + series_object["label"], get_value("breakdown_value", val) + ) + else: + series_object["label"] = get_value("breakdown_value", val) series_object["breakdown_value"] = get_value("breakdown_value", val) res.append(series_object) diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index ce490cadfc834..1fb210226f619 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -298,7 +298,7 @@ def _breakdown_filter(_filter: Dict): def _group_aggregation_filter(filter: Dict): - if _insight_type(filter) == "STICKINESS": + if _insight_type(filter) == "STICKINESS" or _insight_type(filter) == "LIFECYCLE": return {} return {"aggregation_group_type_index": filter.get("aggregation_group_type_index")} diff --git a/posthog/hogql_queries/persons_query_runner.py b/posthog/hogql_queries/persons_query_runner.py index 34c86ee13300b..9d45b0a8ccb69 100644 --- a/posthog/hogql_queries/persons_query_runner.py +++ b/posthog/hogql_queries/persons_query_runner.py @@ -1,15 +1,13 @@ import json from datetime import timedelta -from typing import Optional, Any, Dict, List, cast, Literal +from typing import List, cast, Literal from posthog.hogql import ast from posthog.hogql.constants import DEFAULT_RETURNED_ROWS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.parser import parse_expr, parse_order_expr from posthog.hogql.property import property_to_expr, has_aggregation from posthog.hogql.query import execute_hogql_query -from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.query_runner import QueryRunner, get_query_runner -from posthog.models import Team from posthog.schema import PersonsQuery, PersonsQueryResponse PERSON_FULL_TUPLE = ["id", "properties", "created_at", "is_identified"] @@ -19,25 +17,13 @@ class PersonsQueryRunner(QueryRunner): query: PersonsQuery query_type = PersonsQuery - def __init__( - self, - query: PersonsQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - in_export_context: Optional[bool] = False, - ): - super().__init__(query=query, team=team, timings=timings, in_export_context=in_export_context) - if isinstance(query, PersonsQuery): - self.query = query - else: - self.query = PersonsQuery.model_validate(query) - def calculate(self) -> PersonsQueryResponse: response = execute_hogql_query( query_type="PersonsQuery", query=self.to_query(), team=self.team, timings=self.timings, + modifiers=self.modifiers, ) input_columns = self.input_columns() if "person" in input_columns: diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index 13c59c6d51c88..d3127fde4e4de 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -28,6 +28,7 @@ HogQLQuery, InsightPersonsQuery, DashboardFilter, + HogQLQueryModifiers, ) from posthog.utils import generate_cache_key, get_safe_cache @@ -86,6 +87,7 @@ def get_query_runner( team: Team, timings: Optional[HogQLTimings] = None, in_export_context: Optional[bool] = False, + modifiers: Optional[HogQLQueryModifiers] = None, ) -> "QueryRunner": kind = None if isinstance(query, dict): @@ -103,6 +105,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "TrendsQuery": from .insights.trends.trends_query_runner import TrendsQueryRunner @@ -112,6 +115,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "EventsQuery": from .events_query_runner import EventsQueryRunner @@ -121,6 +125,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "PersonsQuery": from .persons_query_runner import PersonsQueryRunner @@ -130,6 +135,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "InsightPersonsQuery": from .insights.insight_persons_query_runner import InsightPersonsQueryRunner @@ -139,6 +145,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "HogQLQuery": from .hogql_query_runner import HogQLQueryRunner @@ -148,6 +155,7 @@ def get_query_runner( team=team, timings=timings, in_export_context=in_export_context, + modifiers=modifiers, ) if kind == "SessionsTimelineQuery": from .sessions_timeline_query_runner import SessionsTimelineQueryRunner @@ -156,19 +164,20 @@ def get_query_runner( query=cast(SessionsTimelineQuery | Dict[str, Any], query), team=team, timings=timings, + modifiers=modifiers, ) if kind == "WebOverviewQuery": from .web_analytics.web_overview import WebOverviewQueryRunner - return WebOverviewQueryRunner(query=query, team=team, timings=timings) + return WebOverviewQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) if kind == "WebTopClicksQuery": from .web_analytics.top_clicks import WebTopClicksQueryRunner - return WebTopClicksQueryRunner(query=query, team=team, timings=timings) + return WebTopClicksQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) if kind == "WebStatsTableQuery": from .web_analytics.stats_table import WebStatsTableQueryRunner - return WebStatsTableQueryRunner(query=query, team=team, timings=timings) + return WebStatsTableQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) raise ValueError(f"Can't get a runner for an unknown query kind: {kind}") @@ -178,6 +187,7 @@ class QueryRunner(ABC): query_type: Type[RunnableQueryNode] team: Team timings: HogQLTimings + modifiers: HogQLQueryModifiers in_export_context: bool def __init__( @@ -185,11 +195,13 @@ def __init__( query: RunnableQueryNode | BaseModel | Dict[str, Any], team: Team, timings: Optional[HogQLTimings] = None, + modifiers: Optional[HogQLQueryModifiers] = None, in_export_context: Optional[bool] = False, ): self.team = team self.timings = timings or HogQLTimings() self.in_export_context = in_export_context or False + self.modifiers = create_default_modifiers_for_team(team, modifiers) if isinstance(query, self.query_type): self.query = query # type: ignore else: @@ -244,7 +256,7 @@ def to_hogql(self) -> str: team_id=self.team.pk, enable_select_queries=True, timings=self.timings, - modifiers=create_default_modifiers_for_team(self.team), + modifiers=self.modifiers, ), "hogql", ) @@ -253,8 +265,9 @@ def toJSON(self) -> str: return self.query.model_dump_json(exclude_defaults=True, exclude_none=True) def _cache_key(self) -> str: + modifiers = self.modifiers.model_dump_json(exclude_defaults=True, exclude_none=True) return generate_cache_key( - f"query_{self.toJSON()}_{self.__class__.__name__}_{self.team.pk}_{self.team.timezone}" + f"query_{self.toJSON()}_{self.__class__.__name__}_{self.team.pk}_{self.team.timezone}_{modifiers}" ) @abstractmethod diff --git a/posthog/hogql_queries/sessions_timeline_query_runner.py b/posthog/hogql_queries/sessions_timeline_query_runner.py index abea2867e2b90..54f024900ff06 100644 --- a/posthog/hogql_queries/sessions_timeline_query_runner.py +++ b/posthog/hogql_queries/sessions_timeline_query_runner.py @@ -1,6 +1,6 @@ from datetime import timedelta import json -from typing import Dict, Optional, Any, cast +from typing import Dict, cast from posthog.api.element import ElementSerializer @@ -10,7 +10,6 @@ from posthog.hogql.query import execute_hogql_query from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.query_runner import QueryRunner -from posthog.models import Team from posthog.models.element.element import chain_to_elements from posthog.schema import EventType, SessionsTimelineQuery, SessionsTimelineQueryResponse, TimelineEntry from posthog.utils import relative_date_parse @@ -37,18 +36,6 @@ class SessionsTimelineQueryRunner(QueryRunner): query: SessionsTimelineQuery query_type = SessionsTimelineQuery - def __init__( - self, - query: SessionsTimelineQuery | Dict[str, Any], - team: Team, - timings: Optional[HogQLTimings] = None, - ): - super().__init__(query, team, timings) - if isinstance(query, SessionsTimelineQuery): - self.query = query - else: - self.query = SessionsTimelineQuery.model_validate(query) - def _get_events_subquery(self) -> ast.SelectQuery: after = relative_date_parse(self.query.after or "-24h", self.team.timezone_info) before = relative_date_parse(self.query.before or "-0h", self.team.timezone_info) @@ -147,6 +134,7 @@ def calculate(self) -> SessionsTimelineQueryResponse: workload=Workload.ONLINE, query_type="SessionsTimelineQuery", timings=self.timings, + modifiers=self.modifiers, ) assert query_result.results is not None timeline_entries_map: Dict[str, TimelineEntry] = {} diff --git a/posthog/hogql_queries/test/test_query_runner.py b/posthog/hogql_queries/test/test_query_runner.py index 5b82b0fae5af9..28a0d47036778 100644 --- a/posthog/hogql_queries/test/test_query_runner.py +++ b/posthog/hogql_queries/test/test_query_runner.py @@ -12,6 +12,7 @@ RunnableQueryNode, ) from posthog.models.team.team import Team +from posthog.schema import HogQLQueryModifiers, MaterializationMode, HogQLQuery from posthog.test.base import BaseTest @@ -92,7 +93,7 @@ def test_cache_key(self): runner = TestQueryRunner(query={"some_attr": "bla"}, team=team) # type: ignore cache_key = runner._cache_key() - self.assertEqual(cache_key, "cache_33c9ea3098895d5a363a75feefafef06") + self.assertEqual(cache_key, "cache_b8a6b70478ec6139c8f7f379c808d5b9") def test_cache_key_runner_subclass(self): TestQueryRunner = self.setup_test_query_runner_class() @@ -106,7 +107,7 @@ class TestSubclassQueryRunner(TestQueryRunner): # type: ignore runner = TestSubclassQueryRunner(query={"some_attr": "bla"}, team=team) # type: ignore cache_key = runner._cache_key() - self.assertEqual(cache_key, "cache_d626615de8ad0df73c1d8610ca586597") + self.assertEqual(cache_key, "cache_cfab9e42d088def74792922de5b513ac") def test_cache_key_different_timezone(self): TestQueryRunner = self.setup_test_query_runner_class() @@ -117,7 +118,7 @@ def test_cache_key_different_timezone(self): runner = TestQueryRunner(query={"some_attr": "bla"}, team=team) # type: ignore cache_key = runner._cache_key() - self.assertEqual(cache_key, "cache_aeb23ec9e8de56dd8499f99f2e976d5a") + self.assertEqual(cache_key, "cache_9f12fefe07c0ab79e93935aed6b0bfa6") def test_cache_response(self): TestQueryRunner = self.setup_test_query_runner_class() @@ -143,3 +144,28 @@ def test_cache_response(self): # returns fresh response if stale response = runner.run(refresh_requested=False) self.assertEqual(response.is_cached, False) + + def test_modifier_passthrough(self): + try: + from ee.clickhouse.materialized_columns.analyze import materialize + from posthog.hogql_queries.hogql_query_runner import HogQLQueryRunner + + materialize("events", "$browser") + except ModuleNotFoundError: + # EE not available? Assume we're good + self.assertEqual(1 + 2, 3) + return + + runner = HogQLQueryRunner( + query=HogQLQuery(query="select properties.$browser from events"), + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_string), + ) + assert "events.`mat_$browser" in runner.calculate().clickhouse + + runner = HogQLQueryRunner( + query=HogQLQuery(query="select properties.$browser from events"), + team=self.team, + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.disabled), + ) + assert "events.`mat_$browser" not in runner.calculate().clickhouse diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index 0e3f9b67a1943..815ce775c91d8 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -77,6 +77,7 @@ def calculate(self): query=self.to_query(), team=self.team, timings=self.timings, + modifiers=self.modifiers, ) return WebStatsTableQueryResponse( diff --git a/posthog/hogql_queries/web_analytics/top_clicks.py b/posthog/hogql_queries/web_analytics/top_clicks.py index 1693f2c1d86ce..3218e68975f7a 100644 --- a/posthog/hogql_queries/web_analytics/top_clicks.py +++ b/posthog/hogql_queries/web_analytics/top_clicks.py @@ -50,6 +50,7 @@ def calculate(self): query=self.to_query(), team=self.team, timings=self.timings, + modifiers=self.modifiers, ) return WebTopClicksQueryResponse( diff --git a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py index 4c8b2b857eec3..201fad05baf8c 100644 --- a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py +++ b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py @@ -48,17 +48,21 @@ def property_filters_without_pathname(self) -> List[Union[EventPropertyFilter, P return [p for p in self.query.properties if p.key != "$pathname"] def session_where(self, include_previous_period: Optional[bool] = None): - properties = [ - parse_expr( - "events.timestamp < {date_to} AND events.timestamp >= minus({date_from}, toIntervalHour(1))", - placeholders={ - "date_from": self.query_date_range.previous_period_date_from_as_hogql() - if include_previous_period - else self.query_date_range.date_from_as_hogql(), - "date_to": self.query_date_range.date_to_as_hogql(), - }, - ) - ] + self.property_filters_without_pathname + properties = ( + [ + parse_expr( + "events.timestamp < {date_to} AND events.timestamp >= minus({date_from}, toIntervalHour(1))", + placeholders={ + "date_from": self.query_date_range.previous_period_date_from_as_hogql() + if include_previous_period + else self.query_date_range.date_from_as_hogql(), + "date_to": self.query_date_range.date_to_as_hogql(), + }, + ) + ] + + self.property_filters_without_pathname + + self._test_account_filters + ) return property_to_expr( properties, self.team, @@ -91,17 +95,29 @@ def session_having(self, include_previous_period: Optional[bool] = None): ) def events_where(self): - properties = [ - parse_expr( - "events.timestamp >= {date_from}", - placeholders={"date_from": self.query_date_range.date_from_as_hogql()}, - ) - ] + self.query.properties + properties = ( + [ + parse_expr( + "events.timestamp >= {date_from}", + placeholders={"date_from": self.query_date_range.date_from_as_hogql()}, + ) + ] + + self.query.properties + + self._test_account_filters + ) + return property_to_expr( properties, self.team, ) + @cached_property + def _test_account_filters(self): + if isinstance(self.team.test_account_filters, list) and len(self.team.test_account_filters) > 0: + return self.team.test_account_filters + else: + return [] + def _is_stale(self, cached_result_package): date_to = self.query_date_range.date_to() interval = self.query_date_range.interval_name diff --git a/posthog/hogql_queries/web_analytics/web_overview.py b/posthog/hogql_queries/web_analytics/web_overview.py index 19a587245443d..bd7aba12364dd 100644 --- a/posthog/hogql_queries/web_analytics/web_overview.py +++ b/posthog/hogql_queries/web_analytics/web_overview.py @@ -104,6 +104,7 @@ def calculate(self): query=self.to_query(), team=self.team, timings=self.timings, + modifiers=self.modifiers, ) row = response.results[0] diff --git a/posthog/management/commands/create_batch_export_from_app.py b/posthog/management/commands/create_batch_export_from_app.py index 2386d67e0a6f7..9e6202427d701 100644 --- a/posthog/management/commands/create_batch_export_from_app.py +++ b/posthog/management/commands/create_batch_export_from_app.py @@ -228,9 +228,27 @@ def map_plugin_config_to_destination(plugin_config: PluginConfig) -> tuple[str, "exclude_events": plugin_config.config.get("eventsToIgnore", "").split(",") or None, } export_type = "Postgres" + + elif plugin.name == "Redshift Export Plugin": + config = { + "database": plugin_config.config["dbName"], + "user": plugin_config.config["dbUsername"], + "password": plugin_config.config["dbPassword"], + "schema": "", + "host": plugin_config.config["clusterHost"], + "port": int( + plugin_config.config.get("clusterPort", "5439"), + ), + "table_name": plugin_config.config.get("tableName", "posthog_event"), + "exclude_events": plugin_config.config.get("eventsToIgnore", "").split(",") or None, + "properties_data_type": plugin_config.config.get("propertiesDataType", "varchar"), + } + export_type = "Redshift" + else: raise CommandError( - f"Unsupported Plugin: '{plugin.name}'. Supported Plugins are: 'Snowflake Export' and 'S3 Export Plugin'" + f"Unsupported Plugin: '{plugin.name}'." + "Supported Plugins are: 'BigQuery Export', 'PostgreSQL Export Plugin', 'Redshift Export Plugin', 'Snowflake Export', and 'S3 Export Plugin'" ) return (export_type, config) diff --git a/posthog/management/commands/test/test_create_batch_export_from_app.py b/posthog/management/commands/test/test_create_batch_export_from_app.py index 6932f518928d7..9f1b8be67f683 100644 --- a/posthog/management/commands/test/test_create_batch_export_from_app.py +++ b/posthog/management/commands/test/test_create_batch_export_from_app.py @@ -84,6 +84,18 @@ def postgres_plugin(organization) -> typing.Generator[Plugin, None, None]: plugin.delete() +@pytest.fixture +def redshift_plugin(organization) -> typing.Generator[Plugin, None, None]: + plugin = Plugin.objects.create( + name="Redshift Export Plugin", + url="https://github.com/PostHog/postgres-plugin", + plugin_type="custom", + organization=organization, + ) + yield plugin + plugin.delete() + + test_snowflake_config = { "account": "snowflake-account", "username": "test-user", @@ -142,6 +154,16 @@ def postgres_plugin(organization) -> typing.Generator[Plugin, None, None]: "eventsToIgnore": "$feature_flag_called,$pageleave,$pageview,$rageclick,$identify", "hasSelfSignedCert": "Yes", } +test_redshift_config = { + "clusterHost": "localhost", + "clusterPort": "5439", + "dbName": "dev", + "tableName": "posthog_event", + "dbPassword": "password", + "dbUsername": "username", + "eventsToIgnore": "$feature_flag_called", + "propertiesDataType": "super", +} PluginConfigParams = collections.namedtuple( "PluginConfigParams", ("plugin_type", "disabled", "database_url"), defaults=(False, False) @@ -168,13 +190,15 @@ def config(request) -> dict[str, str]: return test_postgres_config_with_database_url else: return test_postgres_config + case "Redshift": + return test_redshift_config case _: raise ValueError(f"Unsupported plugin: {request.param}") @pytest.fixture def plugin_config( - request, bigquery_plugin, postgres_plugin, s3_plugin, snowflake_plugin, team + request, bigquery_plugin, postgres_plugin, s3_plugin, snowflake_plugin, team, redshift_plugin ) -> typing.Generator[PluginConfig, None, None]: """Manage a PluginConfig for testing. @@ -215,6 +239,10 @@ def plugin_config( else: config = test_postgres_config + case "Redshift": + plugin = redshift_plugin + config = test_redshift_config + case _: raise ValueError(f"Unsupported plugin: {params.plugin_type}") @@ -258,6 +286,7 @@ def plugin_config( ("BigQuery", "BigQuery", "BigQuery"), ("Postgres", "Postgres", "Postgres"), (("Postgres", False, True), ("Postgres", False, True), "Postgres"), + ("Redshift", "Redshift", "Redshift"), ], indirect=["plugin_config", "config"], ) @@ -277,7 +306,7 @@ def test_map_plugin_config_to_destination(plugin_config, config, expected_type): assert (value == "Yes") == export_config["has_self_signed_cert"] continue - if key == "port": + if key in ("port", "clusterPort"): value = int(value) if key in ( @@ -293,7 +322,7 @@ def test_map_plugin_config_to_destination(plugin_config, config, expected_type): @pytest.mark.django_db @pytest.mark.parametrize( "plugin_config", - ("S3", "Snowflake", "BigQuery", "Postgres", ("Postgres", False, True)), + ("S3", "Snowflake", "BigQuery", "Postgres", ("Postgres", False, True), "Redshift"), indirect=True, ) def test_create_batch_export_from_app_fails_with_mismatched_team_id(plugin_config): @@ -311,7 +340,7 @@ def test_create_batch_export_from_app_fails_with_mismatched_team_id(plugin_confi @pytest.mark.django_db @pytest.mark.parametrize( "plugin_config", - ("S3", "Snowflake", "BigQuery", "Postgres", ("Postgres", False, True)), + ("S3", "Snowflake", "BigQuery", "Postgres", ("Postgres", False, True), "Redshift"), indirect=True, ) def test_create_batch_export_from_app_dry_run(plugin_config): @@ -340,7 +369,14 @@ def test_create_batch_export_from_app_dry_run(plugin_config): @pytest.mark.parametrize("interval", ("hour", "day")) @pytest.mark.parametrize( "plugin_config", - (("S3", False), ("Snowflake", False), ("BigQuery", False), ("Postgres", False), ("Postgres", False, True)), + ( + ("S3", False), + ("Snowflake", False), + ("BigQuery", False), + ("Redshift", False), + ("Postgres", False), + ("Postgres", False, True), + ), indirect=True, ) @pytest.mark.parametrize("disable_plugin_config", (True, False)) @@ -399,7 +435,14 @@ def test_create_batch_export_from_app( @pytest.mark.parametrize("interval", ("hour", "day")) @pytest.mark.parametrize( "plugin_config", - (("S3", True), ("Snowflake", True), ("BigQuery", True), ("Postgres", True), ("Postgres", True, True)), + ( + ("S3", True), + ("Snowflake", True), + ("BigQuery", True), + ("Redshift", True), + ("Postgres", True), + ("Postgres", True, True), + ), indirect=True, ) @pytest.mark.parametrize("migrate_disabled_plugin_config", (True, False)) @@ -484,7 +527,14 @@ async def wait_for_workflow_executions( @pytest.mark.parametrize("interval", ("hour", "day")) @pytest.mark.parametrize( "plugin_config", - (("S3", False), ("Snowflake", False), ("BigQuery", False), ("Postgres", False), ("Postgres", False, True)), + ( + ("S3", False), + ("Snowflake", False), + ("BigQuery", False), + ("Redshift", False), + ("Postgres", False), + ("Postgres", False, True), + ), indirect=True, ) def test_create_batch_export_from_app_with_backfill(interval, plugin_config): diff --git a/posthog/migrations/0364_team_external_data_workspace_rows.py b/posthog/migrations/0364_team_external_data_workspace_rows.py new file mode 100644 index 0000000000000..ec9478becd1c9 --- /dev/null +++ b/posthog/migrations/0364_team_external_data_workspace_rows.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.19 on 2023-11-07 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0363_add_replay_payload_capture_config"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="external_data_workspace_last_synced_at", + field=models.DateTimeField(blank=True, null=True), + ) + ] diff --git a/posthog/migrations/0365_update_created_by_flag_constraint.py b/posthog/migrations/0365_update_created_by_flag_constraint.py new file mode 100644 index 0000000000000..e8912598ae3fd --- /dev/null +++ b/posthog/migrations/0365_update_created_by_flag_constraint.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.19 on 2023-11-09 10:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0364_team_external_data_workspace_rows"), + ] + + # :TRICKY: + # We are replacing the original generated migration: + # migrations.AlterField( + # model_name='experiment', + # name='created_by', + # field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + # ), + # migrations.AlterField( + # model_name='featureflag', + # name='created_by', + # field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + # ), + # with one that adds the 'NOT VALID' directive, which applies the constraint only for inserts/updates. + # This ensures the table is not locked when creating the new constraint. + # A follow up migration will validate the constraint. + # The code here is exactly the same as the one generated by the default migration, except for the 'NOT VALID' directive. + + operations = [ + # make the created_by column nullable in experiments & flags + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="experiment", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="posthog.user", + ), + ), + migrations.AlterField( + model_name="featureflag", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="posthog.user", + ), + ), + ], + database_operations=[ + # We add -- existing-table-constraint-ignore to ignore the constraint validation in CI. + # This should be safe, because we are making the constraint NOT VALID, so doesn't lock things up for long. + migrations.RunSQL( + """ + SET CONSTRAINTS "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id" IMMEDIATE; -- existing-table-constraint-ignore + ALTER TABLE "posthog_experiment" DROP CONSTRAINT "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id"; -- existing-table-constraint-ignore + ALTER TABLE "posthog_experiment" ALTER COLUMN "created_by_id" DROP NOT NULL; + ALTER TABLE "posthog_experiment" ADD CONSTRAINT "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id" FOREIGN KEY ("created_by_id") REFERENCES "posthog_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; -- existing-table-constraint-ignore + """, + reverse_sql=""" + SET CONSTRAINTS "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id" IMMEDIATE; + ALTER TABLE "posthog_experiment" DROP CONSTRAINT "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id"; + ALTER TABLE "posthog_experiment" ALTER COLUMN "created_by_id" SET NOT NULL; + ALTER TABLE "posthog_experiment" ADD CONSTRAINT "posthog_experiment_created_by_id_b40aea95_fk_posthog_user_id" FOREIGN KEY ("created_by_id") REFERENCES "posthog_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; + """, + ), + migrations.RunSQL( + """SET CONSTRAINTS "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id" IMMEDIATE; -- existing-table-constraint-ignore + ALTER TABLE "posthog_featureflag" DROP CONSTRAINT "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id"; -- existing-table-constraint-ignore + ALTER TABLE "posthog_featureflag" ALTER COLUMN "created_by_id" DROP NOT NULL; + ALTER TABLE "posthog_featureflag" ADD CONSTRAINT "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id" FOREIGN KEY ("created_by_id") REFERENCES "posthog_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; -- existing-table-constraint-ignore + """, + reverse_sql=""" + SET CONSTRAINTS "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id" IMMEDIATE; + ALTER TABLE "posthog_featureflag" DROP CONSTRAINT "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id"; + ALTER TABLE "posthog_featureflag" ALTER COLUMN "created_by_id" SET NOT NULL; + ALTER TABLE "posthog_featureflag" ADD CONSTRAINT "posthog_featureflag_created_by_id_4571fe1a_fk_posthog_user_id" FOREIGN KEY ("created_by_id") REFERENCES "posthog_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; + -- existing-table-constraint-ignore + """, + ), + ], + ), + ] diff --git a/posthog/migrations/0366_alter_action_created_by.py b/posthog/migrations/0366_alter_action_created_by.py new file mode 100644 index 0000000000000..996183b7625bf --- /dev/null +++ b/posthog/migrations/0366_alter_action_created_by.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-11-21 14:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0365_update_created_by_flag_constraint"), + ] + + operations = [ + migrations.AlterField( + model_name="action", + name="created_by", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/posthog/models/action/action.py b/posthog/models/action/action.py index 368100fcbc978..698957f8dbafd 100644 --- a/posthog/models/action/action.py +++ b/posthog/models/action/action.py @@ -20,7 +20,7 @@ class Meta: team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) description: models.TextField = models.TextField(blank=True, default="") created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True, blank=True) - created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.CASCADE, null=True, blank=True) + created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) events: models.ManyToManyField = models.ManyToManyField("Event", blank=True) post_to_slack: models.BooleanField = models.BooleanField(default=False) diff --git a/posthog/models/cohort/cohort.py b/posthog/models/cohort/cohort.py index b907df41c934a..9310900e0f8d0 100644 --- a/posthog/models/cohort/cohort.py +++ b/posthog/models/cohort/cohort.py @@ -238,7 +238,7 @@ def calculate_people_ch(self, pending_version): def insert_users_by_list(self, items: List[str]) -> None: """ - Items can be distinct_id or email + Items is a list of distinct_ids """ batchsize = 1000 @@ -298,9 +298,8 @@ def insert_users_by_list(self, items: List[str]) -> None: self.save() capture_exception(err) - def insert_users_list_by_uuid(self, items: List[str]) -> None: - batchsize = 1000 - from posthog.models.cohort.util import get_static_cohort_size + def insert_users_list_by_uuid(self, items: List[str], insert_in_clickhouse: bool = False, batchsize=1000) -> None: + from posthog.models.cohort.util import get_static_cohort_size, insert_static_cohort try: cursor = connection.cursor() @@ -309,6 +308,12 @@ def insert_users_list_by_uuid(self, items: List[str]) -> None: persons_query = ( Person.objects.filter(team_id=self.team_id).filter(uuid__in=batch).exclude(cohort__id=self.id) ) + if insert_in_clickhouse: + insert_static_cohort( + [p for p in persons_query.values_list("uuid", flat=True)], + self.pk, + self.team, + ) sql, params = persons_query.distinct("pk").only("pk").query.sql_with_params() query = UPDATE_QUERY.format( cohort_id=self.pk, diff --git a/posthog/models/cohort/test/test_util.py b/posthog/models/cohort/test/test_util.py index d8ff051a0bb41..dce0258746828 100644 --- a/posthog/models/cohort/test/test_util.py +++ b/posthog/models/cohort/test/test_util.py @@ -508,3 +508,39 @@ def test_dependent_cohorts_for_complex_nested_cohort(self): self.assertEqual(get_dependent_cohorts(cohort3), [cohort2, cohort1]) self.assertEqual(get_dependent_cohorts(cohort4), [cohort1]) self.assertEqual(get_dependent_cohorts(cohort5), [cohort4, cohort1, cohort2]) + + def test_dependent_cohorts_ignore_invalid_ids(self): + cohort1 = _create_cohort( + team=self.team, + name="cohort1", + groups=[{"properties": [{"key": "name", "value": "test", "type": "person"}]}], + ) + + cohort2 = _create_cohort( + team=self.team, + name="cohort2", + groups=[ + { + "properties": [ + {"key": "id", "value": cohort1.pk, "type": "cohort"}, + {"key": "id", "value": "invalid-key", "type": "cohort"}, + ] + } + ], + ) + + cohort3 = _create_cohort( + team=self.team, + name="cohorte", + groups=[ + { + "properties": [ + {"key": "id", "value": cohort2.pk, "type": "cohort"}, + {"key": "id", "value": "invalid-key", "type": "cohort"}, + ] + } + ], + ) + + self.assertEqual(get_dependent_cohorts(cohort2), [cohort1]) + self.assertEqual(get_dependent_cohorts(cohort3), [cohort2, cohort1]) diff --git a/posthog/models/cohort/util.py b/posthog/models/cohort/util.py index 800b937d51f15..c4201fbaf3f47 100644 --- a/posthog/models/cohort/util.py +++ b/posthog/models/cohort/util.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union import structlog from dateutil import parser @@ -440,7 +440,7 @@ def get_all_cohort_ids_by_person_uuid(uuid: str, team_id: int) -> List[int]: def get_dependent_cohorts( cohort: Cohort, using_database: str = "default", - seen_cohorts_cache: Optional[Dict[str, Cohort]] = None, + seen_cohorts_cache: Optional[Dict[int, Cohort]] = None, ) -> List[Cohort]: if seen_cohorts_cache is None: seen_cohorts_cache = {} @@ -449,22 +449,84 @@ def get_dependent_cohorts( seen_cohort_ids = set() seen_cohort_ids.add(cohort.id) - queue = [prop.value for prop in cohort.properties.flat if prop.type == "cohort"] + queue = [] + for prop in cohort.properties.flat: + if prop.type == "cohort" and not isinstance(prop.value, list): + try: + queue.append(int(prop.value)) + except (ValueError, TypeError): + continue while queue: cohort_id = queue.pop() try: - parsed_cohort_id = str(cohort_id) - if parsed_cohort_id in seen_cohorts_cache: - cohort = seen_cohorts_cache[parsed_cohort_id] + if cohort_id in seen_cohorts_cache: + cohort = seen_cohorts_cache[cohort_id] else: cohort = Cohort.objects.using(using_database).get(pk=cohort_id) - seen_cohorts_cache[parsed_cohort_id] = cohort + seen_cohorts_cache[cohort_id] = cohort if cohort.id not in seen_cohort_ids: cohorts.append(cohort) seen_cohort_ids.add(cohort.id) - queue += [prop.value for prop in cohort.properties.flat if prop.type == "cohort"] + + for prop in cohort.properties.flat: + if prop.type == "cohort" and not isinstance(prop.value, list): + try: + queue.append(int(prop.value)) + except (ValueError, TypeError): + continue + except Cohort.DoesNotExist: continue return cohorts + + +def sort_cohorts_topologically(cohort_ids: Set[int], seen_cohorts_cache: Dict[int, Cohort]) -> List[int]: + """ + Sorts the given cohorts in an order where cohorts with no dependencies are placed first, + followed by cohorts that depend on the preceding ones. It ensures that each cohort in the sorted list + only depends on cohorts that appear earlier in the list. + """ + + if not cohort_ids: + return [] + + dependency_graph: Dict[int, List[int]] = {} + seen = set() + + # build graph (adjacency list) + def traverse(cohort): + # add parent + dependency_graph[cohort.id] = [] + for prop in cohort.properties.flat: + if prop.type == "cohort" and not isinstance(prop.value, list): + # add child + dependency_graph[cohort.id].append(int(prop.value)) + + neighbor_cohort = seen_cohorts_cache[int(prop.value)] + if cohort.id not in seen: + seen.add(cohort.id) + traverse(neighbor_cohort) + + for cohort_id in cohort_ids: + cohort = seen_cohorts_cache[int(cohort_id)] + traverse(cohort) + + # post-order DFS (children first, then the parent) + def dfs(node, seen, sorted_arr): + neighbors = dependency_graph.get(node, []) + for neighbor in neighbors: + if neighbor not in seen: + dfs(neighbor, seen, sorted_arr) + sorted_arr.append(int(node)) + seen.add(node) + + sorted_cohort_ids: List[int] = [] + seen = set() + for cohort_id in cohort_ids: + if cohort_id not in seen: + seen.add(cohort_id) + dfs(cohort_id, seen, sorted_cohort_ids) + + return sorted_cohort_ids diff --git a/posthog/models/experiment.py b/posthog/models/experiment.py index ea970c5b2db12..74e631b6fab8c 100644 --- a/posthog/models/experiment.py +++ b/posthog/models/experiment.py @@ -23,8 +23,8 @@ class Experiment(models.Model): # A list of filters for secondary metrics secondary_metrics: models.JSONField = models.JSONField(default=list, null=True) + created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True) feature_flag: models.ForeignKey = models.ForeignKey("FeatureFlag", blank=False, on_delete=models.RESTRICT) - created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.CASCADE) start_date: models.DateTimeField = models.DateTimeField(null=True) end_date: models.DateTimeField = models.DateTimeField(null=True) created_at: models.DateTimeField = models.DateTimeField(default=timezone.now) diff --git a/posthog/models/feature_flag/feature_flag.py b/posthog/models/feature_flag/feature_flag.py index 97d5d3e1aace8..c339abe44d0ed 100644 --- a/posthog/models/feature_flag/feature_flag.py +++ b/posthog/models/feature_flag/feature_flag.py @@ -37,7 +37,7 @@ class Meta: rollout_percentage: models.IntegerField = models.IntegerField(null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) - created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.CASCADE) + created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True) created_at: models.DateTimeField = models.DateTimeField(default=timezone.now) deleted: models.BooleanField = models.BooleanField(default=False) active: models.BooleanField = models.BooleanField(default=True) @@ -134,7 +134,7 @@ def get_filters(self): def transform_cohort_filters_for_easy_evaluation( self, using_database: str = "default", - seen_cohorts_cache: Optional[Dict[str, Cohort]] = None, + seen_cohorts_cache: Optional[Dict[int, Cohort]] = None, ): """ Expands cohort filters into person property filters when possible. @@ -168,18 +168,17 @@ def transform_cohort_filters_for_easy_evaluation( for prop in props: if prop.get("type") == "cohort": cohort_condition = True - cohort_id = prop.get("value") + cohort_id = int(prop.get("value")) if cohort_id: if len(props) > 1: # We cannot expand this cohort condition if it's not the only property in its group. return self.conditions try: - parsed_cohort_id = str(cohort_id) - if parsed_cohort_id in seen_cohorts_cache: - cohort = seen_cohorts_cache[parsed_cohort_id] + if cohort_id in seen_cohorts_cache: + cohort = seen_cohorts_cache[cohort_id] else: cohort = Cohort.objects.using(using_database).get(pk=cohort_id) - seen_cohorts_cache[parsed_cohort_id] = cohort + seen_cohorts_cache[cohort_id] = cohort except Cohort.DoesNotExist: return self.conditions if not cohort_condition: @@ -259,9 +258,10 @@ def transform_cohort_filters_for_easy_evaluation( def get_cohort_ids( self, using_database: str = "default", - seen_cohorts_cache: Optional[Dict[str, Cohort]] = None, + seen_cohorts_cache: Optional[Dict[int, Cohort]] = None, + sort_by_topological_order=False, ) -> List[int]: - from posthog.models.cohort.util import get_dependent_cohorts + from posthog.models.cohort.util import get_dependent_cohorts, sort_cohorts_topologically if seen_cohorts_cache is None: seen_cohorts_cache = {} @@ -271,14 +271,13 @@ def get_cohort_ids( props = condition.get("properties", []) for prop in props: if prop.get("type") == "cohort": - cohort_id = prop.get("value") + cohort_id = int(prop.get("value")) try: - parsed_cohort_id = str(cohort_id) - if parsed_cohort_id in seen_cohorts_cache: - cohort: Cohort = seen_cohorts_cache[parsed_cohort_id] + if cohort_id in seen_cohorts_cache: + cohort: Cohort = seen_cohorts_cache[cohort_id] else: cohort = Cohort.objects.using(using_database).get(pk=cohort_id) - seen_cohorts_cache[parsed_cohort_id] = cohort + seen_cohorts_cache[cohort_id] = cohort cohort_ids.add(cohort.pk) cohort_ids.update( @@ -293,6 +292,8 @@ def get_cohort_ids( ) except Cohort.DoesNotExist: continue + if sort_by_topological_order: + return sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) return list(cohort_ids) diff --git a/posthog/models/feature_flag/flag_matching.py b/posthog/models/feature_flag/flag_matching.py index 9a2722b86fc59..d81f44bd61807 100644 --- a/posthog/models/feature_flag/flag_matching.py +++ b/posthog/models/feature_flag/flag_matching.py @@ -138,6 +138,7 @@ def __init__( property_value_overrides: Dict[str, Union[str, int]] = {}, group_property_value_overrides: Dict[str, Dict[str, Union[str, int]]] = {}, skip_database_flags: bool = False, + cohorts_cache: Optional[Dict[int, Cohort]] = None, ): self.feature_flags = feature_flags self.distinct_id = distinct_id @@ -147,7 +148,11 @@ def __init__( self.property_value_overrides = property_value_overrides self.group_property_value_overrides = group_property_value_overrides self.skip_database_flags = skip_database_flags - self.cohorts_cache: Dict[int, Cohort] = {} + + if cohorts_cache is None: + self.cohorts_cache = {} + else: + self.cohorts_cache = cohorts_cache def get_match(self, feature_flag: FeatureFlag) -> FeatureFlagMatch: # If aggregating flag by groups and relevant group type is not passed - flag is off! @@ -481,7 +486,8 @@ def condition_eval(key, condition): group_fields, ) - if any(feature_flag.uses_cohorts for feature_flag in self.feature_flags): + # only fetch all cohorts if not passed in any cached cohorts + if not self.cohorts_cache and any(feature_flag.uses_cohorts for feature_flag in self.feature_flags): all_cohorts = { cohort.pk: cohort for cohort in Cohort.objects.using(DATABASE_FOR_FLAG_MATCHING).filter( @@ -582,6 +588,10 @@ def can_compute_locally( self.cache.group_type_index_to_name[group_type_index], {} ) for property in properties: + # can't locally compute if property is a cohort + # need to atleast fetch the cohort + if property.type == "cohort": + return False if property.key not in target_properties: return False if property.operator == "is_not_set": @@ -602,19 +612,24 @@ def get_highest_priority_match_evaluation( def get_feature_flag_hash_key_overrides( - team_id: int, distinct_ids: List[str], using_database: str = "default" + team_id: int, + distinct_ids: List[str], + using_database: str = "default", + person_id_to_distinct_id_mapping: Optional[Dict[int, str]] = None, ) -> Dict[str, str]: feature_flag_to_key_overrides = {} # Priority to the first distinctID's values, to keep this function deterministic - person_and_distinct_ids = list( - PersonDistinctId.objects.using(using_database) - .filter(distinct_id__in=distinct_ids, team_id=team_id) - .values_list("person_id", "distinct_id") - ) - - person_id_to_distinct_id = {person_id: distinct_id for person_id, distinct_id in person_and_distinct_ids} + if not person_id_to_distinct_id_mapping: + person_and_distinct_ids = list( + PersonDistinctId.objects.using(using_database) + .filter(distinct_id__in=distinct_ids, team_id=team_id) + .values_list("person_id", "distinct_id") + ) + person_id_to_distinct_id = {person_id: distinct_id for person_id, distinct_id in person_and_distinct_ids} + else: + person_id_to_distinct_id = person_id_to_distinct_id_mapping person_ids = list(person_id_to_distinct_id.keys()) diff --git a/posthog/models/feature_flag/permissions.py b/posthog/models/feature_flag/permissions.py index 95d39636c4c07..8f766b4fccc60 100644 --- a/posthog/models/feature_flag/permissions.py +++ b/posthog/models/feature_flag/permissions.py @@ -12,7 +12,7 @@ def can_user_edit_feature_flag(request, feature_flag): else: if not request.user.organization.is_feature_available(AvailableFeature.ROLE_BASED_ACCESS): return True - if feature_flag.created_by == request.user: + if hasattr(feature_flag, "created_by") and feature_flag.created_by and feature_flag.created_by == request.user: return True if ( request.user.organization_memberships.get(organization=request.user.organization).level diff --git a/posthog/models/filters/test/__snapshots__/test_filter.ambr b/posthog/models/filters/test/__snapshots__/test_filter.ambr index a7300c5402d2c..9985054fff217 100644 --- a/posthog/models/filters/test/__snapshots__/test_filter.ambr +++ b/posthog/models/filters/test/__snapshots__/test_filter.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -106,7 +107,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -163,7 +165,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -220,7 +223,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -277,7 +281,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 diff --git a/posthog/models/organization.py b/posthog/models/organization.py index 4e1c7af79838c..461c5f777f568 100644 --- a/posthog/models/organization.py +++ b/posthog/models/organization.py @@ -24,12 +24,7 @@ from posthog.cloud_utils import is_cloud from posthog.constants import MAX_SLUG_LENGTH, AvailableFeature from posthog.email import is_email_available -from posthog.models.utils import ( - LowercaseSlugField, - UUIDModel, - create_with_slug, - sane_repr, -) +from posthog.models.utils import LowercaseSlugField, UUIDModel, create_with_slug, sane_repr from posthog.redis import get_client from posthog.utils import absolute_uri @@ -53,6 +48,7 @@ class OrganizationUsageResource(TypedDict): class OrganizationUsageInfo(TypedDict): events: Optional[OrganizationUsageResource] recordings: Optional[OrganizationUsageResource] + rows_synced: Optional[OrganizationUsageResource] period: Optional[List[str]] @@ -415,3 +411,19 @@ def ensure_organization_membership_consistency(sender, instance: OrganizationMem save_user = True if save_user: instance.user.save() + + +@receiver(models.signals.pre_save, sender=OrganizationMembership) +def organization_membership_saved(sender: Any, instance: OrganizationMembership, **kwargs: Any) -> None: + from posthog.event_usage import report_user_organization_membership_level_changed + + try: + old_instance = OrganizationMembership.objects.get(id=instance.id) + if old_instance.level != instance.level: + # the level has been changed + report_user_organization_membership_level_changed( + instance.user, instance.organization, instance.level, old_instance.level + ) + except OrganizationMembership.DoesNotExist: + # The instance is new, or we are setting up test data + pass diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index 2f5654e0f039a..d03799ad2343b 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -247,6 +247,7 @@ def aggregate_users_by_distinct_id(self) -> bool: event_properties_with_usage: models.JSONField = models.JSONField(default=list, blank=True) event_properties_numerical: models.JSONField = models.JSONField(default=list, blank=True) external_data_workspace_id: models.CharField = models.CharField(max_length=400, null=True, blank=True) + external_data_workspace_last_synced_at: models.DateTimeField = models.DateTimeField(null=True, blank=True) objects: TeamManager = TeamManager() diff --git a/posthog/models/test/test_organization_model.py b/posthog/models/test/test_organization_model.py index f140dcc862f26..8c35602a64be5 100644 --- a/posthog/models/test/test_organization_model.py +++ b/posthog/models/test/test_organization_model.py @@ -1,8 +1,10 @@ from unittest import mock +from unittest.mock import patch from django.utils import timezone from posthog.models import Organization, OrganizationInvite, Plugin +from posthog.models.organization import OrganizationMembership from posthog.plugins.test.mock import mocked_plugin_requests_get from posthog.plugins.test.plugin_archives import HELLO_WORLD_PLUGIN_GITHUB_ZIP from posthog.test.base import BaseTest @@ -77,3 +79,28 @@ def test_update_available_features_ignored_if_usage_info_exists(self): new_org.usage = {"events": {"usage": 1000, "limit": None}} new_org.update_available_features() assert new_org.available_features == ["test1", "test2"] + + +class TestOrganizationMembership(BaseTest): + @patch("posthoganalytics.capture") + def test_event_sent_when_membership_level_changed( + self, + mock_capture, + ): + user = self._create_user("user1") + organization = Organization.objects.create(name="Test Org") + membership = OrganizationMembership.objects.create(user=user, organization=organization, level=1) + mock_capture.assert_not_called() + # change the level + membership.level = 15 + membership.save() + # check that the event was sent + mock_capture.assert_called_once_with( + user.distinct_id, + "membership level changed", + properties={ + "new_level": 15, + "previous_level": 1, + }, + groups=mock.ANY, + ) diff --git a/posthog/models/test/test_user_model.py b/posthog/models/test/test_user_model.py index fe26931522eac..9c07f36b16466 100644 --- a/posthog/models/test/test_user_model.py +++ b/posthog/models/test/test_user_model.py @@ -10,6 +10,7 @@ def test_create_user_with_distinct_id(self): self.assertNotEqual(user.distinct_id, None) def test_analytics_metadata(self): + self.maxDiff = None # One org, one team, anonymized organization, team, user = User.objects.bootstrap( organization_name="Test Org", @@ -32,6 +33,7 @@ def test_analytics_metadata(self): "team_member_count_all": 1, "completed_onboarding_once": False, "organization_id": str(organization.id), + "current_organization_membership_level": 15, "project_id": str(team.uuid), "project_setup_complete": False, "has_password_set": True, @@ -67,6 +69,7 @@ def test_analytics_metadata(self): "team_member_count_all": 2, "completed_onboarding_once": True, "organization_id": str(self.organization.id), + "current_organization_membership_level": 1, "project_id": str(self.team.uuid), "project_setup_complete": True, "has_password_set": True, diff --git a/posthog/models/user.py b/posthog/models/user.py index 423936747e2cc..b25c12776fb1b 100644 --- a/posthog/models/user.py +++ b/posthog/models/user.py @@ -1,14 +1,5 @@ from functools import cached_property -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Tuple, - Type, - TypedDict, -) +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypedDict from django.contrib.auth.models import AbstractUser, BaseUserManager from django.db import models, transaction @@ -237,6 +228,8 @@ def join( # We don't need to check for ExplicitTeamMembership as none can exist for a completely new member self.current_team = organization.teams.order_by("id").filter(access_control=False).first() self.save() + if level == OrganizationMembership.Level.OWNER and not self.current_organization.customer_id: + self.update_billing_customer_email(organization) self.update_billing_distinct_ids(organization) return membership @@ -268,6 +261,12 @@ def update_billing_distinct_ids(self, organization: Organization) -> None: if is_cloud() and get_cached_instance_license() is not None: BillingManager(get_cached_instance_license()).update_billing_distinct_ids(organization) + def update_billing_customer_email(self, organization: Organization) -> None: + from ee.billing.billing_manager import BillingManager # avoid circular import + + if is_cloud() and get_cached_instance_license() is not None: + BillingManager(get_cached_instance_license()).update_billing_customer_email(organization) + def get_analytics_metadata(self): team_member_count_all: int = ( OrganizationMembership.objects.filter(organization__in=self.organizations.all()) @@ -276,6 +275,10 @@ def get_analytics_metadata(self): .count() ) + current_organization_membership = None + if self.organization: + current_organization_membership = self.organization.memberships.filter(user=self).first() + project_setup_complete = False if self.team and self.team.completed_snippet_onboarding and self.team.ingested_event: project_setup_complete = True @@ -294,6 +297,9 @@ def get_analytics_metadata(self): ).exists(), # has completed the onboarding at least for one project # properties dependent on current project / org below "organization_id": str(self.organization.id) if self.organization else None, + "current_organization_membership_level": current_organization_membership.level + if current_organization_membership + else None, "project_id": str(self.team.uuid) if self.team else None, "project_setup_complete": project_setup_complete, "joined_at": self.date_joined, diff --git a/posthog/ph_client.py b/posthog/ph_client.py new file mode 100644 index 0000000000000..e81161a59d470 --- /dev/null +++ b/posthog/ph_client.py @@ -0,0 +1,27 @@ +from posthog.utils import get_instance_region +from posthog.cloud_utils import is_cloud + + +def get_ph_client(): + from posthoganalytics import Posthog + + if not is_cloud(): + return + + # send EU data to EU, US data to US + api_key = None + host = None + region = get_instance_region() + if region == "EU": + api_key = "phc_dZ4GK1LRjhB97XozMSkEwPXx7OVANaJEwLErkY1phUF" + host = "https://eu.posthog.com" + elif region == "US": + api_key = "sTMFPsFhdP1Ssg" + host = "https://app.posthog.com" + + if not api_key: + return + + ph_client = Posthog(api_key, host=host) + + return ph_client diff --git a/posthog/plugins/reload.py b/posthog/plugins/reload.py index aa3d559ed01d0..552b1e5b6bd4a 100644 --- a/posthog/plugins/reload.py +++ b/posthog/plugins/reload.py @@ -8,4 +8,4 @@ def reload_plugins_on_workers(): logger.info("Reloading plugins on workers") - get_client().publish(settings.PLUGINS_RELOAD_PUBSUB_CHANNEL, "reload!") + get_client(settings.PLUGINS_RELOAD_REDIS_URL).publish(settings.PLUGINS_RELOAD_PUBSUB_CHANNEL, "reload!") diff --git a/posthog/queries/test/test_trends.py b/posthog/queries/test/test_trends.py index 63b7024d3d6bf..2dfe50e24b7d9 100644 --- a/posthog/queries/test/test_trends.py +++ b/posthog/queries/test/test_trends.py @@ -474,14 +474,14 @@ def test_trends_breakdown_cumulative(self): self.team, ) - self.assertEqual(response[0]["label"], "sign up - none") + self.assertEqual(response[0]["label"], "none") self.assertEqual(response[0]["labels"][4], "1-Jan-2020") self.assertEqual(response[0]["data"], [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]) - self.assertEqual(response[1]["label"], "sign up - other_value") + self.assertEqual(response[1]["label"], "other_value") self.assertEqual(response[1]["data"], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]) - self.assertEqual(response[2]["label"], "sign up - value") + self.assertEqual(response[2]["label"], "value") self.assertEqual(response[2]["data"], [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]) def test_trends_single_aggregate_dau(self): @@ -919,13 +919,14 @@ def test_trends_breakdown_single_aggregate_cohorts(self): ) for result in event_response: - if result["label"] == "sign up - cohort1": + if result["label"] == "cohort1": self.assertEqual(result["aggregated_value"], 2) - elif result["label"] == "sign up - cohort2": + elif result["label"] == "cohort2": self.assertEqual(result["aggregated_value"], 2) - elif result["label"] == "sign up - cohort3": + elif result["label"] == "cohort3": self.assertEqual(result["aggregated_value"], 3) else: + self.assertEqual(result["label"], "all users") self.assertEqual(result["aggregated_value"], 7) def test_trends_breakdown_single_aggregate(self): @@ -3869,7 +3870,7 @@ def test_breakdown_by_empty_cohort(self): self.team, ) - self.assertEqual(event_response[0]["label"], "$pageview - all users") + self.assertEqual(event_response[0]["label"], "all users") self.assertEqual(sum(event_response[0]["data"]), 1) @also_test_with_person_on_events_v2 @@ -3935,15 +3936,15 @@ def test_breakdown_by_cohort(self): counts[res["label"]] = sum(res["data"]) break_val[res["label"]] = res["breakdown_value"] - self.assertEqual(counts["watched movie - cohort1"], 1) - self.assertEqual(counts["watched movie - cohort2"], 3) - self.assertEqual(counts["watched movie - cohort3"], 4) - self.assertEqual(counts["watched movie - all users"], 7) + self.assertEqual(counts["cohort1"], 1) + self.assertEqual(counts["cohort2"], 3) + self.assertEqual(counts["cohort3"], 4) + self.assertEqual(counts["all users"], 7) - self.assertEqual(break_val["watched movie - cohort1"], cohort.pk) - self.assertEqual(break_val["watched movie - cohort2"], cohort2.pk) - self.assertEqual(break_val["watched movie - cohort3"], cohort3.pk) - self.assertEqual(break_val["watched movie - all users"], "all") + self.assertEqual(break_val["cohort1"], cohort.pk) + self.assertEqual(break_val["cohort2"], cohort2.pk) + self.assertEqual(break_val["cohort3"], cohort3.pk) + self.assertEqual(break_val["all users"], "all") self.assertEntityResponseEqual(event_response, action_response) @@ -4085,7 +4086,7 @@ def test_breakdown_by_person_property(self): for response in event_response: if response["breakdown_value"] == "person1": self.assertEqual(response["count"], 1) - self.assertEqual(response["label"], "watched movie - person1") + self.assertEqual(response["label"], "person1") if response["breakdown_value"] == "person2": self.assertEqual(response["count"], 3) if response["breakdown_value"] == "person3": @@ -4126,7 +4127,7 @@ def test_breakdown_by_person_property_for_person_on_events(self): for response in event_response: if response["breakdown_value"] == "person1": self.assertEqual(response["count"], 1) - self.assertEqual(response["label"], "watched movie - person1") + self.assertEqual(response["label"], "person1") if response["breakdown_value"] == "person2": self.assertEqual(response["count"], 3) if response["breakdown_value"] == "person3": @@ -4666,9 +4667,9 @@ def test_trends_aggregate_by_distinct_id(self): self.team, ) self.assertEqual(daily_response[0]["data"][0], 2) - self.assertEqual(daily_response[0]["label"], "sign up - some_val") + self.assertEqual(daily_response[0]["label"], "some_val") self.assertEqual(daily_response[1]["data"][0], 1) - self.assertEqual(daily_response[1]["label"], "sign up - none") + self.assertEqual(daily_response[1]["label"], "none") # MAU with freeze_time("2019-12-31T13:00:01Z"): @@ -4809,8 +4810,8 @@ def test_breakdown_filtering(self): ) self.assertEqual(response[0]["label"], "sign up - none") - self.assertEqual(response[2]["label"], "sign up - other_value") self.assertEqual(response[1]["label"], "sign up - value") + self.assertEqual(response[2]["label"], "sign up - other_value") self.assertEqual(response[3]["label"], "no events - none") self.assertEqual(sum(response[0]["data"]), 2) @@ -4869,9 +4870,9 @@ def test_breakdown_filtering_persons(self): ), self.team, ) - self.assertEqual(response[0]["label"], "sign up - none") - self.assertEqual(response[1]["label"], "sign up - test@gmail.com") - self.assertEqual(response[2]["label"], "sign up - test@posthog.com") + self.assertEqual(response[0]["label"], "none") + self.assertEqual(response[1]["label"], "test@gmail.com") + self.assertEqual(response[2]["label"], "test@posthog.com") self.assertEqual(response[0]["count"], 1) self.assertEqual(response[1]["count"], 1) @@ -4927,9 +4928,9 @@ def test_breakdown_filtering_persons_with_action_props(self): ), self.team, ) - self.assertEqual(response[0]["label"], "sign up - none") - self.assertEqual(response[1]["label"], "sign up - test@gmail.com") - self.assertEqual(response[2]["label"], "sign up - test@posthog.com") + self.assertEqual(response[0]["label"], "none") + self.assertEqual(response[1]["label"], "test@gmail.com") + self.assertEqual(response[2]["label"], "test@posthog.com") self.assertEqual(response[0]["count"], 1) self.assertEqual(response[1]["count"], 1) @@ -5003,8 +5004,8 @@ def test_breakdown_filtering_with_properties(self): ) response = sorted(response, key=lambda x: x["label"]) - self.assertEqual(response[0]["label"], "sign up - first url") - self.assertEqual(response[1]["label"], "sign up - second url") + self.assertEqual(response[0]["label"], "first url") + self.assertEqual(response[1]["label"], "second url") self.assertEqual(sum(response[0]["data"]), 1) self.assertEqual(response[0]["breakdown_value"], "first url") @@ -5086,7 +5087,7 @@ def test_breakdown_filtering_with_properties_in_new_format(self): ) response = sorted(response, key=lambda x: x["label"]) - self.assertEqual(response[0]["label"], "sign up - second url") + self.assertEqual(response[0]["label"], "second url") self.assertEqual(sum(response[0]["data"]), 1) self.assertEqual(response[0]["breakdown_value"], "second url") @@ -5170,8 +5171,8 @@ def test_mau_with_breakdown_filtering_and_prop_filter(self): self.team, ) - self.assertEqual(event_response[0]["label"], "sign up - some_val") - self.assertEqual(event_response[1]["label"], "sign up - some_val2") + self.assertEqual(event_response[0]["label"], "some_val") + self.assertEqual(event_response[1]["label"], "some_val2") self.assertEqual(sum(event_response[0]["data"]), 2) self.assertEqual(event_response[0]["data"][5], 1) @@ -5211,8 +5212,8 @@ def test_dau_with_breakdown_filtering(self): self.team, ) - self.assertEqual(event_response[1]["label"], "sign up - other_value") - self.assertEqual(event_response[2]["label"], "sign up - value") + self.assertEqual(event_response[1]["label"], "other_value") + self.assertEqual(event_response[2]["label"], "value") self.assertEqual(sum(event_response[1]["data"]), 1) self.assertEqual(event_response[1]["data"][5], 1) @@ -5256,8 +5257,8 @@ def test_dau_with_breakdown_filtering_with_sampling(self): self.team, ) - self.assertEqual(event_response[1]["label"], "sign up - other_value") - self.assertEqual(event_response[2]["label"], "sign up - value") + self.assertEqual(event_response[1]["label"], "other_value") + self.assertEqual(event_response[2]["label"], "value") self.assertEqual(sum(event_response[1]["data"]), 1) self.assertEqual(event_response[1]["data"][5], 1) @@ -5301,7 +5302,7 @@ def test_dau_with_breakdown_filtering_with_prop_filter(self): self.team, ) - self.assertEqual(event_response[0]["label"], "sign up - other_value") + self.assertEqual(event_response[0]["label"], "other_value") self.assertEqual(sum(event_response[0]["data"]), 1) self.assertEqual(event_response[0]["data"][5], 1) # property not defined diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index e891190f6e310..458aabdc14198 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -676,7 +676,11 @@ def _breakdown_result_descriptors(self, breakdown_value, filter: Filter, entity: extra_label = self._determine_breakdown_label( breakdown_value, filter.breakdown_type, filter.breakdown, breakdown_value ) - label = "{} - {}".format(entity.name, extra_label) + if len(filter.entities) > 1: + # if there are multiple entities in the query, include the entity name in the labels + label = "{} - {}".format(entity.name, extra_label) + else: + label = extra_label additional_values = {"label": label} if filter.breakdown_type == "cohort": additional_values["breakdown_value"] = "all" if breakdown_value == ALL_USERS_COHORT_ID else breakdown_value diff --git a/posthog/schema.py b/posthog/schema.py index 3b86559f8fc78..46d107122cd8e 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -3,6 +3,7 @@ from __future__ import annotations +from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional, Union @@ -255,6 +256,13 @@ class InCohortVia(str, Enum): subquery = "subquery" +class MaterializationMode(str, Enum): + auto = "auto" + legacy_null_as_string = "legacy_null_as_string" + legacy_null_as_null = "legacy_null_as_null" + disabled = "disabled" + + class PersonsArgMaxVersion(str, Enum): auto = "auto" v1 = "v1" @@ -273,6 +281,7 @@ class HogQLQueryModifiers(BaseModel): extra="forbid", ) inCohortVia: Optional[InCohortVia] = None + materializationMode: Optional[MaterializationMode] = None personsArgMaxVersion: Optional[PersonsArgMaxVersion] = None personsOnEventsMode: Optional[PersonsOnEventsMode] = None @@ -431,6 +440,23 @@ class PropertyOperator(str, Enum): max = "max" +class QueryStatus(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + complete: Optional[bool] = False + end_time: Optional[datetime] = None + error: Optional[bool] = False + error_message: Optional[str] = "" + expiration_time: Optional[datetime] = None + id: str + query_async: Optional[bool] = True + results: Optional[Any] = None + start_time: Optional[datetime] = None + task_id: Optional[str] = None + team_id: int + + class QueryTiming(BaseModel): model_config = ConfigDict( extra="forbid", @@ -467,57 +493,6 @@ class RetentionType(str, Enum): retention_first_time = "retention_first_time" -class SavedInsightNode(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - allowSorting: Optional[bool] = Field( - default=None, description="Can the user click on column headers to sort the table? (default: true)" - ) - embedded: Optional[bool] = Field(default=None, description="Query is embedded inside another bordered component") - expandable: Optional[bool] = Field( - default=None, description="Can expand row to show raw event data (default: true)" - ) - full: Optional[bool] = Field( - default=None, description="Show with most visual options enabled. Used in insight scene." - ) - hidePersonsModal: Optional[bool] = None - kind: Literal["SavedInsightNode"] = "SavedInsightNode" - propertiesViaUrl: Optional[bool] = Field(default=None, description="Link properties via the URL (default: false)") - shortId: str - showActions: Optional[bool] = Field(default=None, description="Show the kebab menu at the end of the row") - showColumnConfigurator: Optional[bool] = Field( - default=None, description="Show a button to configure the table's columns if possible" - ) - showCorrelationTable: Optional[bool] = None - showDateRange: Optional[bool] = Field(default=None, description="Show date range selector") - showElapsedTime: Optional[bool] = Field(default=None, description="Show the time it takes to run a query") - showEventFilter: Optional[bool] = Field( - default=None, description="Include an event filter above the table (EventsNode only)" - ) - showExport: Optional[bool] = Field(default=None, description="Show the export button") - showFilters: Optional[bool] = None - showHeader: Optional[bool] = None - showHogQLEditor: Optional[bool] = Field(default=None, description="Include a HogQL query editor above HogQL tables") - showLastComputation: Optional[bool] = None - showLastComputationRefresh: Optional[bool] = None - showOpenEditorButton: Optional[bool] = Field( - default=None, description="Show a button to open the current query as a new insight. (default: true)" - ) - showPersistentColumnConfigurator: Optional[bool] = Field( - default=None, description="Show a button to configure and persist the table's default columns if possible" - ) - showPropertyFilter: Optional[bool] = Field(default=None, description="Include a property filter above the table") - showReload: Optional[bool] = Field(default=None, description="Show a reload button") - showResults: Optional[bool] = None - showResultsTable: Optional[bool] = Field(default=None, description="Show a results table") - showSavedQueries: Optional[bool] = Field(default=None, description="Shows a list of saved queries") - showSearch: Optional[bool] = Field(default=None, description="Include a free text search field (PersonsNode only)") - showTable: Optional[bool] = None - showTimings: Optional[bool] = Field(default=None, description="Show a detailed query timing breakdown") - suppressSessionAnalysisWarning: Optional[bool] = None - - class SessionPropertyFilter(BaseModel): model_config = ConfigDict( extra="forbid", @@ -596,6 +571,7 @@ class TrendsFilter(BaseModel): display: Optional[ChartDisplayType] = None formula: Optional[str] = None hidden_legend_indexes: Optional[List[float]] = None + show_labels_on_series: Optional[bool] = None show_legend: Optional[bool] = None show_percent_stack_view: Optional[bool] = None show_values_on_series: Optional[bool] = None @@ -614,6 +590,31 @@ class TrendsQueryResponse(BaseModel): timings: Optional[List[QueryTiming]] = None +class ActionsPie(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + disableHoverOffset: Optional[bool] = None + hideAggregation: Optional[bool] = None + + +class RETENTION(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + hideLineGraph: Optional[bool] = None + hideSizeColumn: Optional[bool] = None + useSmallLayout: Optional[bool] = None + + +class VizSpecificOptions(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + ActionsPie: Optional[ActionsPie] = None + RETENTION: Optional[RETENTION] = None + + class Kind(str, Enum): unit = "unit" duration_s = "duration_s" @@ -922,6 +923,58 @@ class RetentionFilter(BaseModel): total_intervals: Optional[float] = None +class SavedInsightNode(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + allowSorting: Optional[bool] = Field( + default=None, description="Can the user click on column headers to sort the table? (default: true)" + ) + embedded: Optional[bool] = Field(default=None, description="Query is embedded inside another bordered component") + expandable: Optional[bool] = Field( + default=None, description="Can expand row to show raw event data (default: true)" + ) + full: Optional[bool] = Field( + default=None, description="Show with most visual options enabled. Used in insight scene." + ) + hidePersonsModal: Optional[bool] = None + kind: Literal["SavedInsightNode"] = "SavedInsightNode" + propertiesViaUrl: Optional[bool] = Field(default=None, description="Link properties via the URL (default: false)") + shortId: str + showActions: Optional[bool] = Field(default=None, description="Show the kebab menu at the end of the row") + showColumnConfigurator: Optional[bool] = Field( + default=None, description="Show a button to configure the table's columns if possible" + ) + showCorrelationTable: Optional[bool] = None + showDateRange: Optional[bool] = Field(default=None, description="Show date range selector") + showElapsedTime: Optional[bool] = Field(default=None, description="Show the time it takes to run a query") + showEventFilter: Optional[bool] = Field( + default=None, description="Include an event filter above the table (EventsNode only)" + ) + showExport: Optional[bool] = Field(default=None, description="Show the export button") + showFilters: Optional[bool] = None + showHeader: Optional[bool] = None + showHogQLEditor: Optional[bool] = Field(default=None, description="Include a HogQL query editor above HogQL tables") + showLastComputation: Optional[bool] = None + showLastComputationRefresh: Optional[bool] = None + showOpenEditorButton: Optional[bool] = Field( + default=None, description="Show a button to open the current query as a new insight. (default: true)" + ) + showPersistentColumnConfigurator: Optional[bool] = Field( + default=None, description="Show a button to configure and persist the table's default columns if possible" + ) + showPropertyFilter: Optional[bool] = Field(default=None, description="Include a property filter above the table") + showReload: Optional[bool] = Field(default=None, description="Show a reload button") + showResults: Optional[bool] = None + showResultsTable: Optional[bool] = Field(default=None, description="Show a results table") + showSavedQueries: Optional[bool] = Field(default=None, description="Shows a list of saved queries") + showSearch: Optional[bool] = Field(default=None, description="Include a free text search field (PersonsNode only)") + showTable: Optional[bool] = None + showTimings: Optional[bool] = Field(default=None, description="Show a detailed query timing breakdown") + suppressSessionAnalysisWarning: Optional[bool] = None + vizSpecificOptions: Optional[VizSpecificOptions] = None + + class SessionsTimelineQueryResponse(BaseModel): model_config = ConfigDict( extra="forbid", @@ -1439,7 +1492,6 @@ class StickinessQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) - aggregation_group_type_index: Optional[float] = Field(default=None, description="Groups aggregation") dateRange: Optional[DateRange] = Field(default=None, description="Date range for the query") filterTestAccounts: Optional[bool] = Field( default=None, description="Exclude internal and test users by applying the respective filters" @@ -1637,7 +1689,6 @@ class LifecycleQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) - aggregation_group_type_index: Optional[float] = Field(default=None, description="Groups aggregation") dateRange: Optional[DateRange] = Field(default=None, description="Date range for the query") filterTestAccounts: Optional[bool] = Field( default=None, description="Exclude internal and test users by applying the respective filters" @@ -1732,6 +1783,7 @@ class InsightVizNode(BaseModel): showTable: Optional[bool] = None source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] suppressSessionAnalysisWarning: Optional[bool] = None + vizSpecificOptions: Optional[VizSpecificOptions] = None class InsightPersonsQuery(BaseModel): diff --git a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr index 883b68b6b75f1..0afe5e0ac247d 100644 --- a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr +++ b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -106,7 +107,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -163,7 +165,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -220,7 +223,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -277,7 +281,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -356,7 +361,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -487,7 +493,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -681,7 +688,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -753,7 +761,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -810,7 +819,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -867,7 +877,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -924,7 +935,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -981,7 +993,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1038,7 +1051,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1117,7 +1131,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1185,7 +1200,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1264,7 +1280,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1540,7 +1557,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1619,7 +1637,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1908,7 +1927,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1987,7 +2007,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2234,7 +2255,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2321,7 +2343,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2400,7 +2423,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2700,7 +2724,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2779,7 +2804,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2829,7 +2855,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -3545,7 +3572,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -3624,7 +3652,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -3892,7 +3921,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -3982,7 +4012,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -4252,7 +4283,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -4331,7 +4363,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -4614,7 +4647,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -4693,7 +4727,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ diff --git a/posthog/settings/data_stores.py b/posthog/settings/data_stores.py index 9f6f9ca74cab8..f3402a748111f 100644 --- a/posthog/settings/data_stores.py +++ b/posthog/settings/data_stores.py @@ -280,6 +280,12 @@ def _parse_kafka_hosts(hosts_string: str) -> List[str]: # so that we don't have to worry about changing config. REDIS_READER_URL = os.getenv("REDIS_READER_URL", None) +# Ingestion is now using a separate Redis cluster for better resource isolation. +# Django and plugin-server currently communicate via the reload-plugins Redis +# pubsub channel, pushed to when plugin configs change. +# We should move away to a different communication channel and remove this. +PLUGINS_RELOAD_REDIS_URL = os.getenv("PLUGINS_RELOAD_REDIS_URL", REDIS_URL) + CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", diff --git a/posthog/settings/ingestion.py b/posthog/settings/ingestion.py index bab3d7f2506ae..b559f5726ca29 100644 --- a/posthog/settings/ingestion.py +++ b/posthog/settings/ingestion.py @@ -36,4 +36,7 @@ NEW_ANALYTICS_CAPTURE_ENDPOINT = os.getenv("NEW_CAPTURE_ENDPOINT", "/i/v0/e/") NEW_ANALYTICS_CAPTURE_TEAM_IDS = get_set(os.getenv("NEW_ANALYTICS_CAPTURE_TEAM_IDS", "")) +NEW_ANALYTICS_CAPTURE_EXCLUDED_TEAM_IDS = get_set(os.getenv("NEW_ANALYTICS_CAPTURE_EXCLUDED_TEAM_IDS", "")) NEW_ANALYTICS_CAPTURE_SAMPLING_RATE = get_from_env("NEW_ANALYTICS_CAPTURE_SAMPLING_RATE", type_cast=float, default=1.0) + +ELEMENT_CHAIN_AS_STRING_TEAMS = get_set(os.getenv("ELEMENT_CHAIN_AS_STRING_TEAMS", "")) diff --git a/posthog/tasks/calculate_cohort.py b/posthog/tasks/calculate_cohort.py index 1c4492071c78a..066469636dc37 100644 --- a/posthog/tasks/calculate_cohort.py +++ b/posthog/tasks/calculate_cohort.py @@ -71,3 +71,10 @@ def insert_cohort_from_insight_filter(cohort_id: int, filter_data: Dict[str, Any insert_cohort_actors_into_ch(cohort, filter_data) insert_cohort_people_into_pg(cohort=cohort) + + +@shared_task(ignore_result=True, max_retries=1) +def insert_cohort_from_feature_flag(cohort_id: int, flag_key: str, team_id: int) -> None: + from posthog.api.cohort import get_cohort_actors_for_feature_flag + + get_cohort_actors_for_feature_flag(cohort_id, flag_key, team_id) diff --git a/posthog/tasks/exports/csv_exporter.py b/posthog/tasks/exports/csv_exporter.py index 622798774ec1d..8f6fffd0c9f90 100644 --- a/posthog/tasks/exports/csv_exporter.py +++ b/posthog/tasks/exports/csv_exporter.py @@ -7,7 +7,7 @@ from django.http import QueryDict from sentry_sdk import capture_exception, push_scope -from posthog.api.query import process_query +from posthog.api.services.query import process_query from posthog.jwt import PosthogJwtAudience, encode_jwt from posthog.models.exported_asset import ExportedAsset, save_content from posthog.utils import absolute_uri diff --git a/posthog/tasks/test/__snapshots__/test_usage_report.ambr b/posthog/tasks/test/__snapshots__/test_usage_report.ambr index 74f71be82a5cc..9cabd193acff2 100644 --- a/posthog/tasks/test/__snapshots__/test_usage_report.ambr +++ b/posthog/tasks/test/__snapshots__/test_usage_report.ambr @@ -255,6 +255,24 @@ GROUP BY team_id ' --- +# name: TestFeatureFlagsUsageReport.test_usage_report_decide_requests.24 + ' + + SELECT team, + sum(rows_synced) + FROM + (SELECT JSONExtractString(properties, 'job_id') AS job_id, + distinct_id AS team, + any(JSONExtractInt(properties, 'count')) AS rows_synced + FROM events + WHERE team_id = 2 + AND event = 'external data sync job' + AND parseDateTimeBestEffort(JSONExtractString(properties, 'startTime')) BETWEEN '2022-01-10 00:00:00' AND '2022-01-10 23:59:59' + GROUP BY job_id, + team) + GROUP BY team + ' +--- # name: TestFeatureFlagsUsageReport.test_usage_report_decide_requests.3 ' diff --git a/posthog/tasks/test/test_usage_report.py b/posthog/tasks/test/test_usage_report.py index 715c3829855d2..a10a16e17893a 100644 --- a/posthog/tasks/test/test_usage_report.py +++ b/posthog/tasks/test/test_usage_report.py @@ -399,6 +399,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, "date": "2022-01-09", "organization_id": str(self.organization.id), "organization_name": "Test", @@ -440,6 +441,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, }, str(self.org_1_team_2.id): { "event_count_lifetime": 11, @@ -475,6 +477,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, }, }, }, @@ -533,6 +536,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, "date": "2022-01-09", "organization_id": str(self.org_2.id), "organization_name": "Org 2", @@ -574,6 +578,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, } }, }, @@ -980,6 +985,95 @@ def test_usage_report_survey_responses(self, billing_task_mock: MagicMock, posth assert org_2_report["teams"]["5"]["survey_responses_count_in_month"] == 7 +@freeze_time("2022-01-10T00:01:00Z") +class TestExternalDataSyncUsageReport(ClickhouseDestroyTablesMixin, TestCase, ClickhouseTestMixin): + def setUp(self) -> None: + Team.objects.all().delete() + return super().setUp() + + def _setup_teams(self) -> None: + self.analytics_org = Organization.objects.create(name="PostHog") + self.org_1 = Organization.objects.create(name="Org 1") + self.org_2 = Organization.objects.create(name="Org 2") + + self.analytics_team = Team.objects.create(pk=2, organization=self.analytics_org, name="Analytics") + + self.org_1_team_1 = Team.objects.create(pk=3, organization=self.org_1, name="Team 1 org 1") + self.org_1_team_2 = Team.objects.create(pk=4, organization=self.org_1, name="Team 2 org 1") + self.org_2_team_3 = Team.objects.create(pk=5, organization=self.org_2, name="Team 3 org 2") + + @patch("posthog.tasks.usage_report.Client") + @patch("posthog.tasks.usage_report.send_report_to_billing_service") + def test_external_data_rows_synced_response( + self, billing_task_mock: MagicMock, posthog_capture_mock: MagicMock + ) -> None: + self._setup_teams() + + for i in range(5): + start_time = (now() - relativedelta(hours=i)).strftime("%Y-%m-%dT%H:%M:%SZ") + _create_event( + distinct_id="3", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "startTime": start_time, + }, + timestamp=now() - relativedelta(hours=i), + team=self.analytics_team, + ) + # identical job id should be deduped and not counted + _create_event( + distinct_id="3", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "startTime": start_time, + }, + timestamp=now() - relativedelta(hours=i, minutes=i), + team=self.analytics_team, + ) + + for i in range(5): + _create_event( + distinct_id="4", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "startTime": (now() - relativedelta(hours=i)).strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + timestamp=now() - relativedelta(hours=i), + team=self.analytics_team, + ) + + flush_persons_and_events() + + period = get_previous_day(at=now() + relativedelta(days=1)) + period_start, period_end = period + all_reports = _get_all_org_reports(period_start, period_end) + + assert len(all_reports) == 3 + + org_1_report = _get_full_org_usage_report_as_dict( + _get_full_org_usage_report(all_reports[str(self.org_1.id)], get_instance_metadata(period)) + ) + + org_2_report = _get_full_org_usage_report_as_dict( + _get_full_org_usage_report(all_reports[str(self.org_2.id)], get_instance_metadata(period)) + ) + + assert org_1_report["organization_name"] == "Org 1" + assert org_1_report["rows_synced_in_period"] == 20 + + assert org_1_report["teams"]["3"]["rows_synced_in_period"] == 10 + assert org_1_report["teams"]["4"]["rows_synced_in_period"] == 10 + + assert org_2_report["organization_name"] == "Org 2" + assert org_2_report["rows_synced_in_period"] == 0 + + class SendUsageTest(LicensedTestMixin, ClickhouseDestroyTablesMixin, APIBaseTest): def setUp(self) -> None: super().setUp() @@ -1039,6 +1133,10 @@ def _usage_report_response(self) -> Any: "usage": 1000, "limit": None, }, + "rows_synced": { + "usage": 1000, + "limit": None, + }, }, } } @@ -1185,6 +1283,7 @@ def test_org_usage_updated_correctly(self, mock_post: MagicMock, mock_client: Ma assert self.team.organization.usage == { "events": {"limit": None, "usage": 10000, "todays_usage": 0}, "recordings": {"limit": None, "usage": 1000, "todays_usage": 0}, + "rows_synced": {"limit": None, "usage": 1000, "todays_usage": 0}, "period": ["2021-10-01T00:00:00Z", "2021-10-31T00:00:00Z"], } diff --git a/posthog/tasks/test/test_warehouse.py b/posthog/tasks/test/test_warehouse.py new file mode 100644 index 0000000000000..20b669b754995 --- /dev/null +++ b/posthog/tasks/test/test_warehouse.py @@ -0,0 +1,167 @@ +from posthog.test.base import APIBaseTest +import datetime +from unittest.mock import patch, MagicMock +from posthog.tasks.warehouse import ( + _traverse_jobs_by_field, + capture_workspace_rows_synced_by_team, + check_external_data_source_billing_limit_by_team, +) +from posthog.warehouse.models import ExternalDataSource +from freezegun import freeze_time + + +class TestWarehouse(APIBaseTest): + @patch("posthog.tasks.warehouse.send_request") + @freeze_time("2023-11-07") + def test_traverse_jobs_by_field(self, send_request_mock: MagicMock) -> None: + send_request_mock.return_value = { + "data": [ + { + "jobId": 5827835, + "status": "succeeded", + "jobType": "sync", + "startTime": "2023-11-07T16:50:49Z", + "connectionId": "fake", + "lastUpdatedAt": "2023-11-07T16:52:54Z", + "duration": "PT2M5S", + "rowsSynced": 93353, + }, + { + "jobId": 5783573, + "status": "succeeded", + "jobType": "sync", + "startTime": "2023-11-05T18:32:41Z", + "connectionId": "fake-2", + "lastUpdatedAt": "2023-11-05T18:35:11Z", + "duration": "PT2M30S", + "rowsSynced": 97747, + }, + ] + } + mock_capture = MagicMock() + response = _traverse_jobs_by_field(mock_capture, self.team, "fake-url", "rowsSynced") + + self.assertEqual( + response, + [ + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + {"count": 97747, "startTime": "2023-11-05T18:32:41Z"}, + ], + ) + + self.assertEqual(mock_capture.capture.call_count, 2) + mock_capture.capture.assert_called_with( + self.team.pk, + "external data sync job", + { + "count": 97747, + "workspace_id": self.team.external_data_workspace_id, + "team_id": self.team.pk, + "team_uuid": self.team.uuid, + "startTime": "2023-11-05T18:32:41Z", + "job_id": "5783573", + }, + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + traverse_jobs_mock.return_value = [ + {"count": 97747, "startTime": "2023-11-05T18:32:41Z"}, + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team_month_cutoff( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + # external_data_workspace_last_synced_at unset + traverse_jobs_mock.return_value = [ + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team_month_cutoff_field_set( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + self.team.external_data_workspace_last_synced_at = datetime.datetime( + 2023, 10, 29, 18, 32, 41, tzinfo=datetime.timezone.utc + ) + self.team.save() + traverse_jobs_mock.return_value = [ + {"count": 97747, "startTime": "2023-10-30T18:32:41Z"}, + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.warehouse.external_data_source.connection.send_request") + @patch("ee.billing.quota_limiting.list_limited_team_attributes") + def test_external_data_source_billing_limit_deactivate( + self, usage_limit_mock: MagicMock, send_request_mock: MagicMock + ) -> None: + usage_limit_mock.return_value = [self.team.pk] + + external_source = ExternalDataSource.objects.create( + source_id="test_id", + connection_id="fake connectino_id", + destination_id="fake destination_id", + team=self.team, + status="running", + source_type="Stripe", + ) + + check_external_data_source_billing_limit_by_team(self.team.pk) + + external_source.refresh_from_db() + self.assertEqual(external_source.status, "inactive") + + @patch("posthog.warehouse.external_data_source.connection.send_request") + @patch("ee.billing.quota_limiting.list_limited_team_attributes") + def test_external_data_source_billing_limit_activate( + self, usage_limit_mock: MagicMock, send_request_mock: MagicMock + ) -> None: + usage_limit_mock.return_value = [] + + external_source = ExternalDataSource.objects.create( + source_id="test_id", + connection_id="fake connectino_id", + destination_id="fake destination_id", + team=self.team, + status="inactive", + source_type="Stripe", + ) + + check_external_data_source_billing_limit_by_team(self.team.pk) + + external_source.refresh_from_db() + self.assertEqual(external_source.status, "running") diff --git a/posthog/tasks/usage_report.py b/posthog/tasks/usage_report.py index a9a06ecbff7c5..3e8d4907d4f3f 100644 --- a/posthog/tasks/usage_report.py +++ b/posthog/tasks/usage_report.py @@ -16,7 +16,6 @@ ) import requests -from retry import retry import structlog from dateutil import parser from django.conf import settings @@ -24,6 +23,7 @@ from django.db.models import Count, Q from posthoganalytics.client import Client from psycopg2 import sql +from retry import retry from sentry_sdk import capture_exception from posthog import version_requirement @@ -110,6 +110,8 @@ class UsageReportCounters: # Surveys survey_responses_count_in_period: int survey_responses_count_in_month: int + # Data Warehouse + rows_synced_in_period: int # Instance metadata to be included in oveall report @@ -591,6 +593,34 @@ def get_teams_with_survey_responses_count_in_period( return results +@timed_log() +@retry(tries=QUERY_RETRIES, delay=QUERY_RETRY_DELAY, backoff=QUERY_RETRY_BACKOFF) +def get_teams_with_rows_synced_in_period(begin: datetime, end: datetime) -> List[Tuple[int, int]]: + team_to_query = 1 if get_instance_region() == "EU" else 2 + + # dedup by job id incase there were duplicates sent + results = sync_execute( + """ + SELECT team, sum(rows_synced) FROM ( + SELECT JSONExtractString(properties, 'job_id') AS job_id, distinct_id AS team, any(JSONExtractInt(properties, 'count')) AS rows_synced + FROM events + WHERE team_id = %(team_to_query)s AND event = 'external data sync job' AND parseDateTimeBestEffort(JSONExtractString(properties, 'startTime')) BETWEEN %(begin)s AND %(end)s + GROUP BY job_id, team + ) + GROUP BY team + """, + { + "begin": begin, + "end": end, + "team_to_query": team_to_query, + }, + workload=Workload.OFFLINE, + settings=CH_BILLING_SETTINGS, + ) + + return results + + @app.task(ignore_result=True, max_retries=0) def capture_report( capture_event_name: str, @@ -784,6 +814,7 @@ def _get_all_usage_data(period_start: datetime, period_end: datetime) -> Dict[st teams_with_survey_responses_count_in_month=get_teams_with_survey_responses_count_in_period( period_start.replace(day=1), period_end ), + teams_with_rows_synced_in_period=get_teams_with_rows_synced_in_period(period_start, period_end), ) @@ -854,6 +885,7 @@ def _get_team_report(all_data: Dict[str, Any], team: Team) -> UsageReportCounter event_explorer_api_duration_ms=all_data["teams_with_event_explorer_api_duration_ms"].get(team.id, 0), survey_responses_count_in_period=all_data["teams_with_survey_responses_count_in_period"].get(team.id, 0), survey_responses_count_in_month=all_data["teams_with_survey_responses_count_in_month"].get(team.id, 0), + rows_synced_in_period=all_data["teams_with_rows_synced_in_period"].get(team.id, 0), ) diff --git a/posthog/tasks/warehouse.py b/posthog/tasks/warehouse.py new file mode 100644 index 0000000000000..2450251830c59 --- /dev/null +++ b/posthog/tasks/warehouse.py @@ -0,0 +1,167 @@ +from django.conf import settings +import datetime +from posthog.models import Team +from posthog.warehouse.external_data_source.client import send_request +from posthog.warehouse.models.external_data_source import ExternalDataSource +from posthog.warehouse.models import DataWarehouseCredential, DataWarehouseTable +from posthog.warehouse.external_data_source.connection import retrieve_sync +from urllib.parse import urlencode +from posthog.ph_client import get_ph_client +from typing import Any, Dict, List, TYPE_CHECKING +from posthog.celery import app +import structlog + +logger = structlog.get_logger(__name__) + +AIRBYTE_JOBS_URL = "https://api.airbyte.com/v1/jobs" +DEFAULT_DATE_TIME = datetime.datetime(2023, 11, 7, tzinfo=datetime.timezone.utc) + +if TYPE_CHECKING: + from posthoganalytics import Posthog + + +def sync_resources() -> None: + resources = ExternalDataSource.objects.filter(are_tables_created=False, status__in=["running", "error"]) + + for resource in resources: + sync_resource.delay(resource.pk) + + +@app.task(ignore_result=True) +def sync_resource(resource_id: str) -> None: + resource = ExternalDataSource.objects.get(pk=resource_id) + + try: + job = retrieve_sync(resource.connection_id) + except Exception as e: + logger.exception("Data Warehouse: Sync Resource failed with an unexpected exception.", exc_info=e) + resource.status = "error" + resource.save() + return + + if job is None: + logger.error(f"Data Warehouse: No jobs found for connection: {resource.connection_id}") + resource.status = "error" + resource.save() + return + + if job["status"] == "succeeded": + resource = ExternalDataSource.objects.get(pk=resource_id) + credential, _ = DataWarehouseCredential.objects.get_or_create( + team_id=resource.team.pk, + access_key=settings.AIRBYTE_BUCKET_KEY, + access_secret=settings.AIRBYTE_BUCKET_SECRET, + ) + + data = { + "credential": credential, + "name": "stripe_customers", + "format": "Parquet", + "url_pattern": f"https://{settings.AIRBYTE_BUCKET_DOMAIN}/airbyte/{resource.team.pk}/customers/*.parquet", + "team_id": resource.team.pk, + } + + table = DataWarehouseTable(**data) + try: + table.columns = table.get_columns() + except Exception as e: + logger.exception( + f"Data Warehouse: Sync Resource failed with an unexpected exception for connection: {resource.connection_id}", + exc_info=e, + ) + else: + table.save() + + resource.are_tables_created = True + resource.status = job["status"] + resource.save() + + else: + resource.status = job["status"] + resource.save() + + +DEFAULT_USAGE_LIMIT = 1000000 +ROWS_PER_DOLLAR = 66666 # 1 million rows per $15 + + +@app.task(ignore_result=True, max_retries=2) +def check_external_data_source_billing_limit_by_team(team_id: int) -> None: + from posthog.warehouse.external_data_source.connection import deactivate_connection_by_id, activate_connection_by_id + from ee.billing.quota_limiting import list_limited_team_attributes, QuotaResource + + limited_teams_rows_synced = list_limited_team_attributes(QuotaResource.ROWS_SYNCED) + + team = Team.objects.get(pk=team_id) + all_active_connections = ExternalDataSource.objects.filter(team=team, status__in=["running", "succeeded"]) + all_inactive_connections = ExternalDataSource.objects.filter(team=team, status="inactive") + + # TODO: consider more boundaries + if team_id in limited_teams_rows_synced: + for connection in all_active_connections: + deactivate_connection_by_id(connection.connection_id) + connection.status = "inactive" + connection.save() + else: + for connection in all_inactive_connections: + activate_connection_by_id(connection.connection_id) + connection.status = "running" + connection.save() + + +@app.task(ignore_result=True, max_retries=2) +def capture_workspace_rows_synced_by_team(team_id: int) -> None: + ph_client = get_ph_client() + team = Team.objects.get(pk=team_id) + now = datetime.datetime.now(datetime.timezone.utc) + begin = team.external_data_workspace_last_synced_at or DEFAULT_DATE_TIME + + params = { + "workspaceIds": team.external_data_workspace_id, + "limit": 100, + "offset": 0, + "status": "succeeded", + "orderBy": "createdAt|ASC", + "updatedAtStart": begin.strftime("%Y-%m-%dT%H:%M:%SZ"), + "updatedAtEnd": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + } + result_totals = _traverse_jobs_by_field(ph_client, team, AIRBYTE_JOBS_URL + "?" + urlencode(params), "rowsSynced") + + # TODO: check assumption that ordering is possible with API + team.external_data_workspace_last_synced_at = result_totals[-1]["startTime"] if result_totals else now + team.save() + + ph_client.shutdown() + + +def _traverse_jobs_by_field( + ph_client: "Posthog", team: Team, url: str, field: str, acc: List[Dict[str, Any]] = [] +) -> List[Dict[str, Any]]: + response = send_request(url, method="GET") + response_data = response.get("data", []) + response_next = response.get("next", None) + + for job in response_data: + acc.append( + { + "count": job[field], + "startTime": job["startTime"], + } + ) + ph_client.capture( + team.pk, + "external data sync job", + { + "count": job[field], + "workspace_id": team.external_data_workspace_id, + "team_id": team.pk, + "team_uuid": team.uuid, + "startTime": job["startTime"], + "job_id": str(job["jobId"]), + }, + ) + + if response_next: + return _traverse_jobs_by_field(ph_client, team, response_next, field, acc) + + return acc diff --git a/posthog/temporal/sentry.py b/posthog/temporal/sentry.py new file mode 100644 index 0000000000000..290cc0182d2d8 --- /dev/null +++ b/posthog/temporal/sentry.py @@ -0,0 +1,87 @@ +from dataclasses import is_dataclass +from typing import Any, Optional, Type, Union + +from temporalio import activity, workflow +from temporalio.worker import ( + ActivityInboundInterceptor, + ExecuteActivityInput, + ExecuteWorkflowInput, + Interceptor, + WorkflowInboundInterceptor, + WorkflowInterceptorClassInput, +) + +with workflow.unsafe.imports_passed_through(): + from sentry_sdk import Hub, capture_exception, set_context, set_tag + + +def _set_common_workflow_tags(info: Union[workflow.Info, activity.Info]): + set_tag("temporal.workflow.type", info.workflow_type) + set_tag("temporal.workflow.id", info.workflow_id) + + +class _SentryActivityInboundInterceptor(ActivityInboundInterceptor): + async def execute_activity(self, input: ExecuteActivityInput) -> Any: + # https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues + with Hub(Hub.current): + set_tag("temporal.execution_type", "activity") + set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__) + + activity_info = activity.info() + _set_common_workflow_tags(activity_info) + set_tag("temporal.activity.id", activity_info.activity_id) + set_tag("temporal.activity.type", activity_info.activity_type) + set_tag("temporal.activity.task_queue", activity_info.task_queue) + set_tag("temporal.workflow.namespace", activity_info.workflow_namespace) + set_tag("temporal.workflow.run_id", activity_info.workflow_run_id) + try: + return await super().execute_activity(input) + except Exception as e: + if len(input.args) == 1 and is_dataclass(input.args[0]): + team_id = getattr(input.args[0], "team_id", None) + if team_id: + set_tag("team_id", team_id) + set_context("temporal.activity.info", activity.info().__dict__) + capture_exception() + raise e + + +class _SentryWorkflowInterceptor(WorkflowInboundInterceptor): + async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: + # https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues + with Hub(Hub.current): + set_tag("temporal.execution_type", "workflow") + set_tag("module", input.run_fn.__module__ + "." + input.run_fn.__qualname__) + workflow_info = workflow.info() + _set_common_workflow_tags(workflow_info) + set_tag("temporal.workflow.task_queue", workflow_info.task_queue) + set_tag("temporal.workflow.namespace", workflow_info.namespace) + set_tag("temporal.workflow.run_id", workflow_info.run_id) + try: + return await super().execute_workflow(input) + except Exception as e: + if len(input.args) == 1 and is_dataclass(input.args[0]): + team_id = getattr(input.args[0], "team_id", None) + if team_id: + set_tag("team_id", team_id) + set_context("temporal.workflow.info", workflow.info().__dict__) + + if not workflow.unsafe.is_replaying(): + with workflow.unsafe.sandbox_unrestricted(): + capture_exception() + raise e + + +class SentryInterceptor(Interceptor): + """Temporal Interceptor class which will report workflow & activity exceptions to Sentry""" + + def intercept_activity(self, next: ActivityInboundInterceptor) -> ActivityInboundInterceptor: + """Implementation of + :py:meth:`temporalio.worker.Interceptor.intercept_activity`. + """ + return _SentryActivityInboundInterceptor(super().intercept_activity(next)) + + def workflow_interceptor_class( + self, input: WorkflowInterceptorClassInput + ) -> Optional[Type[WorkflowInboundInterceptor]]: + return _SentryWorkflowInterceptor diff --git a/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py index 176b487ff94a0..cea71a458013f 100644 --- a/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py @@ -26,6 +26,7 @@ RedshiftBatchExportWorkflow, RedshiftInsertInputs, insert_into_redshift_activity, + remove_escaped_whitespace_recursive, ) REQUIRED_ENV_VARS = ( @@ -63,14 +64,14 @@ async def assert_events_in_redshift(connection, schema, table_name, events, excl if exclude_events is not None and event_name in exclude_events: continue - properties = event.get("properties", None) - elements_chain = event.get("elements_chain", None) + raw_properties = event.get("properties", None) + properties = remove_escaped_whitespace_recursive(raw_properties) if raw_properties else None expected_event = { "distinct_id": event.get("distinct_id"), - "elements": json.dumps(elements_chain) if elements_chain else None, + "elements": "", "event": event_name, "ip": properties.get("$ip", None) if properties else None, - "properties": json.dumps(properties) if properties else None, + "properties": json.dumps(properties, ensure_ascii=False) if properties else None, "set": properties.get("$set", None) if properties else None, "set_once": properties.get("$set_once", None) if properties else None, # Kept for backwards compatibility, but not exported anymore. @@ -114,7 +115,7 @@ def redshift_config(): return { "user": user, "password": password, - "database": "dev", + "database": "posthog_batch_exports_test_2", "schema": "exports_test_schema", "host": host, "port": int(port), @@ -124,7 +125,10 @@ def redshift_config(): @pytest.fixture def postgres_config(redshift_config): """We shadow this name so that setup_postgres_test_db works with Redshift.""" - return redshift_config + psycopg._encodings._py_codecs["UNICODE"] = "utf-8" + psycopg._encodings.py_codecs.update((k.encode(), v) for k, v in psycopg._encodings._py_codecs.items()) + + yield redshift_config @pytest_asyncio.fixture @@ -137,6 +141,7 @@ async def psycopg_connection(redshift_config, setup_postgres_test_db): host=redshift_config["host"], port=redshift_config["port"], ) + connection.prepare_threshold = None yield connection @@ -176,7 +181,14 @@ async def test_insert_into_redshift_activity_inserts_data_into_redshift_table( count_outside_range=10, count_other_team=10, duplicate=True, - properties={"$browser": "Chrome", "$os": "Mac OS X"}, + properties={ + "$browser": "Chrome", + "$os": "Mac OS X", + "whitespace": "hi\t\n\r\f\bhi", + "nested_whitespace": {"whitespace": "hi\t\n\r\f\bhi"}, + "sequence": {"mucho_whitespace": ["hi", "hi\t\n\r\f\bhi", "hi\t\n\r\f\bhi", "hi"]}, + "multi-byte": "é", + }, person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, ) @@ -344,3 +356,20 @@ async def test_redshift_export_workflow( events=events, exclude_events=exclude_events, ) + + +@pytest.mark.parametrize( + "value,expected", + [ + ([1, 2, 3], [1, 2, 3]), + ("hi\t\n\r\f\bhi", "hi hi"), + ([["\t\n\r\f\b"]], [[""]]), + (("\t\n\r\f\b",), ("",)), + ({"\t\n\r\f\b"}, {""}), + ({"key": "\t\n\r\f\b"}, {"key": ""}), + ({"key": ["\t\n\r\f\b"]}, {"key": [""]}), + ], +) +def test_remove_escaped_whitespace_recursive(value, expected): + """Test we remove some whitespace values.""" + assert remove_escaped_whitespace_recursive(value) == expected diff --git a/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py index 53f8d5f855e5d..9561a8bf2ea35 100644 --- a/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py @@ -2,13 +2,17 @@ import datetime as dt import gzip import json +import os +import random import re +import unittest.mock from collections import deque from uuid import uuid4 import pytest import pytest_asyncio import responses +import snowflake.connector from django.conf import settings from django.test import override_settings from requests.models import PreparedRequest @@ -19,7 +23,6 @@ from temporalio.testing import WorkflowEnvironment from temporalio.worker import UnsandboxedWorkflowRunner, Worker -from posthog.temporal.tests.utils.datetimes import to_isoformat from posthog.temporal.tests.utils.events import generate_test_events_in_clickhouse from posthog.temporal.tests.utils.models import acreate_batch_export, adelete_batch_export, afetch_batch_export_runs from posthog.temporal.workflows.batch_exports import ( @@ -36,6 +39,92 @@ pytestmark = [pytest.mark.asyncio, pytest.mark.django_db] +class FakeSnowflakeCursor: + """A fake Snowflake cursor that can fail on PUT and COPY queries.""" + + def __init__(self, *args, failure_mode: str | None = None, **kwargs): + self._execute_calls = [] + self._execute_async_calls = [] + self._sfqid = 1 + self._fail = failure_mode + + @property + def sfqid(self): + current = self._sfqid + self._sfqid += 1 + return current + + def execute(self, query, params=None, file_stream=None): + self._execute_calls.append({"query": query, "params": params, "file_stream": file_stream}) + + def execute_async(self, query, params=None, file_stream=None): + self._execute_async_calls.append({"query": query, "params": params, "file_stream": file_stream}) + + def get_results_from_sfqid(self, query_id): + pass + + def fetchone(self): + if self._fail == "put": + return ( + "test", + "test.gz", + 456, + 0, + "NONE", + "GZIP", + "FAILED", + "Some error on put", + ) + else: + return ( + "test", + "test.gz", + 456, + 0, + "NONE", + "GZIP", + "UPLOADED", + None, + ) + + def fetchall(self): + if self._fail == "copy": + return [("test", "LOAD FAILED", 100, 99, 1, 1, "Some error on copy", 3)] + else: + return [("test", "LOADED", 100, 99, 1, 1, "Some error on copy", 3)] + + +class FakeSnowflakeConnection: + def __init__( + self, + *args, + failure_mode: str | None = None, + **kwargs, + ): + self._cursors = [] + self._is_running = True + self.failure_mode = failure_mode + + def cursor(self) -> FakeSnowflakeCursor: + cursor = FakeSnowflakeCursor(failure_mode=self.failure_mode) + self._cursors.append(cursor) + return cursor + + def get_query_status_throw_if_error(self, query_id): + return snowflake.connector.constants.QueryStatus.SUCCESS + + def is_still_running(self, status): + current_status = self._is_running + self._is_running = not current_status + return current_status + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + pass + + def contains_queries_in_order(queries: list[str], *queries_to_find: str): """Check if a list of queries contains a list of queries in order.""" # We use a deque to pop the queries we find off the list of queries to @@ -204,21 +293,52 @@ def query_request_handler(request: PreparedRequest): return queries, staged_files +@pytest.fixture +def database(): + """Generate a unique database name for tests.""" + return f"test_batch_exports_{uuid4()}" + + +@pytest.fixture +def schema(): + """Generate a unique schema name for tests.""" + return f"test_batch_exports_{uuid4()}" + + +@pytest.fixture +def table_name(ateam, interval): + return f"test_workflow_table_{ateam.pk}_{interval}" + + +@pytest.fixture +def snowflake_config(database, schema) -> dict[str, str]: + """Return a Snowflake configuration dictionary to use in tests. + + We set default configuration values to support tests against the Snowflake API + and tests that mock it. + """ + password = os.getenv("SNOWFLAKE_PASSWORD", "password") + warehouse = os.getenv("SNOWFLAKE_WAREHOUSE", "COMPUTE_WH") + account = os.getenv("SNOWFLAKE_ACCOUNT", "account") + username = os.getenv("SNOWFLAKE_USERNAME", "hazzadous") + + return { + "password": password, + "user": username, + "warehouse": warehouse, + "account": account, + "database": database, + "schema": schema, + } + + @pytest_asyncio.fixture -async def snowflake_batch_export(ateam, interval, temporal_client): +async def snowflake_batch_export(ateam, table_name, snowflake_config, interval, exclude_events, temporal_client): + """Manage BatchExport model (and associated Temporal Schedule) for tests""" destination_data = { "type": "Snowflake", - "config": { - "user": "hazzadous", - "password": "password", - "account": "account", - "database": "PostHog", - "schema": "test", - "warehouse": "COMPUTE_WH", - "table_name": "events", - }, + "config": {**snowflake_config, "table_name": table_name, "exclude_events": exclude_events}, } - batch_export_data = { "name": "my-production-snowflake-export", "destination": destination_data, @@ -238,7 +358,9 @@ async def snowflake_batch_export(ateam, interval, temporal_client): @pytest.mark.parametrize("interval", ["hour", "day"], indirect=True) -async def test_snowflake_export_workflow_exports_events(ateam, clickhouse_client, snowflake_batch_export, interval): +async def test_snowflake_export_workflow_exports_events( + ateam, clickhouse_client, database, schema, snowflake_batch_export, interval, table_name +): """Test that the whole workflow not just the activity works. It should update the batch export run status to completed, as well as updating the record @@ -247,7 +369,7 @@ async def test_snowflake_export_workflow_exports_events(ateam, clickhouse_client data_interval_end = dt.datetime.fromisoformat("2023-04-25T14:30:00.000000+00:00") data_interval_start = data_interval_end - snowflake_batch_export.interval_time_delta - (events, _, _) = await generate_test_events_in_clickhouse( + await generate_test_events_in_clickhouse( client=clickhouse_client, team_id=ateam.pk, start_time=data_interval_start, @@ -281,10 +403,12 @@ async def test_snowflake_export_workflow_exports_events(ateam, clickhouse_client ], workflow_runner=UnsandboxedWorkflowRunner(), ): - with responses.RequestsMock( - target="snowflake.connector.vendored.requests.adapters.HTTPAdapter.send" - ) as rsps, override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1**2): - queries, staged_files = add_mock_snowflake_api(rsps) + with unittest.mock.patch( + "posthog.temporal.workflows.snowflake_batch_export.snowflake.connector.connect", + ) as mock, override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1): + fake_conn = FakeSnowflakeConnection() + mock.return_value = fake_conn + await activity_environment.client.execute_workflow( SnowflakeBatchExportWorkflow.run, inputs, @@ -294,49 +418,27 @@ async def test_snowflake_export_workflow_exports_events(ateam, clickhouse_client retry_policy=RetryPolicy(maximum_attempts=1), ) - assert contains_queries_in_order( - queries, - 'USE DATABASE "PostHog"', - 'USE SCHEMA "test"', - 'CREATE TABLE IF NOT EXISTS "PostHog"."test"."events"', - # NOTE: we check that we at least have two PUT queries to - # ensure we hit the multi file upload code path - 'PUT file://.* @%"events"', - 'PUT file://.* @%"events"', - 'COPY INTO "events"', - ) + execute_calls = [] + for cursor in fake_conn._cursors: + for call in cursor._execute_calls: + execute_calls.append(call["query"]) - staged_data = "\n".join(staged_files) + execute_async_calls = [] + for cursor in fake_conn._cursors: + for call in cursor._execute_async_calls: + execute_async_calls.append(call["query"]) - # Check that the data is correct. - json_data = [json.loads(line) for line in staged_data.split("\n") if line] - # Pull out the fields we inserted only - json_data = [ - { - "uuid": event["uuid"], - "event": event["event"], - "timestamp": event["timestamp"], - "properties": event["properties"], - "person_id": event["person_id"], - } - for event in json_data + assert execute_calls[0:3] == [ + f'USE DATABASE "{database}"', + f'USE SCHEMA "{schema}"', + "SET ABORT_DETACHED_QUERY = FALSE", ] - json_data.sort(key=lambda x: x["timestamp"]) - # Drop _timestamp and team_id from events - expected_events = [] - for event in events: - expected_event = { - key: value - for key, value in event.items() - if key in ("uuid", "event", "timestamp", "properties", "person_id") - } - expected_event["timestamp"] = to_isoformat(event["timestamp"]) - expected_events.append(expected_event) - expected_events.sort(key=lambda x: x["timestamp"]) + assert all(query.startswith("PUT") for query in execute_calls[3:12]) + assert all(f"_{n}.jsonl" in query for n, query in enumerate(execute_calls[3:12])) - assert json_data[0] == expected_events[0] - assert json_data == expected_events + assert execute_async_calls[0].strip().startswith(f'CREATE TABLE IF NOT EXISTS "{table_name}"') + assert execute_async_calls[1].strip().startswith(f'COPY INTO "{table_name}"') runs = await afetch_batch_export_runs(batch_export_id=snowflake_batch_export.id) assert len(runs) == 1 @@ -451,11 +553,15 @@ async def test_snowflake_export_workflow_raises_error_on_put_fail( ], workflow_runner=UnsandboxedWorkflowRunner(), ): - with responses.RequestsMock( - target="snowflake.connector.vendored.requests.adapters.HTTPAdapter.send" - ) as rsps, override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1**2): - add_mock_snowflake_api(rsps, fail="put") + class FakeSnowflakeConnectionFailOnPut(FakeSnowflakeConnection): + def __init__(self, *args, **kwargs): + super().__init__(*args, failure_mode="put", **kwargs) + + with unittest.mock.patch( + "posthog.temporal.workflows.snowflake_batch_export.snowflake.connector.connect", + side_effect=FakeSnowflakeConnectionFailOnPut, + ): with pytest.raises(WorkflowFailureError) as exc_info: await activity_environment.client.execute_workflow( SnowflakeBatchExportWorkflow.run, @@ -513,11 +619,15 @@ async def test_snowflake_export_workflow_raises_error_on_copy_fail( ], workflow_runner=UnsandboxedWorkflowRunner(), ): - with responses.RequestsMock( - target="snowflake.connector.vendored.requests.adapters.HTTPAdapter.send" - ) as rsps, override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1**2): - add_mock_snowflake_api(rsps, fail="copy") + class FakeSnowflakeConnectionFailOnCopy(FakeSnowflakeConnection): + def __init__(self, *args, **kwargs): + super().__init__(*args, failure_mode="copy", **kwargs) + + with unittest.mock.patch( + "posthog.temporal.workflows.snowflake_batch_export.snowflake.connector.connect", + side_effect=FakeSnowflakeConnectionFailOnCopy, + ): with pytest.raises(WorkflowFailureError) as exc_info: await activity_environment.client.execute_workflow( SnowflakeBatchExportWorkflow.run, @@ -577,8 +687,11 @@ async def insert_into_snowflake_activity_mocked(_: SnowflakeInsertInputs) -> str assert run.latest_error == "ValueError: A useful error message" -async def test_snowflake_export_workflow_handles_cancellation(ateam, snowflake_batch_export): - """Test that Snowflake Export Workflow can gracefully handle cancellations when inserting Snowflake data.""" +async def test_snowflake_export_workflow_handles_cancellation_mocked(ateam, snowflake_batch_export): + """Test that Snowflake Export Workflow can gracefully handle cancellations when inserting Snowflake data. + + We mock the insert_into_snowflake_activity for this test. + """ workflow_id = str(uuid4()) inputs = SnowflakeBatchExportInputs( team_id=ateam.pk, @@ -624,3 +737,462 @@ async def never_finish_activity(_: SnowflakeInsertInputs) -> str: run = runs[0] assert run.status == "Cancelled" assert run.latest_error == "Cancelled" + + +def assert_events_in_snowflake( + cursor: snowflake.connector.cursor.SnowflakeCursor, table_name: str, events: list, exclude_events: list[str] +): + """Assert provided events are present in Snowflake table.""" + cursor.execute(f'SELECT * FROM "{table_name}"') + + rows = cursor.fetchall() + + columns = {index: metadata.name for index, metadata in enumerate(cursor.description)} + json_columns = ("properties", "elements", "people_set", "people_set_once") + + # Rows are tuples, so we construct a dictionary using the metadata from cursor.description. + # We rely on the order of the columns in each row matching the order set in cursor.description. + # This seems to be the case, at least for now. + inserted_events = [ + { + columns[index]: json.loads(row[index]) + if columns[index] in json_columns and row[index] is not None + else row[index] + for index in columns.keys() + } + for row in rows + ] + inserted_events.sort(key=lambda x: (x["event"], x["timestamp"])) + + expected_events = [] + for event in events: + event_name = event.get("event") + + if exclude_events is not None and event_name in exclude_events: + continue + + properties = event.get("properties", None) + elements_chain = event.get("elements_chain", None) + expected_event = { + "distinct_id": event.get("distinct_id"), + "elements": json.dumps(elements_chain), + "event": event_name, + "ip": properties.get("$ip", None) if properties else None, + "properties": event.get("properties"), + "people_set": properties.get("$set", None) if properties else None, + "people_set_once": properties.get("$set_once", None) if properties else None, + "site_url": "", + "timestamp": dt.datetime.fromisoformat(event.get("timestamp")), + "team_id": event.get("team_id"), + "uuid": event.get("uuid"), + } + expected_events.append(expected_event) + + expected_events.sort(key=lambda x: (x["event"], x["timestamp"])) + + assert inserted_events[0] == expected_events[0] + assert inserted_events == expected_events + + +REQUIRED_ENV_VARS = ( + "SNOWFLAKE_WAREHOUSE", + "SNOWFLAKE_PASSWORD", + "SNOWFLAKE_ACCOUNT", + "SNOWFLAKE_USERNAME", +) + +SKIP_IF_MISSING_REQUIRED_ENV_VARS = pytest.mark.skipif( + any(env_var not in os.environ for env_var in REQUIRED_ENV_VARS), + reason="Snowflake required env vars are not set", +) + + +@pytest.fixture +def snowflake_cursor(snowflake_config): + """Manage a snowflake cursor that cleans up after we are done.""" + with snowflake.connector.connect( + user=snowflake_config["user"], + password=snowflake_config["password"], + account=snowflake_config["account"], + warehouse=snowflake_config["warehouse"], + ) as connection: + cursor = connection.cursor() + cursor.execute(f"CREATE DATABASE \"{snowflake_config['database']}\"") + cursor.execute(f"CREATE SCHEMA \"{snowflake_config['database']}\".\"{snowflake_config['schema']}\"") + cursor.execute(f"USE SCHEMA \"{snowflake_config['database']}\".\"{snowflake_config['schema']}\"") + + yield cursor + + cursor.execute(f"DROP DATABASE IF EXISTS \"{snowflake_config['database']}\" CASCADE") + + +@SKIP_IF_MISSING_REQUIRED_ENV_VARS +@pytest.mark.parametrize("exclude_events", [None, ["test-exclude"]], indirect=True) +async def test_insert_into_snowflake_activity_inserts_data_into_snowflake_table( + clickhouse_client, activity_environment, snowflake_cursor, snowflake_config, exclude_events +): + """Test that the insert_into_snowflake_activity function inserts data into a PostgreSQL table. + + We use the generate_test_events_in_clickhouse function to generate several sets + of events. Some of these sets are expected to be exported, and others not. Expected + events are those that: + * Are created for the team_id of the batch export. + * Are created in the date range of the batch export. + * Are not duplicates of other events that are in the same batch. + * Do not have an event name contained in the batch export's exclude_events. + + Once we have these events, we pass them to the assert_events_in_snowflake function to check + that they appear in the expected Snowflake table. This function runs against a real Snowflake + instance, so the environment should be populated with the necessary credentials. + """ + data_interval_start = dt.datetime(2023, 4, 20, 14, 0, 0, tzinfo=dt.timezone.utc) + data_interval_end = dt.datetime(2023, 4, 25, 15, 0, 0, tzinfo=dt.timezone.utc) + + team_id = random.randint(1, 1000000) + (events, _, _) = await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=team_id, + start_time=data_interval_start, + end_time=data_interval_end, + count=1000, + count_outside_range=10, + count_other_team=10, + duplicate=True, + properties={"$browser": "Chrome", "$os": "Mac OS X"}, + person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, + ) + + if exclude_events: + for event_name in exclude_events: + await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=team_id, + start_time=data_interval_start, + end_time=data_interval_end, + count=5, + count_outside_range=0, + count_other_team=0, + event_name=event_name, + ) + + table_name = f"test_insert_activity_table_{team_id}" + insert_inputs = SnowflakeInsertInputs( + team_id=team_id, + table_name=table_name, + data_interval_start=data_interval_start.isoformat(), + data_interval_end=data_interval_end.isoformat(), + exclude_events=exclude_events, + **snowflake_config, + ) + + await activity_environment.run(insert_into_snowflake_activity, insert_inputs) + + assert_events_in_snowflake( + cursor=snowflake_cursor, + table_name=table_name, + events=events, + exclude_events=exclude_events, + ) + + +@SKIP_IF_MISSING_REQUIRED_ENV_VARS +@pytest.mark.parametrize("interval", ["hour", "day"], indirect=True) +@pytest.mark.parametrize("exclude_events", [None, ["test-exclude"]], indirect=True) +async def test_snowflake_export_workflow( + clickhouse_client, + snowflake_cursor, + interval, + snowflake_batch_export, + ateam, + exclude_events, +): + """Test Redshift Export Workflow end-to-end. + + The workflow should update the batch export run status to completed and produce the expected + records to the provided Redshift instance. + """ + data_interval_end = dt.datetime.fromisoformat("2023-04-25T14:30:00.000000+00:00") + data_interval_start = data_interval_end - snowflake_batch_export.interval_time_delta + + (events, _, _) = await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=ateam.pk, + start_time=data_interval_start, + end_time=data_interval_end, + count=100, + count_outside_range=10, + count_other_team=10, + duplicate=True, + properties={"$browser": "Chrome", "$os": "Mac OS X"}, + person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, + ) + + if exclude_events: + for event_name in exclude_events: + await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=ateam.pk, + start_time=data_interval_start, + end_time=data_interval_end, + count=5, + count_outside_range=0, + count_other_team=0, + event_name=event_name, + ) + + workflow_id = str(uuid4()) + inputs = SnowflakeBatchExportInputs( + team_id=ateam.pk, + batch_export_id=str(snowflake_batch_export.id), + data_interval_end=data_interval_end.isoformat(), + interval=interval, + **snowflake_batch_export.destination.config, + ) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[SnowflakeBatchExportWorkflow], + activities=[ + create_export_run, + insert_into_snowflake_activity, + update_export_run_status, + ], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + await activity_environment.client.execute_workflow( + SnowflakeBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + execution_timeout=dt.timedelta(seconds=10), + ) + + runs = await afetch_batch_export_runs(batch_export_id=snowflake_batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Completed" + + assert_events_in_snowflake( + cursor=snowflake_cursor, + table_name=snowflake_batch_export.destination.config["table_name"], + events=events, + exclude_events=exclude_events, + ) + + +@SKIP_IF_MISSING_REQUIRED_ENV_VARS +@pytest.mark.parametrize("interval", ["hour", "day"], indirect=True) +@pytest.mark.parametrize("exclude_events", [None, ["test-exclude"]], indirect=True) +async def test_snowflake_export_workflow_with_many_files( + clickhouse_client, + snowflake_cursor, + interval, + snowflake_batch_export, + ateam, + exclude_events, +): + """Test Snowflake Export Workflow end-to-end with multiple file uploads. + + This test overrides the chunk size and sets it to 1 byte to trigger multiple file uploads. + We want to assert that all files are properly copied into the table. Of course, 1 byte limit + means we are uploading one file at a time, which is very innefficient. For this reason, this test + can take longer, so we keep the event count low and bump the Workflow timeout. + """ + data_interval_end = dt.datetime.fromisoformat("2023-04-25T14:30:00.000000+00:00") + data_interval_start = data_interval_end - snowflake_batch_export.interval_time_delta + + (events, _, _) = await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=ateam.pk, + start_time=data_interval_start, + end_time=data_interval_end, + count=10, + count_outside_range=10, + count_other_team=10, + duplicate=True, + properties={"$browser": "Chrome", "$os": "Mac OS X"}, + person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, + ) + + workflow_id = str(uuid4()) + inputs = SnowflakeBatchExportInputs( + team_id=ateam.pk, + batch_export_id=str(snowflake_batch_export.id), + data_interval_end=data_interval_end.isoformat(), + interval=interval, + **snowflake_batch_export.destination.config, + ) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[SnowflakeBatchExportWorkflow], + activities=[ + create_export_run, + insert_into_snowflake_activity, + update_export_run_status, + ], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + with override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1): + await activity_environment.client.execute_workflow( + SnowflakeBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + execution_timeout=dt.timedelta(seconds=20), + ) + + runs = await afetch_batch_export_runs(batch_export_id=snowflake_batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Completed" + + assert_events_in_snowflake( + cursor=snowflake_cursor, + table_name=snowflake_batch_export.destination.config["table_name"], + events=events, + exclude_events=exclude_events, + ) + + +@SKIP_IF_MISSING_REQUIRED_ENV_VARS +async def test_snowflake_export_workflow_handles_cancellation( + clickhouse_client, ateam, snowflake_batch_export, interval, snowflake_cursor +): + """Test that Snowflake Export Workflow can gracefully handle cancellations when inserting Snowflake data.""" + data_interval_end = dt.datetime.fromisoformat("2023-04-25T14:30:00.000000+00:00") + data_interval_start = data_interval_end - snowflake_batch_export.interval_time_delta + + await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=ateam.pk, + start_time=data_interval_start, + end_time=data_interval_end, + count=100, + count_outside_range=10, + count_other_team=10, + duplicate=True, + properties={"$browser": "Chrome", "$os": "Mac OS X"}, + person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, + ) + + workflow_id = str(uuid4()) + inputs = SnowflakeBatchExportInputs( + team_id=ateam.pk, + batch_export_id=str(snowflake_batch_export.id), + data_interval_end=data_interval_end.isoformat(), + interval=interval, + **snowflake_batch_export.destination.config, + ) + + async with await WorkflowEnvironment.start_time_skipping() as activity_environment: + async with Worker( + activity_environment.client, + task_queue=settings.TEMPORAL_TASK_QUEUE, + workflows=[SnowflakeBatchExportWorkflow], + activities=[ + create_export_run, + insert_into_snowflake_activity, + update_export_run_status, + ], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + # We set the chunk size low on purpose to slow things down and give us time to cancel. + with override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1): + handle = await activity_environment.client.start_workflow( + SnowflakeBatchExportWorkflow.run, + inputs, + id=workflow_id, + task_queue=settings.TEMPORAL_TASK_QUEUE, + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + # We need to wait a bit for the activity to start running. + await asyncio.sleep(5) + await handle.cancel() + + with pytest.raises(WorkflowFailureError): + await handle.result() + + runs = await afetch_batch_export_runs(batch_export_id=snowflake_batch_export.id) + assert len(runs) == 1 + + run = runs[0] + assert run.status == "Cancelled" + assert run.latest_error == "Cancelled" + + +@SKIP_IF_MISSING_REQUIRED_ENV_VARS +async def test_insert_into_snowflake_activity_heartbeats( + clickhouse_client, + ateam, + snowflake_batch_export, + snowflake_cursor, + snowflake_config, + activity_environment, +): + """Test that the insert_into_snowflake_activity activity sends heartbeats. + + We use a function that runs on_heartbeat to check and track the heartbeat contents. + """ + data_interval_end = dt.datetime.fromisoformat("2023-04-20T14:30:00.000000+00:00") + data_interval_start = data_interval_end - snowflake_batch_export.interval_time_delta + + events_in_files = [] + n_expected_files = 3 + + for i in range(1, n_expected_files + 1): + part_inserted_at = data_interval_end - snowflake_batch_export.interval_time_delta / i + + (events, _, _) = await generate_test_events_in_clickhouse( + client=clickhouse_client, + team_id=ateam.pk, + start_time=data_interval_start, + end_time=data_interval_end, + count=1, + count_outside_range=0, + count_other_team=0, + duplicate=False, + inserted_at=part_inserted_at, + ) + events_in_files += events + + captured_details = [] + + def capture_heartbeat_details(*details): + """A function to track what we heartbeat.""" + nonlocal captured_details + + captured_details.append(details) + + activity_environment.on_heartbeat = capture_heartbeat_details + + table_name = f"test_insert_activity_table_{ateam.pk}" + insert_inputs = SnowflakeInsertInputs( + team_id=ateam.pk, + table_name=table_name, + data_interval_start=data_interval_start.isoformat(), + data_interval_end=data_interval_end.isoformat(), + **snowflake_config, + ) + + with override_settings(BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES=1): + await activity_environment.run(insert_into_snowflake_activity, insert_inputs) + + assert n_expected_files == len(captured_details) + + for index, details_captured in enumerate(captured_details): + assert dt.datetime.fromisoformat( + details_captured[0] + ) == data_interval_end - snowflake_batch_export.interval_time_delta / (index + 1) + assert details_captured[1] == index + 1 + + assert_events_in_snowflake(snowflake_cursor, table_name, events_in_files, exclude_events=[]) diff --git a/posthog/temporal/utils.py b/posthog/temporal/utils.py index d8757e2949a29..efc19c9e8ef4a 100644 --- a/posthog/temporal/utils.py +++ b/posthog/temporal/utils.py @@ -1,35 +1,115 @@ -import asyncio -from functools import wraps -from typing import Any, Awaitable, Callable, TypeVar, cast +import collections.abc +import dataclasses +import datetime as dt +import typing -from temporalio import activity -F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) +class EmptyHeartbeatError(Exception): + """Raised when an activity heartbeat is empty. + This is also the error we expect when no heartbeatting is happening, as the sequence will be empty. + """ + + def __init__(self): + super().__init__(f"Heartbeat details sequence is empty") + + +class NotEnoughHeartbeatValuesError(Exception): + """Raised when an activity heartbeat doesn't contain the right amount of values we expect.""" + + def __init__(self, details_len: int, expected: int): + super().__init__(f"Not enough values in heartbeat details (expected {expected}, got {details_len})") + + +class HeartbeatParseError(Exception): + """Raised when an activity heartbeat cannot be parsed into it's expected types.""" + + def __init__(self, field: str): + super().__init__(f"Parsing {field} from heartbeat details encountered an error") + + +@dataclasses.dataclass +class HeartbeatDetails: + """The batch export details included in every heartbeat. + + Each batch export destination should subclass this and implement whatever details are specific to that + batch export and required to resume it. + + Attributes: + last_inserted_at: The last inserted_at we managed to upload or insert, depending on the destination. + _remaining: Any remaining values in the heartbeat_details tuple that we do not parse. + """ + + last_inserted_at: dt.datetime + _remaining: collections.abc.Sequence[typing.Any] + + @property + def total_details(self) -> int: + """The total number of details that we have parsed + those remaining to parse.""" + return (len(dataclasses.fields(self.__class__)) - 1) + len(self._remaining) + + @classmethod + def from_activity(cls, activity): + """Attempt to initialize HeartbeatDetails from an activity's info.""" + details = activity.info().heartbeat_details + + if len(details) == 0: + raise EmptyHeartbeatError() -def auto_heartbeater(fn: F) -> F: - # We want to ensure that the type hints from the original callable are - # available via our wrapper, so we use the functools wraps decorator - @wraps(fn) - async def wrapper(*args, **kwargs): - heartbeat_timeout = activity.info().heartbeat_timeout - heartbeat_task = None - if heartbeat_timeout: - # Heartbeat twice as often as the timeout - heartbeat_task = asyncio.create_task(heartbeat_every(heartbeat_timeout.total_seconds() / 2)) try: - return await fn(*args, **kwargs) - finally: - if heartbeat_task: - heartbeat_task.cancel() - # Wait for heartbeat cancellation to complete - await asyncio.wait([heartbeat_task]) + last_inserted_at = dt.datetime.fromisoformat(details[0]) + except (TypeError, ValueError) as e: + raise HeartbeatParseError("last_inserted_at") from e + + return cls(last_inserted_at, _remaining=details[1:]) + + +HeartbeatType = typing.TypeVar("HeartbeatType", bound=HeartbeatDetails) + + +async def should_resume_from_activity_heartbeat( + activity, heartbeat_type: typing.Type[HeartbeatType], logger +) -> tuple[bool, HeartbeatType | None]: + """Check if a batch export should resume from an activity's heartbeat details. + + We understand that a batch export should resume any time that we receive heartbeat details and + those details can be correctly parsed. However, the decision is ultimately up to the batch export + activity to decide if it must resume and how to do so. + + Returns: + A tuple with the first element indicating if the batch export should resume. If the first element + is True, the second tuple element will be the heartbeat details themselves, otherwise None. + """ + try: + heartbeat_details = heartbeat_type.from_activity(activity) + + except EmptyHeartbeatError: + # We don't log this as a warning/error because it's the expected exception when heartbeat is empty. + heartbeat_details = None + received = False + logger.debug("Did not receive details from previous activity execution") + + except NotEnoughHeartbeatValuesError: + heartbeat_details = None + received = False + logger.warning("Details from previous activity execution did not contain the expected amount of values") + + except HeartbeatParseError: + heartbeat_details = None + received = False + logger.warning("Details from previous activity execution could not be parsed.") - return cast(F, wrapper) + except Exception: + # We should start from the beginning, but we make a point to log unexpected errors. + # Ideally, any new exceptions should be added to the previous blocks after the first time and we will never land here. + heartbeat_details = None + received = False + logger.exception("Did not receive details from previous activity Excecution due to an unexpected error") + else: + received = True + logger.debug( + f"Received details from previous activity: {heartbeat_details}", + ) -async def heartbeat_every(delay: float, *details: Any) -> None: - # Heartbeat every so often while not cancelled - while True: - await asyncio.sleep(delay) - activity.heartbeat(*details) + return received, heartbeat_details diff --git a/posthog/temporal/worker.py b/posthog/temporal/worker.py index 81cc28cdfd685..05666379e45c6 100644 --- a/posthog/temporal/worker.py +++ b/posthog/temporal/worker.py @@ -7,6 +7,7 @@ from posthog.temporal.client import connect from posthog.temporal.workflows import ACTIVITIES, WORKFLOWS +from posthog.temporal.sentry import SentryInterceptor async def start_worker( @@ -36,6 +37,7 @@ async def start_worker( activities=ACTIVITIES, workflow_runner=UnsandboxedWorkflowRunner(), graceful_shutdown_timeout=timedelta(minutes=5), + interceptors=[SentryInterceptor()], ) # catch the TERM signal, and stop the worker gracefully diff --git a/posthog/temporal/workflows/bigquery_batch_export.py b/posthog/temporal/workflows/bigquery_batch_export.py index 98f4a51d3c4d1..759b755427f2d 100644 --- a/posthog/temporal/workflows/bigquery_batch_export.py +++ b/posthog/temporal/workflows/bigquery_batch_export.py @@ -1,7 +1,8 @@ +import asyncio import contextlib +import dataclasses import datetime as dt import json -from dataclasses import dataclass from django.conf import settings from google.cloud import bigquery @@ -10,6 +11,10 @@ from temporalio.common import RetryPolicy from posthog.batch_exports.service import BigQueryBatchExportInputs +from posthog.temporal.utils import ( + HeartbeatDetails, + should_resume_from_activity_heartbeat, +) from posthog.temporal.workflows.base import PostHogWorkflow from posthog.temporal.workflows.batch_exports import ( BatchExportTemporaryFile, @@ -26,7 +31,7 @@ from posthog.temporal.workflows.metrics import get_bytes_exported_metric, get_rows_exported_metric -def load_jsonl_file_to_bigquery_table(jsonl_file, table, table_schema, bigquery_client): +async def load_jsonl_file_to_bigquery_table(jsonl_file, table, table_schema, bigquery_client): """Execute a COPY FROM query with given connection to copy contents of jsonl_file.""" job_config = bigquery.LoadJobConfig( source_format="NEWLINE_DELIMITED_JSON", @@ -34,10 +39,10 @@ def load_jsonl_file_to_bigquery_table(jsonl_file, table, table_schema, bigquery_ ) load_job = bigquery_client.load_table_from_file(jsonl_file, table, job_config=job_config, rewind=True) - load_job.result() + await asyncio.to_thread(load_job.result) -def create_table_in_bigquery( +async def create_table_in_bigquery( project_id: str, dataset_id: str, table_id: str, @@ -49,12 +54,19 @@ def create_table_in_bigquery( fully_qualified_name = f"{project_id}.{dataset_id}.{table_id}" table = bigquery.Table(fully_qualified_name, schema=table_schema) table.time_partitioning = bigquery.TimePartitioning(type_=bigquery.TimePartitioningType.DAY, field="timestamp") - table = bigquery_client.create_table(table, exists_ok=exists_ok) + table = await asyncio.to_thread(bigquery_client.create_table, table, exists_ok=exists_ok) return table -@dataclass +@dataclasses.dataclass +class BigQueryHeartbeatDetails(HeartbeatDetails): + """The BigQuery batch export details included in every heartbeat.""" + + pass + + +@dataclasses.dataclass class BigQueryInsertInputs: """Inputs for BigQuery.""" @@ -106,6 +118,15 @@ async def insert_into_bigquery_activity(inputs: BigQueryInsertInputs): inputs.data_interval_end, ) + should_resume, details = await should_resume_from_activity_heartbeat(activity, BigQueryHeartbeatDetails, logger) + + if should_resume is True and details is not None: + data_interval_start = details.last_inserted_at.isoformat() + last_inserted_at = details.last_inserted_at + else: + data_interval_start = inputs.data_interval_start + last_inserted_at = None + async with get_client() as client: if not await client.is_alive(): raise ConnectionError("Cannot establish connection to ClickHouse") @@ -113,7 +134,7 @@ async def insert_into_bigquery_activity(inputs: BigQueryInsertInputs): count = await get_rows_count( client=client, team_id=inputs.team_id, - interval_start=inputs.data_interval_start, + interval_start=data_interval_start, interval_end=inputs.data_interval_end, exclude_events=inputs.exclude_events, include_events=inputs.include_events, @@ -132,7 +153,7 @@ async def insert_into_bigquery_activity(inputs: BigQueryInsertInputs): results_iterator = get_results_iterator( client=client, team_id=inputs.team_id, - interval_start=inputs.data_interval_start, + interval_start=data_interval_start, interval_end=inputs.data_interval_end, exclude_events=inputs.exclude_events, include_events=inputs.include_events, @@ -153,8 +174,24 @@ async def insert_into_bigquery_activity(inputs: BigQueryInsertInputs): ] json_columns = ("properties", "elements", "set", "set_once") + result = None + + async def worker_shutdown_handler(): + """Handle the Worker shutting down by heart-beating our latest status.""" + await activity.wait_for_worker_shutdown() + logger.bind(last_inserted_at=last_inserted_at).debug("Worker shutting down!") + + if last_inserted_at is None: + # Don't heartbeat if worker shuts down before we could even send anything + # Just start from the beginning again. + return + + activity.heartbeat(last_inserted_at) + + asyncio.create_task(worker_shutdown_handler()) + with bigquery_client(inputs) as bq_client: - bigquery_table = create_table_in_bigquery( + bigquery_table = await create_table_in_bigquery( inputs.project_id, inputs.dataset_id, inputs.table_id, @@ -166,13 +203,13 @@ async def insert_into_bigquery_activity(inputs: BigQueryInsertInputs): rows_exported = get_rows_exported_metric() bytes_exported = get_bytes_exported_metric() - def flush_to_bigquery(): + async def flush_to_bigquery(): logger.debug( "Loading %s records of size %s bytes", jsonl_file.records_since_last_reset, jsonl_file.bytes_since_last_reset, ) - load_jsonl_file_to_bigquery_table(jsonl_file, bigquery_table, table_schema, bq_client) + await load_jsonl_file_to_bigquery_table(jsonl_file, bigquery_table, table_schema, bq_client) rows_exported.add(jsonl_file.records_since_last_reset) bytes_exported.add(jsonl_file.bytes_since_last_reset) @@ -188,11 +225,20 @@ def flush_to_bigquery(): jsonl_file.write_records_to_jsonl([row]) if jsonl_file.tell() > settings.BATCH_EXPORT_BIGQUERY_UPLOAD_CHUNK_SIZE_BYTES: - flush_to_bigquery() + await flush_to_bigquery() + + last_inserted_at = result["inserted_at"] + activity.heartbeat(last_inserted_at) + jsonl_file.reset() - if jsonl_file.tell() > 0: - flush_to_bigquery() + if jsonl_file.tell() > 0 and result is not None: + await flush_to_bigquery() + + last_inserted_at = result["inserted_at"] + activity.heartbeat(last_inserted_at) + + jsonl_file.reset() @workflow.defn(name="bigquery-export") @@ -263,6 +309,4 @@ async def run(self, inputs: BigQueryBatchExportInputs): "NotFound", ], update_inputs=update_inputs, - # Disable heartbeat timeout until we add heartbeat support. - heartbeat_timeout_seconds=None, ) diff --git a/posthog/temporal/workflows/redshift_batch_export.py b/posthog/temporal/workflows/redshift_batch_export.py index 06843289aee5e..fd2ba4c9e9193 100644 --- a/posthog/temporal/workflows/redshift_batch_export.py +++ b/posthog/temporal/workflows/redshift_batch_export.py @@ -32,10 +32,65 @@ ) +def remove_escaped_whitespace_recursive(value): + """Remove all escaped whitespace characters from given value. + + PostgreSQL supports constant escaped strings by appending an E' to each string that + contains whitespace in them (amongst other characters). See: + https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE + + However, Redshift does not support this syntax. So, to avoid any escaping by + underlying PostgreSQL library, we remove the whitespace ourselves as defined in the + translation table WHITESPACE_TRANSLATE. + + This function is recursive just to be extremely careful and catch any whitespace that + may be sneaked in a dictionary key or sequence. + """ + match value: + case str(s): + return " ".join(s.replace("\b", " ").split()) + + case bytes(b): + return remove_escaped_whitespace_recursive(b.decode("utf-8")) + + case [*sequence]: + # mypy could be bugged as it's raising a Statement unreachable error. + # But we are definitely reaching this statement in tests; hence the ignore comment. + # Maybe: https://github.com/python/mypy/issues/16272. + return type(value)(remove_escaped_whitespace_recursive(sequence_value) for sequence_value in sequence) # type: ignore + + case set(elements): + return set(remove_escaped_whitespace_recursive(element) for element in elements) + + case {**mapping}: + return {k: remove_escaped_whitespace_recursive(v) for k, v in mapping.items()} + + case value: + return value + + +@contextlib.asynccontextmanager +async def redshift_connection(inputs) -> typing.AsyncIterator[psycopg.AsyncConnection]: + """Manage a Redshift connection. + + This just yields a Postgres connection but we adjust a couple of things required for + psycopg to work with Redshift: + 1. Set UNICODE encoding to utf-8 as Redshift reports back UNICODE. + 2. Set prepare_threshold to None on the connection as psycopg attempts to run DEALLOCATE ALL otherwise + which is not supported on Redshift. + """ + psycopg._encodings._py_codecs["UNICODE"] = "utf-8" + psycopg._encodings.py_codecs.update((k.encode(), v) for k, v in psycopg._encodings._py_codecs.items()) + + async with postgres_connection(inputs) as connection: + connection.prepare_threshold = None + yield connection + + async def insert_records_to_redshift( records: collections.abc.Iterator[dict[str, typing.Any]], redshift_connection: psycopg.AsyncConnection, - schema: str, + schema: str | None, table: str, batch_size: int = 100, ): @@ -61,35 +116,41 @@ async def insert_records_to_redshift( first_record = next(records) columns = first_record.keys() + if schema: + table_identifier = sql.Identifier(schema, table) + else: + table_identifier = sql.Identifier(table) + pre_query = sql.SQL("INSERT INTO {table} ({fields}) VALUES").format( - table=sql.Identifier(schema, table), + table=table_identifier, fields=sql.SQL(", ").join(map(sql.Identifier, columns)), ) template = sql.SQL("({})").format(sql.SQL(", ").join(map(sql.Placeholder, columns))) rows_exported = get_rows_exported_metric() async with async_client_cursor_from_connection(redshift_connection) as cursor: - batch = [pre_query.as_string(cursor).encode("utf-8")] + batch = [] + pre_query_str = pre_query.as_string(cursor).encode("utf-8") async def flush_to_redshift(batch): - await cursor.execute(b"".join(batch)) - rows_exported.add(len(batch) - 1) + values = b",".join(batch).replace(b" E'", b" '") + + await cursor.execute(pre_query_str + values) + rows_exported.add(len(batch)) # It would be nice to record BYTES_EXPORTED for Redshift, but it's not worth estimating # the byte size of each batch the way things are currently written. We can revisit this # in the future if we decide it's useful enough. for record in itertools.chain([first_record], records): batch.append(cursor.mogrify(template, record).encode("utf-8")) - if len(batch) < batch_size: - batch.append(b",") continue await flush_to_redshift(batch) - batch = [pre_query.as_string(cursor).encode("utf-8")] + batch = [] if len(batch) > 0: - await flush_to_redshift(batch[:-1]) + await flush_to_redshift(batch) @contextlib.asynccontextmanager @@ -181,7 +242,7 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs): ) properties_type = "VARCHAR(65535)" if inputs.properties_data_type == "varchar" else "SUPER" - async with postgres_connection(inputs) as connection: + async with redshift_connection(inputs) as connection: await create_table_in_postgres( connection, schema=inputs.schema, @@ -218,10 +279,14 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs): def map_to_record(row: dict) -> dict: """Map row to a record to insert to Redshift.""" - return { - key: json.dumps(row[key]) if key in json_columns and row[key] is not None else row[key] + record = { + key: json.dumps(remove_escaped_whitespace_recursive(row[key]), ensure_ascii=False) + if key in json_columns and row[key] is not None + else row[key] for key in schema_columns } + record["elements"] = "" + return record async with postgres_connection(inputs) as connection: await insert_records_to_redshift( diff --git a/posthog/temporal/workflows/s3_batch_export.py b/posthog/temporal/workflows/s3_batch_export.py index fc29b414d2274..42e66f10d2ae3 100644 --- a/posthog/temporal/workflows/s3_batch_export.py +++ b/posthog/temporal/workflows/s3_batch_export.py @@ -276,7 +276,7 @@ class HeartbeatDetails(typing.NamedTuple): def from_activity_details(cls, details): last_uploaded_part_timestamp = details[0] upload_state = S3MultiPartUploadState(*details[1]) - return HeartbeatDetails(last_uploaded_part_timestamp, upload_state) + return cls(last_uploaded_part_timestamp, upload_state) @dataclass diff --git a/posthog/temporal/workflows/snowflake_batch_export.py b/posthog/temporal/workflows/snowflake_batch_export.py index 1831f87fa2f87..b216f20af0412 100644 --- a/posthog/temporal/workflows/snowflake_batch_export.py +++ b/posthog/temporal/workflows/snowflake_batch_export.py @@ -1,17 +1,28 @@ +import asyncio +import contextlib +import dataclasses import datetime as dt +import functools +import io import json -import tempfile -from dataclasses import dataclass +import typing import snowflake.connector from django.conf import settings -from snowflake.connector.cursor import SnowflakeCursor +from snowflake.connector.connection import SnowflakeConnection from temporalio import activity, workflow from temporalio.common import RetryPolicy from posthog.batch_exports.service import SnowflakeBatchExportInputs +from posthog.temporal.utils import ( + HeartbeatDetails, + HeartbeatParseError, + NotEnoughHeartbeatValuesError, + should_resume_from_activity_heartbeat, +) from posthog.temporal.workflows.base import PostHogWorkflow from posthog.temporal.workflows.batch_exports import ( + BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -43,7 +54,32 @@ def __init__(self, table_name: str, status: str, errors_seen: int, first_error: ) -@dataclass +@dataclasses.dataclass +class SnowflakeHeartbeatDetails(HeartbeatDetails): + """The Snowflake batch export details included in every heartbeat. + + Attributes: + file_no: The file number of the last file we managed to upload. + """ + + file_no: int + + @classmethod + def from_activity(cls, activity): + details = super().from_activity(activity) + + if details.total_details < 2: + raise NotEnoughHeartbeatValuesError(details.total_details, 2) + + try: + file_no = int(details._remaining[1]) + except (TypeError, ValueError) as e: + raise HeartbeatParseError("file_no") from e + + return cls(last_inserted_at=details.last_inserted_at, file_no=file_no, _remaining=details._remaining[2:]) + + +@dataclasses.dataclass class SnowflakeInsertInputs: """Inputs for Snowflake.""" @@ -66,23 +102,137 @@ class SnowflakeInsertInputs: include_events: list[str] | None = None -def put_file_to_snowflake_table(cursor: SnowflakeCursor, file_name: str, table_name: str): +def use_namespace(connection: SnowflakeConnection, database: str, schema: str) -> None: + """Switch to a namespace given by database and schema. + + This allows all queries that follow to ignore database and schema. + """ + cursor = connection.cursor() + cursor.execute(f'USE DATABASE "{database}"') + cursor.execute(f'USE SCHEMA "{schema}"') + + +@contextlib.contextmanager +def snowflake_connection(inputs) -> typing.Generator[SnowflakeConnection, None, None]: + """Context manager that yields a Snowflake connection. + + Before yielding we ensure we are in the right namespace, and we set ABORT_DETACHED_QUERY + to FALSE to avoid Snowflake cancelling any async queries. + """ + with snowflake.connector.connect( + user=inputs.user, + password=inputs.password, + account=inputs.account, + warehouse=inputs.warehouse, + database=inputs.database, + schema=inputs.schema, + role=inputs.role, + ) as connection: + use_namespace(connection, inputs.database, inputs.schema) + connection.cursor().execute("SET ABORT_DETACHED_QUERY = FALSE") + + yield connection + + +async def execute_async_query( + connection: SnowflakeConnection, + query: str, + parameters: dict | None = None, + file_stream=None, + poll_interval: float = 1.0, +) -> str: + """Wrap Snowflake connector's polling API in a coroutine. + + This enables asynchronous execution of queries to release the event loop to execute other tasks + while we poll for a query to be done. For example, the event loop may use this time for heartbeating. + + Args: + connection: A SnowflakeConnection object as produced by snowflake.connector.connect. + query: A query string to run asynchronously. + parameters: An optional dictionary of parameters to bind to the query. + poll_interval: Specify how long to wait in between polls. + """ + cursor = connection.cursor() + + # Snowflake docs incorrectly state that the 'params' argument is named 'parameters'. + result = cursor.execute_async(query, params=parameters, file_stream=file_stream) + query_id = cursor.sfqid or result["queryId"] + + # Snowflake does a blocking HTTP request, so we send it to a thread. + query_status = await asyncio.to_thread(connection.get_query_status_throw_if_error, query_id) + + while connection.is_still_running(query_status): + query_status = await asyncio.to_thread(connection.get_query_status_throw_if_error, query_id) + await asyncio.sleep(poll_interval) + + return query_id + + +async def create_table_in_snowflake(connection: SnowflakeConnection, table_name: str) -> None: + """Asynchronously create the table if it doesn't exist. + + Note that we use the same schema as the snowflake-plugin for backwards compatibility.""" + await execute_async_query( + connection, + f""" + CREATE TABLE IF NOT EXISTS "{table_name}" ( + "uuid" STRING, + "event" STRING, + "properties" VARIANT, + "elements" VARIANT, + "people_set" VARIANT, + "people_set_once" VARIANT, + "distinct_id" STRING, + "team_id" INTEGER, + "ip" STRING, + "site_url" STRING, + "timestamp" TIMESTAMP + ) + COMMENT = 'PostHog generated events table' + """, + ) + + +async def put_file_to_snowflake_table( + connection: SnowflakeConnection, + file: BatchExportTemporaryFile, + table_name: str, + file_no: int, +): """Executes a PUT query using the provided cursor to the provided table_name. + Sadly, Snowflake's execute_async does not work with PUT statements. So, we pass the execute + call to run_in_executor: Since execute ends up boiling down to blocking IO (HTTP request), + the event loop should not be locked up. + + We add a file_no to the file_name when executing PUT as Snowflake will reject any files with the same + name. Since batch exports re-use the same file, our name does not change, but we don't want Snowflake + to reject or overwrite our new data. + Args: - cursor: A Snowflake cursor to execute the PUT query. - file_name: The name of the file to PUT. - table_name: The name of the table where to PUT the file. + connection: A SnowflakeConnection object as produced by snowflake.connector.connect. + file: The name of the local file to PUT. + table_name: The name of the Snowflake table where to PUT the file. + file_no: An int to identify which file number this is. Raises: TypeError: If we don't get a tuple back from Snowflake (should never happen). SnowflakeFileNotUploadedError: If the upload status is not 'UPLOADED'. """ - cursor.execute( - f""" - PUT file://{file_name} @%"{table_name}" - """ - ) + file.rewind() + + # We comply with the file-like interface of io.IOBase. + # So we ask mypy to be nice with us. + reader = io.BufferedReader(file) # type: ignore + query = f'PUT file://{file.name}_{file_no}.jsonl @%"{table_name}"' + cursor = connection.cursor() + + execute_put = functools.partial(cursor.execute, query, file_stream=reader) + + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, func=execute_put) + reader.detach() # BufferedReader closes the file otherwise. + result = cursor.fetchone() if not isinstance(result, tuple): # Mostly to appease mypy, as this query should always return a tuple. @@ -93,6 +243,55 @@ def put_file_to_snowflake_table(cursor: SnowflakeCursor, file_name: str, table_n raise SnowflakeFileNotUploadedError(table_name, status, message) +async def copy_loaded_files_to_snowflake_table( + connection: SnowflakeConnection, + table_name: str, +): + """Execute a COPY query in Snowflake to load any files PUT into the table. + + The query is executed asynchronously using Snowflake's polling API. + + Args: + connection: A SnowflakeConnection as returned by snowflake.connector.connect. + table_name: The table we are COPY-ing files into. + """ + query = f""" + COPY INTO "{table_name}" + FILE_FORMAT = (TYPE = 'JSON') + MATCH_BY_COLUMN_NAME = CASE_SENSITIVE + PURGE = TRUE + """ + query_id = await execute_async_query(connection, query) + + cursor = connection.cursor() + cursor.get_results_from_sfqid(query_id) + results = cursor.fetchall() + + for query_result in results: + if not isinstance(query_result, tuple): + # Mostly to appease mypy, as this query should always return a tuple. + raise TypeError(f"Expected tuple from Snowflake COPY INTO query but got: '{type(query_result)}'") + + if len(query_result) < 2: + raise SnowflakeFileNotLoadedError( + table_name, + "NO STATUS", + 0, + query_result[0] if len(query_result) == 1 else "NO ERROR MESSAGE", + ) + + _, status = query_result[0:2] + + if status != "LOADED": + errors_seen, first_error = query_result[5:7] + raise SnowflakeFileNotLoadedError( + table_name, + status or "NO STATUS", + errors_seen or 0, + first_error or "NO ERROR MESSAGE", + ) + + @activity.defn async def insert_into_snowflake_activity(inputs: SnowflakeInsertInputs): """Activity streams data from ClickHouse to Snowflake. @@ -106,6 +305,17 @@ async def insert_into_snowflake_activity(inputs: SnowflakeInsertInputs): inputs.data_interval_end, ) + should_resume, details = await should_resume_from_activity_heartbeat(activity, SnowflakeHeartbeatDetails, logger) + + if should_resume is True and details is not None: + data_interval_start = details.last_inserted_at.isoformat() + last_inserted_at = details.last_inserted_at + file_no = details.file_no + else: + data_interval_start = inputs.data_interval_start + last_inserted_at = None + file_no = 0 + async with get_client() as client: if not await client.is_alive(): raise ConnectionError("Cannot establish connection to ClickHouse") @@ -113,7 +323,7 @@ async def insert_into_snowflake_activity(inputs: SnowflakeInsertInputs): count = await get_rows_count( client=client, team_id=inputs.team_id, - interval_start=inputs.data_interval_start, + interval_start=data_interval_start, interval_end=inputs.data_interval_end, exclude_events=inputs.exclude_events, include_events=inputs.include_events, @@ -129,42 +339,31 @@ async def insert_into_snowflake_activity(inputs: SnowflakeInsertInputs): logger.info("BatchExporting %s rows", count) - conn = snowflake.connector.connect( - user=inputs.user, - password=inputs.password, - account=inputs.account, - warehouse=inputs.warehouse, - database=inputs.database, - schema=inputs.schema, - role=inputs.role, - ) + rows_exported = get_rows_exported_metric() + bytes_exported = get_bytes_exported_metric() - try: - cursor = conn.cursor() - cursor.execute(f'USE DATABASE "{inputs.database}"') - cursor.execute(f'USE SCHEMA "{inputs.schema}"') - - # Create the table if it doesn't exist. Note that we use the same schema - # as the snowflake-plugin for backwards compatibility. - cursor.execute( - f""" - CREATE TABLE IF NOT EXISTS "{inputs.database}"."{inputs.schema}"."{inputs.table_name}" ( - "uuid" STRING, - "event" STRING, - "properties" VARIANT, - "elements" VARIANT, - "people_set" VARIANT, - "people_set_once" VARIANT, - "distinct_id" STRING, - "team_id" INTEGER, - "ip" STRING, - "site_url" STRING, - "timestamp" TIMESTAMP - ) - COMMENT = 'PostHog generated events table' - """ + async def flush_to_snowflake( + connection: SnowflakeConnection, + file: BatchExportTemporaryFile, + table_name: str, + file_no: int, + last: bool = False, + ): + logger.info( + "Putting %sfile %s containing %s records with size %s bytes", + "last " if last else "", + file_no, + file.records_since_last_reset, + file.bytes_since_last_reset, ) + await put_file_to_snowflake_table(connection, file, table_name, file_no) + rows_exported.add(file.records_since_last_reset) + bytes_exported.add(file.bytes_since_last_reset) + + with snowflake_connection(inputs) as connection: + await create_table_in_snowflake(connection, inputs.table_name) + results_iterator = get_results_iterator( client=client, team_id=inputs.team_id, @@ -173,118 +372,59 @@ async def insert_into_snowflake_activity(inputs: SnowflakeInsertInputs): exclude_events=inputs.exclude_events, include_events=inputs.include_events, ) + result = None - local_results_file = tempfile.NamedTemporaryFile(suffix=".jsonl") - rows_in_file = 0 - - rows_exported = get_rows_exported_metric() - bytes_exported = get_bytes_exported_metric() - - def flush_to_snowflake(lrf: tempfile._TemporaryFileWrapper, rows_in_file: int): - lrf.flush() - put_file_to_snowflake_table(cursor, lrf.name, inputs.table_name) - rows_exported.add(rows_in_file) - bytes_exported.add(lrf.tell()) - - try: - while True: - try: - result = results_iterator.__next__() - - except StopIteration: - break - - except json.JSONDecodeError: - logger.info( - "Failed to decode a JSON value while iterating, potentially due to a ClickHouse error" - ) - # This is raised by aiochclient as we try to decode an error message from ClickHouse. - # So far, this error message only indicated that we were too slow consuming rows. - # So, we can resume from the last result. - if result is None: - # We failed right at the beginning - new_interval_start = None - else: - new_interval_start = result.get("inserted_at", None) - - if not isinstance(new_interval_start, str): - new_interval_start = inputs.data_interval_start - - results_iterator = get_results_iterator( - client=client, - team_id=inputs.team_id, - interval_start=new_interval_start, # This means we'll generate at least one duplicate. - interval_end=inputs.data_interval_end, - ) - continue - - if not result: - break - - # Write the results to a local file - local_results_file.write(json.dumps(result).encode("utf-8")) - local_results_file.write("\n".encode("utf-8")) - rows_in_file += 1 - - # Write results to Snowflake when the file reaches 50MB and - # reset the file, or if there is nothing else to write. - if ( - local_results_file.tell() - and local_results_file.tell() > settings.BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES - ): - logger.info("Uploading to Snowflake") - - # Flush the file to make sure everything is written - flush_to_snowflake(local_results_file, rows_in_file) - - # Delete the temporary file and create a new one - local_results_file.close() - local_results_file = tempfile.NamedTemporaryFile(suffix=".jsonl") - rows_in_file = 0 - - # Flush the file to make sure everything is written - flush_to_snowflake(local_results_file, rows_in_file) - - # We don't need the file anymore, close (and delete) it. - local_results_file.close() - cursor.execute( - f""" - COPY INTO "{inputs.table_name}" - FILE_FORMAT = (TYPE = 'JSON') - MATCH_BY_COLUMN_NAME = CASE_SENSITIVE - PURGE = TRUE - """ - ) - results = cursor.fetchall() - - for query_result in results: - if not isinstance(query_result, tuple): - # Mostly to appease mypy, as this query should always return a tuple. - raise TypeError(f"Expected tuple from Snowflake COPY INTO query but got: '{type(result)}'") - - if len(query_result) < 2: - raise SnowflakeFileNotLoadedError( - inputs.table_name, - "NO STATUS", - 0, - query_result[1] if len(query_result) == 1 else "NO ERROR MESSAGE", - ) - - _, status = query_result[0:2] - - if status != "LOADED": - errors_seen, first_error = query_result[5:7] - raise SnowflakeFileNotLoadedError( - inputs.table_name, - status or "NO STATUS", - errors_seen or 0, - first_error or "NO ERROR MESSAGE", - ) - - finally: - local_results_file.close() - finally: - conn.close() + + async def worker_shutdown_handler(): + """Handle the Worker shutting down by heart-beating our latest status.""" + await activity.wait_for_worker_shutdown() + logger.bind(last_inserted_at=last_inserted_at, file_no=file_no).debug("Worker shutting down!") + + if last_inserted_at is None: + # Don't heartbeat if worker shuts down before we could even send anything + # Just start from the beginning again. + return + + activity.heartbeat(last_inserted_at, file_no) + + asyncio.create_task(worker_shutdown_handler()) + + with BatchExportTemporaryFile() as local_results_file: + for result in results_iterator: + record = { + "uuid": result["uuid"], + "event": result["event"], + "properties": result["properties"], + "elements": result["elements"], + "people_set": result["set"], + "people_set_once": result["set_once"], + "distinct_id": result["distinct_id"], + "team_id": result["team_id"], + "ip": result["ip"], + "site_url": result["site_url"], + "timestamp": result["timestamp"], + } + local_results_file.write_records_to_jsonl([record]) + + if local_results_file.tell() > settings.BATCH_EXPORT_SNOWFLAKE_UPLOAD_CHUNK_SIZE_BYTES: + await flush_to_snowflake(connection, local_results_file, inputs.table_name, file_no) + + last_inserted_at = result["inserted_at"] + file_no += 1 + + activity.heartbeat(last_inserted_at, file_no) + + local_results_file.reset() + + if local_results_file.tell() > 0 and result is not None: + await flush_to_snowflake(connection, local_results_file, inputs.table_name, file_no, last=True) + + last_inserted_at = result["inserted_at"] + file_no += 1 + + activity.heartbeat(last_inserted_at, file_no) + + await copy_loaded_files_to_snowflake_table(connection, inputs.table_name) @workflow.defn(name="snowflake-export") @@ -361,6 +501,4 @@ async def run(self, inputs: SnowflakeBatchExportInputs): "ForbiddenError", ], update_inputs=update_inputs, - # Disable heartbeat timeout until we add heartbeat support. - heartbeat_timeout_seconds=None, ) diff --git a/posthog/test/__snapshots__/test_feature_flag.ambr b/posthog/test/__snapshots__/test_feature_flag.ambr index 05f9873311799..da533ef9388f3 100644 --- a/posthog/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/test/__snapshots__/test_feature_flag.ambr @@ -140,7 +140,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -525,7 +526,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 diff --git a/posthog/test/base.py b/posthog/test/base.py index 3cd830a68a49d..21a2fc17c68b7 100644 --- a/posthog/test/base.py +++ b/posthog/test/base.py @@ -460,6 +460,13 @@ def assertQueryMatchesSnapshot(self, query, params=None, replace_all_numbers=Fal if replace_all_numbers: query = re.sub(r"(\"?) = \d+", r"\1 = 2", query) query = re.sub(r"(\"?) IN \(\d+(, \d+)*\)", r"\1 IN (1, 2, 3, 4, 5 /* ... */)", query) + # replace "uuid" IN ('00000000-0000-4000-8000-000000000001'::uuid) effectively: + query = re.sub( + r"\"uuid\" IN \('[0-9a-f-]{36}'(::uuid)?(, '[0-9a-f-]{36}'(::uuid)?)*\)", + r""""uuid" IN ('00000000-0000-0000-0000-000000000000'::uuid, '00000000-0000-0000-0000-000000000001'::uuid /* ... */)\n""", + query, + ) + else: query = re.sub(r"(team|cohort)_id(\"?) = \d+", r"\1_id\2 = 2", query) query = re.sub(r"\d+ as (team|cohort)_id(\"?)", r"2 as \1_id\2", query) @@ -468,6 +475,9 @@ def assertQueryMatchesSnapshot(self, query, params=None, replace_all_numbers=Fal query = re.sub(r"flag_\d+_condition", r"flag_X_condition", query) query = re.sub(r"flag_\d+_super_condition", r"flag_X_super_condition", query) + # replace django cursors + query = re.sub(r"_django_curs_[0-9sync_]*\"", r'_django_curs_X"', query) + # hog ql checks team ids differently query = re.sub( r"equals\(([^.]+\.)?team_id?, \d+\)", diff --git a/posthog/test/test_feature_flag.py b/posthog/test/test_feature_flag.py index da323dd5cb32f..d1056f26e5722 100644 --- a/posthog/test/test_feature_flag.py +++ b/posthog/test/test_feature_flag.py @@ -3458,6 +3458,80 @@ def test_cohort_filters_with_override_properties(self): FeatureFlagMatch(True, None, FeatureFlagMatchReason.CONDITION_MATCH, 0), ) + def test_cohort_filters_with_override_id_property(self): + cohort1 = Cohort.objects.create( + team=self.team, + groups=[ + { + "properties": [ + { + "key": "email", + "type": "person", + "value": "@posthog.com", + "negation": False, + "operator": "icontains", + } + ] + } + ], + name="cohort1", + ) + + feature_flag1: FeatureFlag = self.create_feature_flag( + key="x1", + filters={"groups": [{"properties": [{"key": "id", "value": cohort1.pk, "type": "cohort"}]}]}, + ) + Person.objects.create( + team=self.team, + distinct_ids=["example_id"], + properties={"email": "tim@posthog.com"}, + ) + + with self.assertNumQueries(5): + self.assertEqual( + FeatureFlagMatcher( + [feature_flag1], + "example_id", + property_value_overrides={}, + ).get_match(feature_flag1), + FeatureFlagMatch(True, None, FeatureFlagMatchReason.CONDITION_MATCH, 0), + ) + + with self.assertNumQueries(4): + # no local computation because cohort lookup is required + # no postgres person query required here to get the person, because email is sufficient + # property id override shouldn't confuse the matcher + self.assertEqual( + FeatureFlagMatcher( + [feature_flag1], + "example_id", + property_value_overrides={"id": "example_id", "email": "bzz"}, + ).get_match(feature_flag1), + FeatureFlagMatch(False, None, FeatureFlagMatchReason.NO_CONDITION_MATCH, 0), + ) + + with self.assertNumQueries(4): + # no postgres query required here to get the person + self.assertEqual( + FeatureFlagMatcher( + [feature_flag1], + "example_id", + property_value_overrides={"id": "second_id", "email": "neil@posthog.com"}, + ).get_match(feature_flag1), + FeatureFlagMatch(True, None, FeatureFlagMatchReason.CONDITION_MATCH, 0), + ) + + with self.assertNumQueries(5): + # postgres query required here to get the person + self.assertEqual( + FeatureFlagMatcher( + [feature_flag1], + "example_id", + property_value_overrides={"id": "second_id"}, + ).get_match(feature_flag1), + FeatureFlagMatch(True, None, FeatureFlagMatchReason.CONDITION_MATCH, 0), + ) + @pytest.mark.skip("This case is not supported yet") def test_complex_cohort_filter_with_override_properties(self): # TODO: Currently we don't support this case for persons who haven't been ingested yet diff --git a/posthog/test/test_middleware.py b/posthog/test/test_middleware.py index b5efb9c731891..88e2ba6813f6e 100644 --- a/posthog/test/test_middleware.py +++ b/posthog/test/test_middleware.py @@ -116,7 +116,7 @@ class TestAutoProjectMiddleware(APIBaseTest): @classmethod def setUpTestData(cls): super().setUpTestData() - cls.base_app_num_queries = 40 + cls.base_app_num_queries = 41 # Create another team that the user does have access to cls.second_team = Team.objects.create(organization=cls.organization, name="Second Life") diff --git a/posthog/test/test_user_permissions.py b/posthog/test/test_user_permissions.py index da9faef7330ad..b0562dbca57af 100644 --- a/posthog/test/test_user_permissions.py +++ b/posthog/test/test_user_permissions.py @@ -321,7 +321,7 @@ def test_dashboard_efficiency(self): assert user_permissions.insight(insight).effective_privilege_level is not None def test_team_lookup_efficiency(self): - user = User.objects.create(email="test2@posthog.com") + user = User.objects.create(email="test2@posthog.com", distinct_id="test2") models = [] for _ in range(10): organization, membership, team = Organization.objects.bootstrap( diff --git a/posthog/warehouse/api/external_data_source.py b/posthog/warehouse/api/external_data_source.py index 3bfcc64e497d1..7510ec26cd02b 100644 --- a/posthog/warehouse/api/external_data_source.py +++ b/posthog/warehouse/api/external_data_source.py @@ -10,7 +10,7 @@ from posthog.warehouse.external_data_source.source import StripeSourcePayload, create_stripe_source, delete_source from posthog.warehouse.external_data_source.connection import create_connection, start_sync from posthog.warehouse.external_data_source.destination import create_destination, delete_destination -from posthog.warehouse.sync_resource import sync_resource +from posthog.tasks.warehouse import sync_resource from posthog.api.routing import StructuredViewSetMixin from rest_framework.decorators import action diff --git a/posthog/warehouse/api/test/test_view_link.py b/posthog/warehouse/api/test/test_view_link.py index 3a2dcae6bf160..0bcb57e187b86 100644 --- a/posthog/warehouse/api/test/test_view_link.py +++ b/posthog/warehouse/api/test/test_view_link.py @@ -2,7 +2,7 @@ APIBaseTest, ) from posthog.warehouse.models import DataWarehouseViewLink, DataWarehouseSavedQuery -from posthog.api.query import process_query +from posthog.api.services.query import process_query class TestViewLinkQuery(APIBaseTest): diff --git a/posthog/warehouse/external_data_source/connection.py b/posthog/warehouse/external_data_source/connection.py index fc89f22abb65b..9a37222f9d8d4 100644 --- a/posthog/warehouse/external_data_source/connection.py +++ b/posthog/warehouse/external_data_source/connection.py @@ -37,6 +37,22 @@ def create_connection(source_id: str, destination_id: str) -> ExternalDataConnec ) +def activate_connection_by_id(connection_id: str): + update_connection_status_by_id(connection_id, "active") + + +def deactivate_connection_by_id(connection_id: str): + update_connection_status_by_id(connection_id, "inactive") + + +def update_connection_status_by_id(connection_id: str, status: str): + connection_id_url = f"{AIRBYTE_CONNECTION_URL}/{connection_id}" + + payload = {"status": status} + + send_request(connection_id_url, method="PATCH", payload=payload) + + def update_connection_stream(connection_id: str): connection_id_url = f"{AIRBYTE_CONNECTION_URL}/{connection_id}" diff --git a/posthog/warehouse/external_data_source/workspace.py b/posthog/warehouse/external_data_source/workspace.py index e92c07fc888cd..ceb8ed50ac33f 100644 --- a/posthog/warehouse/external_data_source/workspace.py +++ b/posthog/warehouse/external_data_source/workspace.py @@ -1,6 +1,7 @@ from posthog.models import Team from posthog.warehouse.external_data_source.client import send_request from django.conf import settings +import datetime AIRBYTE_WORKSPACE_URL = "https://api.airbyte.com/v1/workspaces" @@ -23,6 +24,8 @@ def get_or_create_workspace(team_id: int): if not team.external_data_workspace_id: workspace_id = create_workspace(team_id) team.external_data_workspace_id = workspace_id + # start tracking from now + team.external_data_workspace_last_synced_at = datetime.datetime.now(datetime.timezone.utc) team.save() return team.external_data_workspace_id diff --git a/posthog/warehouse/models/datawarehouse_saved_query.py b/posthog/warehouse/models/datawarehouse_saved_query.py index bca809bb30912..9117fa7c4eaf0 100644 --- a/posthog/warehouse/models/datawarehouse_saved_query.py +++ b/posthog/warehouse/models/datawarehouse_saved_query.py @@ -47,7 +47,7 @@ class Meta: ] def get_columns(self) -> Dict[str, str]: - from posthog.api.query import process_query + from posthog.api.services.query import process_query # TODO: catch and raise error response = process_query(self.team, self.query) diff --git a/posthog/warehouse/sync_resource.py b/posthog/warehouse/sync_resource.py deleted file mode 100644 index 3072bf43986d9..0000000000000 --- a/posthog/warehouse/sync_resource.py +++ /dev/null @@ -1,69 +0,0 @@ -from posthog.warehouse.models.external_data_source import ExternalDataSource -from posthog.warehouse.models import DataWarehouseCredential, DataWarehouseTable -from posthog.warehouse.external_data_source.connection import retrieve_sync -from posthog.celery import app - -from django.conf import settings -import structlog - -logger = structlog.get_logger(__name__) - - -def sync_resources(): - resources = ExternalDataSource.objects.filter(are_tables_created=False, status__in=["running", "error"]) - - for resource in resources: - sync_resource.delay(resource.pk) - - -@app.task(ignore_result=True) -def sync_resource(resource_id): - resource = ExternalDataSource.objects.get(pk=resource_id) - - try: - job = retrieve_sync(resource.connection_id) - except Exception as e: - logger.exception("Data Warehouse: Sync Resource failed with an unexpected exception.", exc_info=e) - resource.status = "error" - resource.save() - return - - if job is None: - logger.error(f"Data Warehouse: No jobs found for connection: {resource.connection_id}") - resource.status = "error" - resource.save() - return - - if job["status"] == "succeeded": - resource = ExternalDataSource.objects.get(pk=resource_id) - credential, _ = DataWarehouseCredential.objects.get_or_create( - team_id=resource.team.pk, - access_key=settings.AIRBYTE_BUCKET_KEY, - access_secret=settings.AIRBYTE_BUCKET_SECRET, - ) - - data = { - "credential": credential, - "name": "stripe_customers", - "format": "Parquet", - "url_pattern": f"https://{settings.AIRBYTE_BUCKET_DOMAIN}/airbyte/{resource.team.pk}/customers/*.parquet", - "team_id": resource.team.pk, - } - - table = DataWarehouseTable(**data) - try: - table.columns = table.get_columns() - except Exception as e: - logger.exception( - f"Data Warehouse: Sync Resource failed with an unexpected exception for connection: {resource.connection_id}", - exc_info=e, - ) - else: - table.save() - - resource.are_tables_created = True - resource.status = job["status"] - resource.save() - else: - resource.status = job["status"] - resource.save() diff --git a/requirements.txt b/requirements.txt index e2fa2679f01ad..28c3c88e6c217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ aiobotocore[boto3]==2.5.0 # via # aioboto3 # aiobotocore -aiohttp==3.8.5 +aiohttp==3.8.6 # via # -r requirements.in # aiobotocore diff --git a/tsconfig.json b/tsconfig.json index 658bedd03e802..d00e7af6a118b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,7 @@ "lib": ["dom", "es2019"] }, "include": ["frontend/**/*", ".storybook/**/*"], - "exclude": ["node_modules/**/*", "staticfiles/**/*", "frontend/dist/**/*", "plugin-server/**/*"], + "exclude": ["frontend/dist/**/*"], "ts-node": { "compilerOptions": { "module": "commonjs"