diff --git a/.dockerignore b/.dockerignore index 79a794a0401f78..d06d9902403393 100644 --- a/.dockerignore +++ b/.dockerignore @@ -34,3 +34,7 @@ !share/GeoLite2-City.mmdb !hogvm/python !unit.json +!plugin-transpiler/src +!plugin-transpiler/*.* +!test-runner-jest.config.js +!test-runner-jest-environment.js diff --git a/.eslintrc.js b/.eslintrc.js index 9d54f523057d07..d6285afeb1b8f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -82,11 +82,15 @@ module.exports = { name: 'dayjs', message: 'Do not directly import dayjs. Only import the dayjs exported from lib/dayjs.', }, + { + name: '@ant-design/icons', + message: 'Please use icons from the @posthog/icons package instead', + }, ], }, ], 'react/forbid-dom-props': [ - 1, + 'warn', { forbid: [ { @@ -98,7 +102,7 @@ module.exports = { }, ], 'posthog/warn-elements': [ - 1, + 'warn', { forbid: [ { @@ -142,11 +146,15 @@ module.exports = { element: 'LemonButtonWithDropdown', message: 'use with a child instead', }, + { + element: 'Tag', + message: 'use instead', + }, ], }, ], 'react/forbid-elements': [ - 2, + 'error', { forbid: [ { @@ -200,9 +208,10 @@ module.exports = { ], }, ], - 'no-constant-condition': 0, - 'no-prototype-builtins': 0, - 'no-irregular-whitespace': 0, + 'no-constant-binary-expression': 'error', + 'no-constant-condition': 'off', + 'no-prototype-builtins': 'off', + 'no-irregular-whitespace': 'off', }, overrides: [ { diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4dd278f24632e4..606ff5826450ed 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -19,8 +19,8 @@ labels: bug ## Environment -- [ ] PostHog Cloud US, project ID: [please provide from https://app.posthog.com/project/settings#project-id] -- [ ] PostHog Cloud EU, project ID: [please provide from https://eu.posthog.com/project/settings#project-id] +- [ ] PostHog Cloud US, project ID: [please provide from https://app.posthog.com/settings/project-details#variables] +- [ ] PostHog Cloud EU, project ID: [please provide from https://eu.posthog.com/settings/project-details#variables] - [ ] PostHog Hobby self-hosted with `docker compose`, version/commit: [please provide] - [ ] PostHog self-hosted with Kubernetes (deprecated, see ["Sunsetting Kubernetes support"](https://posthog.com/blog/sunsetting-helm-support-posthog)), version/commit: [please provide] diff --git a/.github/actions/run-backend-tests/action.yml b/.github/actions/run-backend-tests/action.yml index 45e189e23b1b6c..edd36992a66149 100644 --- a/.github/actions/run-backend-tests/action.yml +++ b/.github/actions/run-backend-tests/action.yml @@ -53,14 +53,33 @@ runs: shell: bash id: hogql-parser-diff run: | - changed=$(git diff --quiet HEAD master -- hogql_parser/ && echo "false" || echo "true") - echo "::set-output name=changed::$changed" + git fetch --no-tags --prune --depth=1 origin + changed=$(git diff --quiet HEAD origin/master -- hogql_parser/ && echo "false" || echo "true") + echo "changed=$changed" >> $GITHUB_OUTPUT - name: Install SAML (python3-saml) dependencies shell: bash run: | sudo apt-get update && sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8.x.x + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + + - name: Install plugin-transpiler + shell: bash + run: | + cd plugin-transpiler + pnpm install + pnpm run build + - uses: syphar/restore-virtualenv@v1 id: cache-backend-tests with: diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 191b481a045d2d..caf194fc9c1b86 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,7 +18,7 @@ jobs: environment: clickhouse-benchmarks # Benchmarks are expensive to run so we only run them (periodically) against master branch and for PRs labeled `performance` - if: ${{ github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'performance') }} + if: ${{ github.repository == 'PostHog/posthog' && (github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'performance')) }} env: DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 845c4a60a46977..f80300fca0a56b 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -68,6 +68,7 @@ jobs: - mypy.ini - pytest.ini - frontend/src/queries/schema.json # Used for generating schema.py + - plugin-transpiler/src # Used for transpiling plugins # Make sure we run if someone is explicitly change the workflow - .github/workflows/ci-backend.yml - .github/actions/run-backend-tests/action.yml diff --git a/.github/workflows/container-images-cd.yml b/.github/workflows/container-images-cd.yml index 5345308504bb3d..a433c23be2317b 100644 --- a/.github/workflows/container-images-cd.yml +++ b/.github/workflows/container-images-cd.yml @@ -80,7 +80,7 @@ jobs: push: true file: production-unit.Dockerfile tags: ${{ steps.aws-ecr.outputs.registry }}/posthog-cloud:unit - platforms: linux/amd64 + platforms: linux/arm64,linux/amd64 build-args: COMMIT_HASH=${{ github.sha }} - name: get deployer token diff --git a/.github/workflows/storybook-chromatic.yml b/.github/workflows/storybook-chromatic.yml index ace0ae3e79349f..02397ca2586db1 100644 --- a/.github/workflows/storybook-chromatic.yml +++ b/.github/workflows/storybook-chromatic.yml @@ -64,7 +64,6 @@ jobs: SHARD_COUNT: '2' CYPRESS_INSTALL_BINARY: '0' NODE_OPTIONS: --max-old-space-size=6144 - JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE: '1' # Remove obsolete snapshots OPT_OUT_CAPTURE: 1 outputs: # The below have to be manually listed unfortunately, as GitHub Actions doesn't allow matrix-dependent outputs @@ -132,7 +131,7 @@ jobs: retries=3 while [ $retries -gt 0 ]; do pnpm exec http-server storybook-static --port 6006 --silent & - if pnpm wait-on http://127.0.0.1:6006 --timeout 60; then + if pnpm wait-on http://127.0.0.1:6006 --timeout 15; then break fi retries=$((retries-1)) @@ -146,14 +145,7 @@ jobs: # 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' }} run: | - retries=3 - while [ $retries -gt 0 ]; do - if pnpm test:visual-regression:stories:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT; then - break - fi - retries=$((retries-1)) - echo "Failed @storybook/test-runner, retrying... ($retries retries left)" - done + pnpm test:visual-regression:stories:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT - name: Run @playwright/test (legacy, Chromium-only) if: matrix.browser == 'chromium' && matrix.shard == 1 @@ -163,6 +155,13 @@ jobs: run: | pnpm test:visual-regression:legacy:ci:$VARIANT + - name: Archive failure screenshots + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: failure-screenshots-${{ matrix.browser }} + path: frontend/__snapshots__/__failures__/ + - name: Count and optimize updated snapshots id: diff # Skip on forks diff --git a/.gitignore b/.gitignore index 8f32f6b56d3a74..95a429f2d24e11 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ frontend/.cache/ frontend/dist/ frontend/types/ frontend/__snapshots__/__diff_output__/ +frontend/__snapshots__/__failures__/ *Type.ts frontend/pnpm-error.log frontend/tmp @@ -55,3 +56,4 @@ gen/ upgrade/ hogvm/typescript/dist .wokeignore +plugin-transpiler/dist diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index fbb572e5382377..cf02d028151b9f 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -48,36 +48,35 @@ declare module '@storybook/types' { } } -const RETRY_TIMES = 5 +const RETRY_TIMES = 3 const LOADER_SELECTORS = [ '.ant-skeleton', '.Spinner', '.LemonSkeleton', '.LemonTableLoader', + '.Toastify__toast-container', '[aria-busy="true"]', - '[aria-label="Content is loading..."]', '.SessionRecordingPlayer--buffering', '.Lettermark--unknown', ] const customSnapshotsDir = `${process.cwd()}/frontend/__snapshots__` -const TEST_TIMEOUT_MS = 10000 -const BROWSER_DEFAULT_TIMEOUT_MS = 9000 // Reduce the default timeout down from 30s, to pre-empt Jest timeouts -const SCREENSHOT_TIMEOUT_MS = 9000 +const JEST_TIMEOUT_MS = 15000 +const PLAYWRIGHT_TIMEOUT_MS = 10000 // Must be shorter than JEST_TIMEOUT_MS module.exports = { setup() { expect.extend({ toMatchImageSnapshot }) jest.retryTimes(RETRY_TIMES, { logErrorsBeforeRetry: true }) - jest.setTimeout(TEST_TIMEOUT_MS) + jest.setTimeout(JEST_TIMEOUT_MS) }, async postRender(page, context) { const browserContext = page.context() const storyContext = (await getStoryContext(page, context)) as StoryContext const { skip = false, snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {} - browserContext.setDefaultTimeout(BROWSER_DEFAULT_TIMEOUT_MS) + browserContext.setDefaultTimeout(PLAYWRIGHT_TIMEOUT_MS) if (!skip) { const currentBrowser = browserContext.browser()!.browserType().name() as SupportedBrowserName if (snapshotBrowsers.includes(currentBrowser)) { @@ -202,7 +201,7 @@ async function expectLocatorToMatchStorySnapshot( browser: SupportedBrowserName, options?: LocatorScreenshotOptions ): Promise { - const image = await locator.screenshot({ timeout: SCREENSHOT_TIMEOUT_MS, ...options }) + const image = await locator.screenshot({ ...options }) let customSnapshotIdentifier = context.id if (browser !== 'chromium') { customSnapshotIdentifier += `--${browser}` diff --git a/.vscode/launch.json b/.vscode/launch.json index 497b5f7fca0bfc..22bf39b7180194 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -70,7 +70,8 @@ "DATABASE_URL": "postgres://posthog:posthog@localhost:5432/posthog", "SKIP_SERVICE_VERSION_REQUIREMENTS": "1", "PRINT_SQL": "1", - "REPLAY_EVENTS_NEW_CONSUMER_RATIO": "1.0" + "REPLAY_EVENTS_NEW_CONSUMER_RATIO": "1.0", + "BILLING_SERVICE_URL": "https://billing.dev.posthog.dev" }, "console": "integratedTerminal", "python": "${workspaceFolder}/env/bin/python", diff --git a/Dockerfile.playwright b/Dockerfile.playwright index 303b3ad7ab2260..7591a53141b182 100644 --- a/Dockerfile.playwright +++ b/Dockerfile.playwright @@ -15,6 +15,6 @@ ENV CYPRESS_INSTALL_BINARY=0 RUN pnpm install --frozen-lockfile -COPY playwright.config.ts webpack.config.js babel.config.js tsconfig.json ./ +COPY playwright.config.ts webpack.config.js babel.config.js tsconfig.json test-runner-jest.config.js test-runner-jest-environment.js ./ COPY .storybook/ .storybook/ diff --git a/bin/docker-server-unit b/bin/docker-server-unit index 1eda8374759a58..f1caab6a711745 100755 --- a/bin/docker-server-unit +++ b/bin/docker-server-unit @@ -10,4 +10,6 @@ trap 'rm -rf "$PROMETHEUS_MULTIPROC_DIR"' EXIT export PROMETHEUS_METRICS_EXPORT_PORT=8001 export STATSD_PORT=${STATSD_PORT:-8125} -exec /usr/local/bin/docker-entrypoint.sh unitd --no-daemon +# We need to run as --user root so that nginx unit can proxy the control socket for stats +# However each application is run as "nobody" +exec /usr/local/bin/docker-entrypoint.sh unitd --no-daemon --user root diff --git a/bin/temporal-django-worker b/bin/temporal-django-worker index 4f7462879e285e..ab10a959a87090 100755 --- a/bin/temporal-django-worker +++ b/bin/temporal-django-worker @@ -4,6 +4,6 @@ set -e trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -python3 manage.py start_temporal_worker +python3 manage.py start_temporal_worker "$@" wait diff --git a/bin/unit_metrics.py b/bin/unit_metrics.py new file mode 100644 index 00000000000000..227139cf10b09a --- /dev/null +++ b/bin/unit_metrics.py @@ -0,0 +1,78 @@ +import http.client +import json +from prometheus_client import CollectorRegistry, Gauge, multiprocess, generate_latest + +UNIT_CONNECTIONS_ACCEPTED_TOTAL = Gauge( + "unit_connections_accepted_total", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_ACTIVE = Gauge( + "unit_connections_active", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_CLOSED = Gauge( + "unit_connections_closed", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_IDLE = Gauge( + "unit_connections_idle", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_TOTAL = Gauge( + "unit_requests_total", + "", + multiprocess_mode="livesum", +) +UNIT_PROCESSES_RUNNING_GAUGE = Gauge( + "unit_application_processes_running", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_PROCESSES_STARTING_GAUGE = Gauge( + "unit_application_processes_starting", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_PROCESSES_IDLE_GAUGE = Gauge( + "unit_application_processes_idle", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_REQUESTS_ACTIVE_GAUGE = Gauge( + "unit_application_requests_active", "", multiprocess_mode="livesum", labelnames=["application"] +) + + +def application(environ, start_response): + connection = http.client.HTTPConnection("localhost:8081") + connection.request("GET", "/status") + response = connection.getresponse() + + statj = json.loads(response.read()) + connection.close() + + UNIT_CONNECTIONS_ACCEPTED_TOTAL.set(statj["connections"]["accepted"]) + UNIT_CONNECTIONS_ACTIVE.set(statj["connections"]["active"]) + UNIT_CONNECTIONS_IDLE.set(statj["connections"]["idle"]) + UNIT_CONNECTIONS_CLOSED.set(statj["connections"]["closed"]) + UNIT_CONNECTIONS_TOTAL.set(statj["requests"]["total"]) + + for application in statj["applications"].keys(): + UNIT_PROCESSES_RUNNING_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["running"] + ) + UNIT_PROCESSES_STARTING_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["starting"] + ) + UNIT_PROCESSES_IDLE_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["idle"] + ) + UNIT_REQUESTS_ACTIVE_GAUGE.labels(application=application).set( + statj["applications"][application]["requests"]["active"] + ) + + start_response("200 OK", [("Content-Type", "text/plain")]) + # Create the prometheus multi-process metric registry here + # This will aggregate metrics we send from the Django app + # We prepend our unit metrics here. + registry = CollectorRegistry() + multiprocess.MultiProcessCollector(registry) + yield generate_latest(registry) diff --git a/cypress/e2e/a11y.cy.ts b/cypress/e2e/a11y.cy.ts index 3ee87daf2a107f..1d91fe803446eb 100644 --- a/cypress/e2e/a11y.cy.ts +++ b/cypress/e2e/a11y.cy.ts @@ -16,12 +16,10 @@ describe('a11y', () => { 'experiments', 'events', 'datamanagement', - 'persons', - 'cohorts', - 'annotations', + 'personsmanagement', 'apps', 'toolbarlaunch', - 'projectsettings', + 'settings', ] sidebarItems.forEach((sideBarItem) => { diff --git a/cypress/e2e/annotations.cy.ts b/cypress/e2e/annotations.cy.ts index 5300b376a65597..90db0533267703 100644 --- a/cypress/e2e/annotations.cy.ts +++ b/cypress/e2e/annotations.cy.ts @@ -1,10 +1,10 @@ describe('Annotations', () => { beforeEach(() => { - cy.clickNavMenu('annotations') + cy.clickNavMenu('datamanagement') + cy.get('[data-attr=data-management-annotations-tab]').click() }) it('Annotations loaded', () => { - cy.get('h1').should('contain', 'Annotations') cy.get('h2').should('contain', 'Create your first annotation') cy.get('[data-attr="product-introduction-docs-link"]').should('contain', 'Learn more about Annotations') }) diff --git a/cypress/e2e/billingv2.cy.ts b/cypress/e2e/billingv2.cy.ts index 5c25ddeee4a535..3c01f489e1f787 100644 --- a/cypress/e2e/billingv2.cy.ts +++ b/cypress/e2e/billingv2.cy.ts @@ -5,10 +5,10 @@ describe('Billing', () => { cy.visit('/organization/billing') }) - it('Show unsubscribe survey', () => { + it('Show and submit unsubscribe survey', () => { 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( @@ -17,7 +17,8 @@ describe('Billing', () => { ) cy.contains('.LemonModal .LemonButton', 'Unsubscribe').click() - cy.get('[data-attr=upgrade-card-product_analytics]').should('be.visible') + cy.get('.LemonModal').should('not.exist') + cy.wait(['@unsubscribeProductAnalytics']) }) it('Unsubscribe survey text area maintains unique state between product types', () => { diff --git a/cypress/e2e/cohorts.cy.ts b/cypress/e2e/cohorts.cy.ts index 267b579e79021c..1c7e3be39865d8 100644 --- a/cypress/e2e/cohorts.cy.ts +++ b/cypress/e2e/cohorts.cy.ts @@ -1,17 +1,22 @@ describe('Cohorts', () => { + const goToCohorts = (): void => { + cy.clickNavMenu('personsmanagement') + cy.get('[data-attr=persons-management-cohorts-tab]').click() + } + beforeEach(() => { - cy.clickNavMenu('cohorts') + goToCohorts() }) it('Cohorts new and list', () => { // load an empty page - cy.get('h1').should('contain', 'Cohorts') - cy.title().should('equal', 'Cohorts • PostHog') + cy.get('h1').should('contain', 'People') + cy.title().should('equal', 'Cohorts • People • PostHog') cy.get('h2').should('contain', 'Create your first cohort') cy.get('[data-attr="product-introduction-docs-link"]').should('contain', 'Learn more about Cohorts') // go to create a new cohort - cy.get('[data-attr="create-cohort"]').click() + cy.get('[data-attr="new-cohort"]').click() // select "add filter" and "property" cy.get('[data-attr="cohort-selector-field-value"]').click() @@ -34,7 +39,7 @@ describe('Cohorts', () => { cy.get('[data-attr=success-toast]').contains('Cohort saved').should('exist') // back to cohorts - cy.clickNavMenu('cohorts') + goToCohorts() cy.get('tbody').contains('Test Cohort') cy.get('h2').should('not.have.text', 'Create your first cohort') @@ -69,7 +74,7 @@ describe('Cohorts', () => { // delete cohort cy.get('[data-attr="more-button"]').click() cy.get('.Popover__content').contains('Delete cohort').click() - cy.clickNavMenu('cohorts') + goToCohorts() cy.get('tbody').should('not.have.text', 'Test Cohort (dynamic copy) (static copy)') }) }) diff --git a/cypress/e2e/insights-unsaved-confirmation.cy.ts b/cypress/e2e/insights-unsaved-confirmation.cy.ts index 6257fc264e20f7..9d64c3d93df412 100644 --- a/cypress/e2e/insights-unsaved-confirmation.cy.ts +++ b/cypress/e2e/insights-unsaved-confirmation.cy.ts @@ -38,11 +38,9 @@ describe('Insights', () => { const insightName = randomString('to save and then navigate away from') insight.create(insightName) - cy.get('[data-attr="menu-item-annotations"]').click() + cy.get('[data-attr="menu-item-dashboards"]').click() - // the annotations API call is made before the annotations page loads, so we can't wait for it - cy.get('[data-attr="annotations-content"]').should('exist') - cy.url().should('include', '/annotations') + cy.url().should('include', '/dashboard') }) it('Can keep editing changed new insight after navigating away with confirm() rejection (case 1)', () => { diff --git a/cypress/e2e/invites.cy.ts b/cypress/e2e/invites.cy.ts index c05c404ea9420a..6a1c9b9552dafc 100644 --- a/cypress/e2e/invites.cy.ts +++ b/cypress/e2e/invites.cy.ts @@ -8,8 +8,9 @@ describe('Invite Signup', () => { cy.get('[data-attr=top-menu-toggle]').click() cy.get('[data-attr=top-menu-item-org-settings]').click() - cy.location('pathname').should('eq', '/organization/settings') - cy.get('[id="invites"]').contains('Pending Invites').should('exist') + cy.location('pathname').should('eq', '/settings/organization') + cy.get('[id="invites"]').should('exist') + cy.get('h2').contains('Pending Invites').should('exist') // Test invite creation flow cy.get('[data-attr=invite-teammate-button]').click() @@ -66,7 +67,7 @@ describe('Invite Signup', () => { cy.get('[data-attr=top-menu-toggle]').click() cy.get('[data-attr=top-menu-item-org-settings]').click() - cy.location('pathname').should('include', '/organization/settings') + cy.location('pathname').should('include', '/settings/organization') // Click "Invite team member" cy.get('[data-attr=invite-teammate-button]').first().click() @@ -97,8 +98,7 @@ describe('Invite Signup', () => { // Go to organization settings cy.get('[data-attr=top-menu-toggle]').click() cy.get('[data-attr=top-menu-item-org-settings]').click() - cy.location('pathname').should('include', '/organization/settings') - cy.get('.page-title').should('contain', 'Organization') + cy.location('pathname').should('include', '/settings/organization') // Change membership level cy.contains('[data-attr=org-members-table] tr', user).within(() => { diff --git a/cypress/e2e/organizationSettings.cy.ts b/cypress/e2e/organizationSettings.cy.ts index ea4264c58988b4..38f27a7df95773 100644 --- a/cypress/e2e/organizationSettings.cy.ts +++ b/cypress/e2e/organizationSettings.cy.ts @@ -3,7 +3,6 @@ describe('Organization settings', () => { it('can navigate to organization settings', () => { cy.get('[data-attr=top-menu-toggle]').click() cy.get('[data-attr=top-menu-item-org-settings]').click() - cy.location('pathname').should('include', '/organization/settings') - cy.get('.page-title').should('contain', 'Organization') + cy.location('pathname').should('include', '/settings/organization') }) }) diff --git a/cypress/e2e/person.cy.ts b/cypress/e2e/person.cy.ts index a4af1f83e9f40a..5a5b1d2856e32b 100644 --- a/cypress/e2e/person.cy.ts +++ b/cypress/e2e/person.cy.ts @@ -1,8 +1,8 @@ describe('Person Visualization Check', () => { beforeEach(() => { - cy.clickNavMenu('persons') + cy.clickNavMenu('personsmanagement') cy.location('pathname').should('eq', '/persons') - cy.get('.ant-spin-spinning').should('not.exist') // Wait until initial table load to be able to use the search + cy.wait(1000) cy.get('[data-attr=persons-search]').type('deb').should('have.value', 'deb') cy.contains('deborah.fernandez@gmail.com').should('not.exist') cy.contains('deborah.fernandez@gmail.com').click() diff --git a/cypress/e2e/persons.cy.ts b/cypress/e2e/persons.cy.ts index 79765fcf8c8756..1492c6077465a9 100644 --- a/cypress/e2e/persons.cy.ts +++ b/cypress/e2e/persons.cy.ts @@ -1,10 +1,10 @@ describe('Persons', () => { beforeEach(() => { - cy.clickNavMenu('persons') + cy.clickNavMenu('personsmanagement') }) it('All tabs work', () => { - cy.get('h1').should('contain', 'Persons') + cy.get('h1').should('contain', 'People') cy.get('[data-attr=persons-search]').type('marisol').type('{enter}').should('have.value', 'marisol') cy.wait(200) cy.get('[data-row-key]').its('length').should('be.gte', 0) diff --git a/cypress/e2e/trends.cy.ts b/cypress/e2e/trends.cy.ts index de16e4a00021d0..f04a247597cc23 100644 --- a/cypress/e2e/trends.cy.ts +++ b/cypress/e2e/trends.cy.ts @@ -140,7 +140,7 @@ describe('Trends', () => { it('Apply date filter', () => { cy.get('[data-attr=date-filter]').click() cy.get('div').contains('Yesterday').should('exist').click() - cy.get('.trends-insights-container .insight-empty-state').should('exist') + cy.get('[data-attr=trend-line-graph]').should('exist') }) it('Apply property breakdown', () => { diff --git a/ee/clickhouse/models/test/test_filters.py b/ee/clickhouse/models/test/test_filters.py index 26ff79c565c4e2..def3ca4493fe1b 100644 --- a/ee/clickhouse/models/test/test_filters.py +++ b/ee/clickhouse/models/test/test_filters.py @@ -610,6 +610,42 @@ def test_numerical(self): events = _filter_events(filter, self.team) self.assertEqual(events[0]["id"], event1_uuid) + def test_numerical_person_properties(self): + _create_person(team_id=self.team.pk, distinct_ids=["p1"], properties={"$a_number": 4}) + _create_person(team_id=self.team.pk, distinct_ids=["p2"], properties={"$a_number": 5}) + _create_person(team_id=self.team.pk, distinct_ids=["p3"], properties={"$a_number": 6}) + + filter = Filter( + data={ + "properties": [ + { + "type": "person", + "key": "$a_number", + "value": 4, + "operator": "gt", + } + ] + } + ) + self.assertEqual(len(_filter_persons(filter, self.team)), 2) + + filter = Filter(data={"properties": [{"type": "person", "key": "$a_number", "value": 5}]}) + self.assertEqual(len(_filter_persons(filter, self.team)), 1) + + filter = Filter( + data={ + "properties": [ + { + "type": "person", + "key": "$a_number", + "value": 6, + "operator": "lt", + } + ] + } + ) + self.assertEqual(len(_filter_persons(filter, self.team)), 2) + def test_contains(self): _create_event(team=self.team, distinct_id="test", event="$pageview") event2_uuid = _create_event( diff --git a/ee/clickhouse/queries/test/__snapshots__/test_retention.ambr b/ee/clickhouse/queries/test/__snapshots__/test_retention.ambr index 6a1c2323a37651..10cc23278229d4 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_retention.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_retention.ambr @@ -5,7 +5,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -17,13 +17,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -70,7 +70,7 @@ [0] as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -82,13 +82,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -128,7 +128,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_1" as target FROM events e WHERE team_id = 2 @@ -140,13 +140,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_1" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -190,7 +190,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -202,13 +202,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -255,7 +255,7 @@ [0] as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -267,13 +267,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -313,7 +313,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_1" as target FROM events e WHERE team_id = 2 @@ -325,13 +325,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_1" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -375,7 +375,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e.person_id as target FROM events e LEFT JOIN @@ -394,13 +394,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e.person_id as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -451,7 +451,7 @@ NULL as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e.person_id as target FROM events e LEFT JOIN @@ -470,13 +470,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e.person_id as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -632,7 +632,7 @@ [0] as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -644,13 +644,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e @@ -693,7 +693,7 @@ [0] as breakdown_values_filter, NULL as selected_interval, returning_event_query as - (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target FROM events e WHERE team_id = 2 @@ -705,13 +705,13 @@ GROUP BY target, event_date), target_event_query as - (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) AS event_date, + (SELECT DISTINCT toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) AS event_date, e."$group_0" as target, [ dateDiff( 'Week', - toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC')), - toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC')) + toStartOfWeek(toDateTime('2020-06-07 00:00:00', 'UTC'), 0), + toStartOfWeek(toTimeZone(toDateTime(e.timestamp, 'UTC'), 'UTC'), 0) ) ] as breakdown_values FROM events e diff --git a/frontend/__snapshots__/components-cards-insight-card--insight-card.png b/frontend/__snapshots__/components-cards-insight-card--insight-card.png index e76dec328d63d3..3c8919af6eeb8a 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-integrations-slack--slack-integration-added.png b/frontend/__snapshots__/components-integrations-slack--slack-integration-added.png index 6639b1ef129e2f..7b19245e899f82 100644 Binary files a/frontend/__snapshots__/components-integrations-slack--slack-integration-added.png and b/frontend/__snapshots__/components-integrations-slack--slack-integration-added.png differ diff --git a/frontend/__snapshots__/components-integrations-slack--slack-integration-instance-not-configured.png b/frontend/__snapshots__/components-integrations-slack--slack-integration-instance-not-configured.png index 1223f1d621a44f..fb29b0c8a8867e 100644 Binary files a/frontend/__snapshots__/components-integrations-slack--slack-integration-instance-not-configured.png and b/frontend/__snapshots__/components-integrations-slack--slack-integration-instance-not-configured.png differ diff --git a/frontend/__snapshots__/components-integrations-slack--slack-integration.png b/frontend/__snapshots__/components-integrations-slack--slack-integration.png index 1223f1d621a44f..fb29b0c8a8867e 100644 Binary files a/frontend/__snapshots__/components-integrations-slack--slack-integration.png and b/frontend/__snapshots__/components-integrations-slack--slack-integration.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png new file mode 100644 index 00000000000000..75529341446f0b Binary files /dev/null and b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png differ diff --git a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png index 363659a9b58665..d6139bbdf96274 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png and b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--events-free.png b/frontend/__snapshots__/filters-taxonomic-filter--events-free.png index c1dd6086c97490..0566c099ba51fd 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--events-free.png and b/frontend/__snapshots__/filters-taxonomic-filter--events-free.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png b/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png index 417734adb38075..b5826f59f74aed 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png and b/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--properties.png b/frontend/__snapshots__/filters-taxonomic-filter--properties.png index 6166356899a257..9db36eef29cb1d 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--properties.png and b/frontend/__snapshots__/filters-taxonomic-filter--properties.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 29f658fac2fb86..b49b6fc4bd3418 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-lemon-progress-circle--basic.png b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--basic.png new file mode 100644 index 00000000000000..cd99f4c3e0536a Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--basic.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-progress-circle--overview.png b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--overview.png new file mode 100644 index 00000000000000..1662d1716dd2f5 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--overview.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-progress-circle--template.png b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--template.png new file mode 100644 index 00000000000000..cd99f4c3e0536a Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-progress-circle--template.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--dark-mode.png b/frontend/__snapshots__/posthog-3000-navigation--dark-mode.png index 80acd540123ceb..11b06e1c7556d5 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--dark-mode.png and b/frontend/__snapshots__/posthog-3000-navigation--dark-mode.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--light-mode.png b/frontend/__snapshots__/posthog-3000-navigation--light-mode.png index dfa4e5819c3c24..678543a30f547c 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--light-mode.png and b/frontend/__snapshots__/posthog-3000-navigation--light-mode.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png index 4164e2d6ec68f6..5eb005ec637e55 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 b2697e16b93fa5..f37ec4794401f9 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-annotations--annotations.png b/frontend/__snapshots__/scenes-app-annotations--annotations.png index b04aef3ec2f311..1d0a33cc15c084 100644 Binary files a/frontend/__snapshots__/scenes-app-annotations--annotations.png and b/frontend/__snapshots__/scenes-app-annotations--annotations.png differ diff --git a/frontend/__snapshots__/scenes-app-apps--installed.png b/frontend/__snapshots__/scenes-app-apps--installed.png index 34ae3b5167bb3e..ca7fff9f98e957 100644 Binary files a/frontend/__snapshots__/scenes-app-apps--installed.png and b/frontend/__snapshots__/scenes-app-apps--installed.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--create-export.png b/frontend/__snapshots__/scenes-app-batchexports--create-export.png index aaa0d622855be5..5812443d7cc012 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-batchexports--exports.png b/frontend/__snapshots__/scenes-app-batchexports--exports.png index 105535afdb5cce..673b04e0d721e0 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--exports.png and b/frontend/__snapshots__/scenes-app-batchexports--exports.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--view-export.png b/frontend/__snapshots__/scenes-app-batchexports--view-export.png index d46d231d06a04f..f6551bc248623d 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--view-export.png and b/frontend/__snapshots__/scenes-app-batchexports--view-export.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--list.png b/frontend/__snapshots__/scenes-app-dashboards--list.png index f7ed888ae6bf39..527d891a4f1824 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--list.png and b/frontend/__snapshots__/scenes-app-dashboards--list.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables.png b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables.png index 4e53d35d1dffa0..c4d3fc8fb4f30a 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables.png and b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--new.png b/frontend/__snapshots__/scenes-app-dashboards--new.png index 66b685f868517a..58ca8d61687d2c 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--new.png and b/frontend/__snapshots__/scenes-app-dashboards--new.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management--database.png b/frontend/__snapshots__/scenes-app-data-management--database.png index ff22ffdb8d4673..deb21151803413 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--database.png and b/frontend/__snapshots__/scenes-app-data-management--database.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings.png b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings.png index 52b29a9abf121e..03287eac29f238 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings.png and b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings.png differ diff --git a/frontend/__snapshots__/scenes-app-events--event-explorer.png b/frontend/__snapshots__/scenes-app-events--event-explorer.png index a41b3e31d7ca81..4ed82b3bdbbdd5 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 87cad0d1f6db76..8feeeb95edc07c 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--experiment-not-found.png b/frontend/__snapshots__/scenes-app-experiments--experiment-not-found.png index 3332b4e9b583fc..422655b2bf3607 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiment-not-found.png and b/frontend/__snapshots__/scenes-app-experiments--experiment-not-found.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 36ad5c81084e2f..fd9d704276d4c4 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 35578f8df9b479..f6760a46e6b697 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 c0d2a2179228eb..aab6a5a84f625e 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-experiments--view-experiment-pay-gate.png b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate.png index dbfa94be817be4..e4572b8da84096 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate.png and b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--feature-flag-not-found.png b/frontend/__snapshots__/scenes-app-feature-flags--feature-flag-not-found.png index 3848b31fa78a83..143d99730cbf95 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--feature-flag-not-found.png and b/frontend/__snapshots__/scenes-app-feature-flags--feature-flag-not-found.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--feature-flags-list.png b/frontend/__snapshots__/scenes-app-feature-flags--feature-flags-list.png index 6231ed9ab28e70..eaaba388c9c594 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--feature-flags-list.png and b/frontend/__snapshots__/scenes-app-feature-flags--feature-flags-list.png differ diff --git a/frontend/__snapshots__/scenes-app-features--features-list.png b/frontend/__snapshots__/scenes-app-features--features-list.png index b18cd979d9480d..0cfa6720e18591 100644 Binary files a/frontend/__snapshots__/scenes-app-features--features-list.png and b/frontend/__snapshots__/scenes-app-features--features-list.png differ diff --git a/frontend/__snapshots__/scenes-app-features--new-feature-flag.png b/frontend/__snapshots__/scenes-app-features--new-feature-flag.png index 1e8c4ac19a5b5e..9ad9250549df8d 100644 Binary files a/frontend/__snapshots__/scenes-app-features--new-feature-flag.png and b/frontend/__snapshots__/scenes-app-features--new-feature-flag.png differ diff --git a/frontend/__snapshots__/scenes-app-features--not-found-early-access.png b/frontend/__snapshots__/scenes-app-features--not-found-early-access.png index 9cb4f72d11d6ec..b25a28b0fd025c 100644 Binary files a/frontend/__snapshots__/scenes-app-features--not-found-early-access.png and b/frontend/__snapshots__/scenes-app-features--not-found-early-access.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 60774c6d193440..5d9b07803bd264 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 9c54670e67a941..6d0fd5b2d7cb78 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 1b7538c0769755..489d1255fad031 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 e7ae0a9602d9c4..53cb826a62a1ce 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-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png index 6db5ed04bf68a3..14095aac431c65 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 1f0c46fe5c31e4..093643b9deefa5 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-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png index 8d7d747f8990a9..b486123a26dc72 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 a70e35a6c7f2c3..2ffaf24c766406 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-time-to-convert--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png index ad0d74d3d35ac8..226edac304a581 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 20983c9ce3d2c4..b41b5c6455c6db 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 945122d0b64fad..b2330f1d377800 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 d3c5a1c7e9457f..3e3b254a5e43ab 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-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png index 192aee1058894f..db220a7c3905ee 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 0aead0fac3794e..500762ca60561d 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 772ae64573f6af..77cc5d1496a9cd 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 1158f400a0d85d..c61377d5ee00c1 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 12e54f3f008bda..8958fe5dc51ba1 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--lifecycle--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png index e73a067a7514f9..ee1307cd8d1ba5 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 dd0aec35f1f094..373e3e0e19c986 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.png b/frontend/__snapshots__/scenes-app-insights--lifecycle.png index 738d5ec8994c5c..8fda2317ec4d01 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-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png index 5d4b625ed232ff..fd40f5ea3d0164 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--stickiness--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png index 8c33b18f5d8940..bcf04c038e70b6 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 62e04313ddc652..509a482b5eeda3 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.png b/frontend/__snapshots__/scenes-app-insights--stickiness.png index 8a6be5ba22fdce..de3afbf8cc7afd 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-line--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png index f225aae3dea2ea..8aed583ad40641 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-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png index 04c87cafee5c6d..e14d719a073e21 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 89ad7841565cc9..fbeb271f50cf0d 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 new file mode 100644 index 00000000000000..6015dabcec7138 Binary files /dev/null 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 new file mode 100644 index 00000000000000..3f2231d07737c0 Binary files /dev/null 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 new file mode 100644 index 00000000000000..649c945bdf267d Binary files /dev/null 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 new file mode 100644 index 00000000000000..9fc201861aa47c Binary files /dev/null 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 7d0aeeadd935d8..e1b4848578966f 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 b118e92405c221..17a8821fb39edd 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 2a5f905f2d2cb8..796e6548e08c81 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.png b/frontend/__snapshots__/scenes-app-insights--trends-number.png index 932161c2835e5e..48d95207dd24d9 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-table--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png index 0cd8bfe68f2d7f..2ba75da1d8204d 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 45b723002a54ec..063e78dd5feaf0 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.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png index fb9e0493728660..a3a2a9a805370b 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 e0ad1fcf310977..fb0e1de7e73103 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 b7e8cedbda6c6c..4ca89e34bfd222 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 53b2b12ca2cf59..e36f384700aeeb 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--user-paths-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png index aa0201c92e8b62..0db6e20f49afb7 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-notebooks--bullet-list.png b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png index 295d07af47a714..d196998a7e56f5 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 4ffe557aaf74a4..2086ed0aa90aa1 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 05544939768180..3186fed28fd49a 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 dc8cdf401a6f41..0df1f64e9ec3c4 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 a2a7a11db4cc92..c9f29c566c2c6c 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 b9da335cd61d7b..cbe5aefd199edb 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 b03f1fab581cf0..be8910d314d5b5 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 8c133958b39cc6..aa872f0a5cfe9c 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 a803eed954067f..f0ea3f9550997b 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-filtering-page.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-filtering-page.png index 57b43ed4238ffb..a13107e8bbaa89 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 854c2468d8cfcd..c51a24f39fa184 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.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page.png index 4cf705d0bb98b7..dd103d2f11446b 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-recordings--recordings-play-lists.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-lists.png index 142d66d3f95dfb..c4c27f4db817aa 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--empty-state.png b/frontend/__snapshots__/scenes-app-saved-insights--empty-state.png index 76d3085776638f..86a13a05a89268 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 45cbb37f481ddb..68b73d4906ad25 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--new-survey-appearance-section.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section.png index 73966cfe593038..a86eba79b8ac08 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section.png index 1fde22834eecff..59181dae850387 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--survey-not-found.png b/frontend/__snapshots__/scenes-app-surveys--survey-not-found.png index a19faf8440760d..711d125b3d17ed 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--survey-not-found.png and b/frontend/__snapshots__/scenes-app-surveys--survey-not-found.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--survey-view.png b/frontend/__snapshots__/scenes-app-surveys--survey-view.png deleted file mode 100644 index 1fedb0eef095e0..00000000000000 Binary files a/frontend/__snapshots__/scenes-app-surveys--survey-view.png and /dev/null differ diff --git a/frontend/__snapshots__/scenes-app-surveys--surveys-list.png b/frontend/__snapshots__/scenes-app-surveys--surveys-list.png index 2561db683b5f72..012692ee2758d8 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--cloud-with-google-login-enforcement.png b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png index e34fe137f3088f..b0f0ee514ce7f8 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png and b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png index 6b63cb0b7dbc82..ee4ee6206d28ef 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png and b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization.png b/frontend/__snapshots__/scenes-other-settings--settings-organization.png new file mode 100644 index 00000000000000..05fee3ba225115 Binary files /dev/null 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 new file mode 100644 index 00000000000000..71514ddb64f122 Binary files /dev/null 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 new file mode 100644 index 00000000000000..5574601d117944 Binary files /dev/null and b/frontend/__snapshots__/scenes-other-settings--settings-user.png differ diff --git a/frontend/src/exporter/Exporter.stories.tsx b/frontend/src/exporter/Exporter.stories.tsx index 2fec27d41785c2..4c5acbd7257aa5 100644 --- a/frontend/src/exporter/Exporter.stories.tsx +++ b/frontend/src/exporter/Exporter.stories.tsx @@ -38,107 +38,115 @@ const Template: StoryFn = (props) => { } export const TrendsLineInsight: Story = Template.bind({}) -TrendsLineInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsLine.json') } +TrendsLineInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsLine.json') } + +export const TrendsLineMultiInsight: Story = Template.bind({}) +TrendsLineMultiInsight.args = { + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsLineMulti.json'), +} +TrendsLineMultiInsight.parameters = { + mockDate: '2023-07-10', +} export const TrendsLineBreakdownInsight: Story = Template.bind({}) TrendsLineBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsLineBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsLineBreakdown.json'), } export const TrendsBarInsight: Story = Template.bind({}) -TrendsBarInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsBar.json') } +TrendsBarInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsBar.json') } export const TrendsBarBreakdownInsight: Story = Template.bind({}) TrendsBarBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsBarBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsBarBreakdown.json'), } export const TrendsValueInsight: Story = Template.bind({}) -TrendsValueInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsValue.json') } +TrendsValueInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsValue.json') } export const TrendsValueBreakdownInsight: Story = Template.bind({}) TrendsValueBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsValueBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsValueBreakdown.json'), } export const TrendsAreaInsight: Story = Template.bind({}) -TrendsAreaInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsArea.json') } +TrendsAreaInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsArea.json') } export const TrendsAreaBreakdownInsight: Story = Template.bind({}) TrendsAreaBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsAreaBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsAreaBreakdown.json'), } export const TrendsNumberInsight: Story = Template.bind({}) -TrendsNumberInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsNumber.json') } +TrendsNumberInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsNumber.json') } export const TrendsTableInsight: Story = Template.bind({}) -TrendsTableInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsTable.json') } +TrendsTableInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsTable.json') } export const TrendsTableBreakdownInsight: Story = Template.bind({}) TrendsTableBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsTableBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsTableBreakdown.json'), } export const TrendsPieInsight: Story = Template.bind({}) -TrendsPieInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsPie.json') } +TrendsPieInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsPie.json') } export const TrendsPieBreakdownInsight: Story = Template.bind({}) TrendsPieBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsPieBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsPieBreakdown.json'), } export const TrendsWorldMapInsight: Story = Template.bind({}) TrendsWorldMapInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/trendsWorldMap.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/trendsWorldMap.json'), } export const FunnelLeftToRightInsight: Story = Template.bind({}) FunnelLeftToRightInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRight.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRight.json'), } export const FunnelLeftToRightBreakdownInsight: Story = Template.bind({}) FunnelLeftToRightBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRightBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRightBreakdown.json'), } export const FunnelTopToBottomInsight: Story = Template.bind({}) FunnelTopToBottomInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottom.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottom.json'), } export const FunnelTopToBottomBreakdownInsight: Story = Template.bind({}) FunnelTopToBottomBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottomBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottomBreakdown.json'), } export const FunnelHistoricalTrendsInsight: Story = Template.bind({}) FunnelHistoricalTrendsInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelHistoricalTrends.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelHistoricalTrends.json'), } export const FunnelTimeToConvertInsight: Story = Template.bind({}) FunnelTimeToConvertInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/funnelTimeToConvert.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/funnelTimeToConvert.json'), } export const RetentionInsight: Story = Template.bind({}) -RetentionInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/retention.json') } +RetentionInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/retention.json') } export const RetentionBreakdownInsight: Story = Template.bind({}) RetentionBreakdownInsight.args = { - insight: require('../mocks/fixtures/api/projects/:team_id/insights/retentionBreakdown.json'), + insight: require('../mocks/fixtures/api/projects/team_id/insights/retentionBreakdown.json'), } export const LifecycleInsight: Story = Template.bind({}) -LifecycleInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/lifecycle.json') } +LifecycleInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/lifecycle.json') } export const StickinessInsight: Story = Template.bind({}) -StickinessInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/stickiness.json') } +StickinessInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/stickiness.json') } export const UserPathsInsight: Story = Template.bind({}) -UserPathsInsight.args = { insight: require('../mocks/fixtures/api/projects/:team_id/insights/userPaths.json') } +UserPathsInsight.args = { insight: require('../mocks/fixtures/api/projects/team_id/insights/userPaths.json') } export const Dashboard: Story = Template.bind({}) Dashboard.args = { dashboard } diff --git a/frontend/src/layout/GlobalModals.tsx b/frontend/src/layout/GlobalModals.tsx new file mode 100644 index 00000000000000..83e61a3476da01 --- /dev/null +++ b/frontend/src/layout/GlobalModals.tsx @@ -0,0 +1,81 @@ +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 { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { Prompt } from 'lib/logic/newPrompt/Prompt' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { InviteModal } from 'scenes/settings/organization/InviteModal' + +export const globalModalsLogic = kea([ + path(['layout', 'navigation', 'globalModalsLogic']), + actions({ + showCreateOrganizationModal: true, + hideCreateOrganizationModal: true, + showCreateProjectModal: true, + hideCreateProjectModal: true, + }), + reducers({ + isCreateOrganizationModalShown: [ + false, + { + showCreateOrganizationModal: () => true, + hideCreateOrganizationModal: () => false, + }, + ], + isCreateProjectModalShown: [ + false, + { + showCreateProjectModal: () => true, + hideCreateProjectModal: () => false, + }, + ], + }), +]) + +export function GlobalModals(): JSX.Element { + const { isCreateOrganizationModalShown, isCreateProjectModalShown } = useValues(globalModalsLogic) + const { hideCreateOrganizationModal, hideCreateProjectModal } = useActions(globalModalsLogic) + const { isInviteModalShown } = useValues(inviteLogic) + const { hideInviteModal } = useActions(inviteLogic) + const { user } = useValues(userLogic) + + return ( + <> + + + + + + + {user && user.organization?.enforce_2fa && !user.is_2fa_enabled && ( + +

+ Your organization requires you to set up 2FA. +

+

+ + Use an authenticator app like Google Authenticator or 1Password to scan the QR code below. + +

+ { + userLogic.actions.loadUser() + membersLogic.actions.loadMembers() + }} + /> +
+ )} + + + + + ) +} diff --git a/frontend/src/layout/navigation-3000/Navigation.scss b/frontend/src/layout/navigation-3000/Navigation.scss index c9b6ed65e2903c..ad0629bb138fb6 100644 --- a/frontend/src/layout/navigation-3000/Navigation.scss +++ b/frontend/src/layout/navigation-3000/Navigation.scss @@ -22,7 +22,6 @@ position: relative; margin: var(--scene-padding-y) var(--scene-padding-x); min-height: calc(100vh - var(--breadcrumbs-height-full) - var(--scene-padding-y) * 2); - overflow-x: hidden; &.Navigation3000__scene--raw { --scene-padding-y: 0px; --scene-padding-x: 0px; diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index 253156e51398a0..04d577454aadd7 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -29,6 +29,9 @@ export function Navigation({ document.getElementById('bottom-notice')?.remove() }, []) + if (sceneConfig?.layout === 'plain') { + return <>{children} + } return (
diff --git a/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx b/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx index 497d9ec56bad2c..b818d42923c955 100644 --- a/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx +++ b/frontend/src/layout/navigation-3000/components/Breadcrumbs.tsx @@ -36,7 +36,6 @@ export function Breadcrumbs(): JSX.Element | null {
{ + if (popoverShown) { + setPopoverShown(false) + } + }} > {breadcrumbContent} diff --git a/frontend/src/layout/navigation-3000/components/Navbar.tsx b/frontend/src/layout/navigation-3000/components/Navbar.tsx index c91b9289ee8850..26619b7fd6cf20 100644 --- a/frontend/src/layout/navigation-3000/components/Navbar.tsx +++ b/frontend/src/layout/navigation-3000/components/Navbar.tsx @@ -104,9 +104,9 @@ export function Navbar(): JSX.Element { /> } - identifier={Scene.ProjectSettings} + identifier={Scene.Settings} title="Project settings" - to={urls.projectSettings()} + to={urls.settings('project')} /> } diff --git a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx index c6dc5479055b8e..72e07508a9ebf1 100644 --- a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx +++ b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx @@ -4,6 +4,7 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' import clsx from 'clsx' import { useValues } from 'kea' 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' @@ -63,29 +64,46 @@ export const NavbarButton: FunctionComponent = React.forwardR } } + const buttonContent = ( + setHasBeenClicked(false)} + onClick={() => { + setHasBeenClicked(true) + onClick?.() + }} + className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')} + fullWidth + {...buttonProps} + > + {content} + + ) + + const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier }) + return (
  • - - setHasBeenClicked(false)} - onClick={() => { - setHasBeenClicked(true) - onClick?.() - }} - className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')} - fullWidth - {...buttonProps} + {notices.length ? ( + } + placement={notices[0].placement ?? 'right'} + delayMs={0} + visible={true} + > + {buttonContent} + + ) : ( + - {content} - - + {buttonContent} + + )}
  • ) }) diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 48f14cf59fd204..5e5c1237c1b8f7 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -19,9 +19,7 @@ import { IconHome, IconLive, IconPeople, - IconPerson, IconPieChart, - IconQuestion, IconRewindPlay, IconTestTube, IconToggle, @@ -32,8 +30,6 @@ import { IconChat, } from '@posthog/icons' import { urls } from 'scenes/urls' -import { annotationsSidebarLogic } from './sidebars/annotations' -import { cohortsSidebarLogic } from './sidebars/cohorts' import { dashboardsSidebarLogic } from './sidebars/dashboards' import { dataManagementSidebarLogic } from './sidebars/dataManagement' import { experimentsSidebarLogic } from './sidebars/experiments' @@ -322,33 +318,12 @@ export const navigation3000Logic = kea([ to: isUsingSidebar ? undefined : urls.eventDefinitions(), }, { - identifier: Scene.Persons, - label: 'People and groups', - icon: , + identifier: Scene.PersonsManagement, + label: 'People', + icon: , logic: isUsingSidebar ? personsAndGroupsSidebarLogic : undefined, to: isUsingSidebar ? undefined : urls.persons(), }, - { - identifier: Scene.Cohorts, - label: 'Cohorts', - icon: , - logic: isUsingSidebar ? cohortsSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.cohorts(), - }, - { - identifier: Scene.Annotations, - label: 'Annotations', - icon: , - logic: isUsingSidebar ? annotationsSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.annotations(), - }, - { - identifier: Scene.ToolbarLaunch, - label: 'Toolbar', - icon: , - logic: isUsingSidebar ? toolbarSidebarLogic : undefined, - to: isUsingSidebar ? undefined : urls.toolbarLaunch(), - }, ], [ { @@ -364,7 +339,7 @@ export const navigation3000Logic = kea([ label: 'Web analytics', icon: , to: isUsingSidebar ? undefined : urls.webAnalytics(), - tag: 'alpha' as const, + tag: 'beta' as const, } : null, { @@ -415,6 +390,13 @@ export const navigation3000Logic = kea([ icon: , to: urls.projectApps(), }, + { + identifier: Scene.ToolbarLaunch, + label: 'Toolbar', + icon: , + logic: isUsingSidebar ? toolbarSidebarLogic : undefined, + to: isUsingSidebar ? undefined : urls.toolbarLaunch(), + }, ], ] }, diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss index f2744d39ae95bf..4f3d08f139753e 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss @@ -20,7 +20,7 @@ box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2); [theme='dark'] & { - box-shadow: 0px 0px 30px rgba(255, 255, 255, 0.1); + box-shadow: none; } } diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index 6bba9592f0972f..3411db82b14655 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -1,15 +1,19 @@ import { LemonButton } from '@posthog/lemon-ui' import './SidePanel.scss' import { useActions, useValues } from 'kea' -import { SidePanelTab, sidePanelLogic } from './sidePanelLogic' +import { sidePanelLogic } from './sidePanelLogic' import clsx from 'clsx' import { Resizer } from 'lib/components/Resizer/Resizer' import { useRef } from 'react' import { ResizerLogicProps, resizerLogic } from 'lib/components/Resizer/resizerLogic' -import { IconNotebook, IconQuestion, IconInfo } from '@posthog/icons' +import { IconNotebook, IconQuestion, IconInfo, IconGear } from '@posthog/icons' import { SidePanelDocs } from './panels/SidePanelDocs' import { SidePanelSupport } from './panels/SidePanelSupport' import { NotebookPanel } from 'scenes/notebooks/NotebookPanel/NotebookPanel' +import { SidePanelActivation, SidePanelActivationIcon } from './panels/SidePanelActivation' +import { SidePanelSettings } from './panels/SidePanelSettings' +import { SidePanelTab } from '~/types' +import { sidePanelStateLogic } from './sidePanelStateLogic' export const SidePanelTabs: Record = { [SidePanelTab.Notebooks]: { @@ -27,11 +31,23 @@ export const SidePanelTabs: Record
    - {Object.entries(SidePanelTabs) - .filter(([tab]) => enabledTabs.includes(tab as SidePanelTab)) - .map(([tab, { label, Icon }]) => ( + {visibleTabs.map((tab: SidePanelTab) => { + const { Icon, label } = SidePanelTabs[tab] + return ( } @@ -85,7 +101,8 @@ export function SidePanel(): JSX.Element | null { > {label} - ))} + ) + })}
    diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx new file mode 100644 index 00000000000000..f6a137cc850eed --- /dev/null +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelActivation.tsx @@ -0,0 +1,58 @@ +import { useValues } from 'kea' +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' + +export const SidePanelActivation = (): JSX.Element => { + const { activeTasks, completionPercent, completedTasks } = useValues(activationLogic) + + return ( +
    +

    Quick Start

    +

    Use our Quick Start guide to learn about everything PostHog can do for you and your product.

    +
    +
    + + {activeTasks.length} + +

    still to go

    +
    +
    + +
    +
    + {activeTasks.length > 0 && ( +
    +

    What's next?

    +
      + {activeTasks.map((task: ActivationTaskType) => ( + + ))} +
    +
    + )} + {completedTasks.length > 0 && ( +
    +

    Completed

    +
      + {completedTasks.map((task: ActivationTaskType) => ( + + ))} +
    +
    + )} +
    + ) +} + +export const SidePanelActivationIcon = ({ className }: { className: LemonIconProps['className'] }): JSX.Element => { + const { activeTasks, completionPercent } = useValues(activationLogic) + + return ( + + {activeTasks.length} + + ) +} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx new file mode 100644 index 00000000000000..2b9009f3f5be8d --- /dev/null +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSettings.tsx @@ -0,0 +1,47 @@ +import { useActions, useValues } from 'kea' +import { sidePanelSettingsLogic } from './sidePanelSettingsLogic' +import { Settings } from 'scenes/settings/Settings' +import { LemonButton } from '@posthog/lemon-ui' +import { IconOpenInNew } from 'lib/lemon-ui/icons' +import { urls } from 'scenes/urls' +import { SettingsLogicProps, settingsLogic } from 'scenes/settings/settingsLogic' +import { useEffect } from 'react' + +export const SidePanelSettings = (): JSX.Element => { + const { settings } = useValues(sidePanelSettingsLogic) + const { closeSidePanel, setSettings } = useActions(sidePanelSettingsLogic) + + const settingsLogicProps: SettingsLogicProps = { + ...settings, + logicKey: 'sidepanel', + } + const { selectedSectionId, selectedLevel } = useValues(settingsLogic(settingsLogicProps)) + + useEffect(() => { + setSettings({ + sectionId: selectedSectionId ?? undefined, + settingLevelId: selectedLevel, + }) + }, [selectedSectionId, selectedLevel]) + + return ( +
    +
    + closeSidePanel()} + icon={} + > + All settings + + closeSidePanel()}> + Done + +
    +
    + +
    +
    + ) +} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx index 4cfa7be0130e06..71344bb84b4a69 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx @@ -1,12 +1,13 @@ import { useActions, useValues } from 'kea' import { SupportForm, SupportFormButtons } from 'lib/components/Support/SupportForm' -import { SidePanelTab, sidePanelLogic } from '../sidePanelLogic' import { supportLogic } from 'lib/components/Support/supportLogic' import { useEffect } from 'react' +import { sidePanelStateLogic } from '../sidePanelStateLogic' +import { SidePanelTab } from '~/types' export const SidePanelSupport = (): JSX.Element => { - const { closeSidePanel } = useActions(sidePanelLogic) - const { selectedTab } = useValues(sidePanelLogic) + const { closeSidePanel } = useActions(sidePanelStateLogic) + const { selectedTab } = useValues(sidePanelStateLogic) const theLogic = supportLogic({ onClose: () => closeSidePanel(SidePanelTab.Feedback) }) const { title } = useValues(theLogic) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts index a74907018531c6..ed5ae45ebde094 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts @@ -1,14 +1,15 @@ import { actions, kea, reducers, path, listeners, connect } from 'kea' -import { SidePanelTab, sidePanelLogic } from '../sidePanelLogic' import type { sidePanelDocsLogicType } from './sidePanelDocsLogicType' +import { sidePanelStateLogic } from '../sidePanelStateLogic' +import { SidePanelTab } from '~/types' const POSTHOG_COM_DOMAIN = 'https://posthog.com' export const sidePanelDocsLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelDocsLogic']), connect({ - actions: [sidePanelLogic, ['openSidePanel', 'closeSidePanel']], + actions: [sidePanelStateLogic, ['openSidePanel', 'closeSidePanel']], }), actions({ diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx new file mode 100644 index 00000000000000..da07a199f139d9 --- /dev/null +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic.tsx @@ -0,0 +1,60 @@ +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 type { sidePanelSettingsLogicType } from './sidePanelSettingsLogicType' +import { sidePanelStateLogic } from '../sidePanelStateLogic' +import { SidePanelTab } from '~/types' +import { SettingsLogicProps } from 'scenes/settings/settingsLogic' + +export const sidePanelSettingsLogic = kea([ + path(['scenes', 'navigation', 'sidepanel', 'sidePanelSettingsLogic']), + connect({ + values: [featureFlagLogic, ['featureFlags']], + actions: [sidePanelStateLogic, ['openSidePanel', 'closeSidePanel']], + }), + + actions({ + openSettingsPanel: (settingsLogicProps: SettingsLogicProps) => ({ + settingsLogicProps, + }), + setSettings: (settingsLogicProps: SettingsLogicProps) => ({ + settingsLogicProps, + }), + }), + + reducers(() => ({ + settings: [ + {} as SettingsLogicProps, + { persist: true }, + { + openSettingsPanel: (_, { settingsLogicProps }) => { + return settingsLogicProps + }, + setSettings: (_, { settingsLogicProps }) => { + return settingsLogicProps + }, + }, + ], + })), + + listeners(({ actions, values }) => ({ + openSettingsPanel: ({ settingsLogicProps }) => { + if (!values.featureFlags[FEATURE_FLAGS.POSTHOG_3000]) { + LemonDialog.open({ + title: 'Settings', + content: , + width: 600, + primaryButton: { + children: 'Done', + }, + }) + return + } + + actions.openSidePanel(SidePanelTab.Settings) + }, + })), +]) diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx index c51ccb50a07162..aef21253e546f5 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx @@ -1,44 +1,25 @@ -import { actions, kea, reducers, path, listeners, selectors, connect } from 'kea' +import { kea, path, selectors, connect } from 'kea' import type { sidePanelLogicType } from './sidePanelLogicType' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' - -export enum SidePanelTab { - Notebooks = 'notebook', - Feedback = 'feedback', - Docs = 'docs', -} +import { activationLogic } from 'lib/components/ActivationSidebar/activationLogic' +import { SidePanelTab } from '~/types' +import { sidePanelStateLogic } from './sidePanelStateLogic' export const sidePanelLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelLogic']), - actions({ - setSidePanelOpen: (open: boolean) => ({ open }), - openSidePanel: (tab: SidePanelTab) => ({ tab }), - closeSidePanel: (tab?: SidePanelTab) => ({ tab }), - }), - connect({ - values: [featureFlagLogic, ['featureFlags'], preflightLogic, ['isCloudOrDev']], - }), - - reducers(() => ({ - selectedTab: [ - null as SidePanelTab | null, - { persist: true }, - { - openSidePanel: (_, { tab }) => tab, - }, - ], - sidePanelOpen: [ - false, - { persist: true }, - { - setSidePanelOpen: (_, { open }) => open, - }, + values: [ + featureFlagLogic, + ['featureFlags'], + preflightLogic, + ['isCloudOrDev'], + activationLogic, + ['isReady', 'hasCompletedAllTasks'], ], - })), + }), selectors({ enabledTabs: [ @@ -58,23 +39,39 @@ export const sidePanelLogic = kea([ tabs.push(SidePanelTab.Docs) } + tabs.push(SidePanelTab.Settings) + tabs.push(SidePanelTab.Activation) + return tabs }, ], - }), - listeners(({ actions, values }) => ({ - openSidePanel: () => { - actions.setSidePanelOpen(true) - }, - closeSidePanel: ({ tab }) => { - if (!tab) { - // If we aren't specifiying the tab we always close - actions.setSidePanelOpen(false) - } else if (values.selectedTab === tab) { - // Otherwise we only close it if the tab is the currently open one - actions.setSidePanelOpen(false) - } - }, - })), + visibleTabs: [ + (s) => [ + s.enabledTabs, + sidePanelStateLogic.selectors.selectedTab, + sidePanelStateLogic.selectors.sidePanelOpen, + s.isReady, + s.hasCompletedAllTasks, + ], + (enabledTabs, selectedTab, sidePanelOpen, isReady, hasCompletedAllTasks): SidePanelTab[] => { + return enabledTabs.filter((tab: any) => { + if (tab === selectedTab && sidePanelOpen) { + return true + } + + // Hide certain tabs unless they are selected + if ([SidePanelTab.Settings].includes(tab)) { + return false + } + + if (tab === SidePanelTab.Activation && (!isReady || hasCompletedAllTasks)) { + return false + } + + return true + }) + }, + ], + }), ]) diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx new file mode 100644 index 00000000000000..2c29443facbc6b --- /dev/null +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx @@ -0,0 +1,48 @@ +import { actions, kea, listeners, path, reducers } from 'kea' +import { SidePanelTab } from '~/types' + +import type { sidePanelStateLogicType } from './sidePanelStateLogicType' + +// The side panel imports a lot of other components so this allows us to avoid circular dependencies + +export const sidePanelStateLogic = kea([ + path(['scenes', 'navigation', 'sidepanel', 'sidePanelStateLogic']), + actions({ + openSidePanel: (tab: SidePanelTab) => ({ tab }), + closeSidePanel: (tab?: SidePanelTab) => ({ tab }), + setSidePanelOpen: (open: boolean) => ({ open }), + }), + + reducers(() => ({ + selectedTab: [ + null as SidePanelTab | null, + { persist: true }, + { + openSidePanel: (_, { tab }) => tab, + }, + ], + sidePanelOpen: [ + false, + { persist: true }, + { + setSidePanelOpen: (_, { open }) => open, + }, + ], + })), + listeners(({ actions, values }) => ({ + // NOTE: We explicitly reference the actions instead of connecting so that people don't accidentally + // use this logic instead of sidePanelStateLogic + openSidePanel: () => { + actions.setSidePanelOpen(true) + }, + closeSidePanel: ({ tab }) => { + if (!tab) { + // If we aren't specifiying the tab we always close + actions.setSidePanelOpen(false) + } else if (values.selectedTab === tab) { + // Otherwise we only close it if the tab is the currently open one + actions.setSidePanelOpen(false) + } + }, + })), +]) diff --git a/frontend/src/layout/navigation-3000/themeLogic.ts b/frontend/src/layout/navigation-3000/themeLogic.ts index 98ae67f02c45b2..811960d5bf15a7 100644 --- a/frontend/src/layout/navigation-3000/themeLogic.ts +++ b/frontend/src/layout/navigation-3000/themeLogic.ts @@ -4,6 +4,7 @@ import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import type { themeLogicType } from './themeLogicType' +import { sceneLogic } from 'scenes/sceneLogic' export const themeLogic = kea([ path(['layout', 'navigation-3000', 'themeLogic']), @@ -32,8 +33,21 @@ export const themeLogic = kea([ }), selectors({ isDarkModeOn: [ - (s) => [s.darkModeSavedPreference, s.darkModeSystemPreference, featureFlagLogic.selectors.featureFlags], - (darkModeSavedPreference, darkModeSystemPreference, featureFlags) => { + (s) => [ + s.darkModeSavedPreference, + s.darkModeSystemPreference, + featureFlagLogic.selectors.featureFlags, + sceneLogic.selectors.sceneConfig, + ], + (darkModeSavedPreference, darkModeSystemPreference, featureFlags, sceneConfig) => { + // NOTE: Unauthenticated users always get the light mode until we have full support across onboarding flows + if ( + sceneConfig?.layout === 'plain' || + sceneConfig?.allowUnauthenticated || + sceneConfig?.onlyUnauthenticated + ) { + return false + } // Dark mode is a PostHog 3000 feature // User-saved preference is used when set, oterwise we fall back to the system value return featureFlags[FEATURE_FLAGS.POSTHOG_3000] diff --git a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts index 04e0abc6fd424a..dd49842bc5fa7d 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts +++ b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.test.ts @@ -7,7 +7,7 @@ import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' const blankScene = (): any => ({ scene: { component: () => null, logic: null } }) -const scenes: any = { [Scene.Annotations]: blankScene, [Scene.Dashboards]: blankScene } +const scenes: any = { [Scene.SavedInsights]: blankScene, [Scene.Dashboards]: blankScene } describe('breadcrumbsLogic', () => { let logic: ReturnType @@ -24,9 +24,9 @@ describe('breadcrumbsLogic', () => { logic.mount() // test with .delay because subscriptions happen async - router.actions.push(urls.annotations()) - await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Annotations • PostHog' }) - expect(global.document.title).toEqual('Annotations • PostHog') + router.actions.push(urls.savedInsights()) + await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Insights • PostHog' }) + expect(global.document.title).toEqual('Insights • PostHog') router.actions.push(urls.dashboards()) await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Dashboards • PostHog' }) diff --git a/frontend/src/layout/navigation/OrganizationSwitcher.tsx b/frontend/src/layout/navigation/OrganizationSwitcher.tsx index 4c1d5401a854d6..ed9232ee87256e 100644 --- a/frontend/src/layout/navigation/OrganizationSwitcher.tsx +++ b/frontend/src/layout/navigation/OrganizationSwitcher.tsx @@ -11,6 +11,7 @@ import { sceneLogic } from 'scenes/sceneLogic' import { userLogic } from 'scenes/userLogic' import { AvailableFeature, OrganizationBasicType } from '~/types' import { navigationLogic } from './navigationLogic' +import { globalModalsLogic } from '../GlobalModals' export function AccessLevelIndicator({ organization }: { organization: OrganizationBasicType }): JSX.Element { return ( @@ -44,7 +45,8 @@ export function OtherOrganizationButton({ } export function NewOrganizationButton(): JSX.Element { - const { closeSitePopover, showCreateOrganizationModal } = useActions(navigationLogic) + const { closeSitePopover } = useActions(navigationLogic) + const { showCreateOrganizationModal } = useActions(globalModalsLogic) const { guardAvailableFeature } = useActions(sceneLogic) return ( diff --git a/frontend/src/layout/navigation/ProjectNotice.tsx b/frontend/src/layout/navigation/ProjectNotice.tsx index e4e19d9abddf0b..a6f977ae6fad91 100644 --- a/frontend/src/layout/navigation/ProjectNotice.tsx +++ b/frontend/src/layout/navigation/ProjectNotice.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' import { Link } from 'lib/lemon-ui/Link' import { navigationLogic, ProjectNoticeVariant } from './navigationLogic' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconPlus, IconSettings } from 'lib/lemon-ui/icons' import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner' @@ -64,7 +64,7 @@ export function ProjectNotice(): JSX.Element | null { ingestion wizard {' '} or grab your project API key/HTML snippet from{' '} - + Project Settings {' '} to get things moving diff --git a/frontend/src/layout/navigation/ProjectSwitcher.tsx b/frontend/src/layout/navigation/ProjectSwitcher.tsx index 092991119972f0..8e8c9d5422e825 100644 --- a/frontend/src/layout/navigation/ProjectSwitcher.tsx +++ b/frontend/src/layout/navigation/ProjectSwitcher.tsx @@ -11,6 +11,7 @@ import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { AvailableFeature, TeamBasicType } from '~/types' import { navigationLogic } from './navigationLogic' +import { globalModalsLogic } from '../GlobalModals' export function ProjectName({ team }: { team: TeamBasicType }): JSX.Element { return ( @@ -25,7 +26,8 @@ export function ProjectSwitcherOverlay(): JSX.Element { const { currentOrganization, projectCreationForbiddenReason } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) const { guardAvailableFeature } = useActions(sceneLogic) - const { showCreateProjectModal, hideProjectSwitcher } = useActions(navigationLogic) + const { hideProjectSwitcher } = useActions(navigationLogic) + const { showCreateProjectModal } = useActions(globalModalsLogic) return (
    @@ -74,7 +76,7 @@ function CurrentProjectButton(): JSX.Element | null { tooltip: `Go to ${currentTeam.name} settings`, onClick: () => { hideProjectSwitcher() - push(urls.projectSettings()) + push(urls.settings('project')) }, }} title={`Switch to project ${currentTeam.name}`} @@ -101,7 +103,7 @@ function OtherProjectButton({ team }: { team: TeamBasicType }): JSX.Element { tooltip: `Go to ${team.name} settings`, onClick: () => { hideProjectSwitcher() - updateCurrentTeam(team.id, '/project/settings') + updateCurrentTeam(team.id, '/settings') }, }} title={`Switch to project ${team.name}`} diff --git a/frontend/src/layout/navigation/SideBar/PageButton.tsx b/frontend/src/layout/navigation/SideBar/PageButton.tsx index f76b535ddc74e4..1e8fafeca07de6 100644 --- a/frontend/src/layout/navigation/SideBar/PageButton.tsx +++ b/frontend/src/layout/navigation/SideBar/PageButton.tsx @@ -6,6 +6,7 @@ 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 { SidebarChangeNoticeTooltip } from '~/layout/navigation/SideBar/SidebarChangeNotice' export interface PageButtonProps extends Pick { /** Used for highlighting the active scene. `identifier` of type number means dashboard ID instead of scene. */ @@ -32,48 +33,51 @@ export function PageButton({ title, sideAction, identifier, highlight, ...button return (
  • - {sideAction ? ( - - {title} - - ) : ( - - {title} - {highlight === 'alpha' ? ( - - Alpha - - ) : highlight === 'beta' ? ( - - Beta - - ) : highlight === 'new' ? ( - - New - - ) : null} - - )} + + {sideAction ? ( + + {title} + + ) : ( + + {title} + {highlight === 'alpha' ? ( + + Alpha + + ) : highlight === 'beta' ? ( + + Beta + + ) : highlight === 'new' ? ( + + New + + ) : null} + + )} +
  • ) } diff --git a/frontend/src/layout/navigation/SideBar/SideBar.tsx b/frontend/src/layout/navigation/SideBar/SideBar.tsx index dce2dbf9936acb..6e2309a9a5ca24 100644 --- a/frontend/src/layout/navigation/SideBar/SideBar.tsx +++ b/frontend/src/layout/navigation/SideBar/SideBar.tsx @@ -7,7 +7,6 @@ import { IconApps, IconBarChart, IconCohort, - IconComment, IconDatabase, IconExperiment, IconFlag, @@ -15,7 +14,6 @@ import { IconLive, IconMessages, IconOpenInApp, - IconPerson, IconPinOutline, IconPipeline, IconPlus, @@ -39,7 +37,6 @@ import { AvailableFeature } from '~/types' import './SideBar.scss' import { navigationLogic } from '../navigationLogic' import { FEATURE_FLAGS } from 'lib/constants' -import { groupsModel } from '~/models/groupsModel' import { userLogic } from 'scenes/userLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SideBarApps } from '~/layout/navigation/SideBar/SideBarApps' @@ -50,17 +47,16 @@ 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 ActivationSidebar from 'lib/components/ActivationSidebar/ActivationSidebar' 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) const { hideSideBarMobile, toggleProjectSwitcher, hideProjectSwitcher } = useActions(navigationLogic) const { isProjectSwitcherShown } = useValues(navigationLogic) const { pinnedDashboards } = useValues(dashboardsModel) - const { showGroupsOptions } = useValues(groupsModel) const { hasAvailableFeature } = useValues(userLogic) const { preflight } = useValues(preflightLogic) const { currentTeam } = useValues(teamLogic) @@ -182,7 +178,7 @@ function Pages(): JSX.Element { icon={} identifier={Scene.WebAnalytics} to={urls.webAnalytics()} - highlight="alpha" + highlight="beta" /> } identifier={Scene.Replay} to={urls.replay()} /> @@ -222,10 +218,10 @@ function Pages(): JSX.Element { to={urls.eventDefinitions()} /> } - identifier={Scene.Persons} + icon={} + identifier={Scene.PersonsManagement} to={urls.persons()} - title={`Persons${showGroupsOptions ? ' & Groups' : ''}`} + title="People" /> } identifier={Scene.Pipeline} to={urls.pipeline()} /> @@ -239,8 +235,6 @@ function Pages(): JSX.Element { highlight="beta" /> - } identifier={Scene.Cohorts} to={urls.cohorts()} /> - } identifier={Scene.Annotations} to={urls.annotations()} /> {canViewPlugins(currentOrganization) || Object.keys(frontendApps).length > 0 ? ( <>
    Apps
    @@ -277,11 +271,7 @@ function Pages(): JSX.Element { }, }} /> - } - identifier={Scene.ProjectSettings} - to={urls.projectSettings()} - /> + } identifier={Scene.Settings} to={urls.settings('project')} /> )} diff --git a/frontend/src/layout/navigation/SideBar/SidebarChangeNotice.tsx b/frontend/src/layout/navigation/SideBar/SidebarChangeNotice.tsx new file mode 100644 index 00000000000000..c95511a491e733 --- /dev/null +++ b/frontend/src/layout/navigation/SideBar/SidebarChangeNotice.tsx @@ -0,0 +1,130 @@ +import { LemonButton, LemonDivider, Tooltip, TooltipProps } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { IconClose } from 'lib/lemon-ui/icons' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import posthog from 'posthog-js' +import React, { Fragment, useState } from 'react' +import { Scene } from 'scenes/sceneTypes' + +export type SidebarChangeNoticeProps = { + identifier: string | number +} + +export type SidebarChangeNoticeTooltipProps = SidebarChangeNoticeProps & { + children: TooltipProps['children'] +} + +/** + * Used in combination with a feature flag like: + * + * sidebar-notice-annotations-2023-10-30 + * matching: + * properties: + * sidebar_notice/annotations-2023-10-30: doesn't equal true + * joined_at: before 2023-10-30 + * + */ + +const NOTICES: { + identifier: Scene + description: React.ReactNode + placement: TooltipProps['placement'] + flagSuffix: string +}[] = [ + { + identifier: Scene.DataManagement, + description: ( + <> + Annotations have moved here! +
    + You can now find them in Data Management + + ), + placement: 'rightBottom', + flagSuffix: 'annotations-2023-10-30', + }, + { + identifier: Scene.PersonsManagement, + description: ( + <> + Cohorts have moved here! +
    + You can now find them in People + + ), + placement: 'rightTop', + flagSuffix: 'cohorts-2023-10-30', + }, +] + +export function SidebarChangeNoticeContent({ + notices, + onAcknowledged, +}: { + notices: typeof NOTICES + onAcknowledged: () => void +}): JSX.Element | null { + return ( +
    +
    + {notices.map((notice, i) => ( + + {notice.description} + {i < notices.length - 1 && } + + ))} +
    + + } /> +
    + ) +} + +export function useSidebarChangeNotices({ identifier }: SidebarChangeNoticeProps): [typeof NOTICES, () => void] { + const { featureFlags } = useValues(featureFlagLogic) + const [noticeAcknowledged, setNoticeAcknowledged] = useState(false) + + const notices = NOTICES.filter((notice) => notice.identifier === identifier).filter( + (notice) => featureFlags[`sidebar-notice-${notice.flagSuffix}`] + ) + + const onAcknowledged = (): void => { + notices.forEach((change) => { + posthog.capture('sidebar notice acknowledged', { + change: change.flagSuffix, + $set: { + [`sidebar_notice/${change.flagSuffix}`]: true, + }, + }) + setNoticeAcknowledged(true) + }) + } + + return [!noticeAcknowledged ? notices : [], onAcknowledged] +} + +export function SidebarChangeNoticeTooltip({ identifier, children }: SidebarChangeNoticeTooltipProps): JSX.Element { + const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier }) + + if (!notices.length) { + return <>{children} + } + + return ( + } + > + {React.cloneElement(children as React.ReactElement, { + onClick: () => { + onAcknowledged() + if (React.isValidElement(children)) { + children.props.onClick?.() + } + }, + })} + + ) +} diff --git a/frontend/src/layout/navigation/TopBar/SitePopover.tsx b/frontend/src/layout/navigation/TopBar/SitePopover.tsx index c59fb8b443ec51..9d8269df080b1b 100644 --- a/frontend/src/layout/navigation/TopBar/SitePopover.tsx +++ b/frontend/src/layout/navigation/TopBar/SitePopover.tsx @@ -29,7 +29,7 @@ import { NewOrganizationButton, OtherOrganizationButton, } from '~/layout/navigation/OrganizationSwitcher' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +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' @@ -63,7 +63,7 @@ function AccountInfo(): JSX.Element {
    } status="stealth" fullWidth - to={urls.organizationSettings()} + to={urls.settings('organization')} onClick={closeSitePopover} >
    diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index b81bbed16648e6..c769d91869ba10 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -5,35 +5,21 @@ import { Announcement } from './Announcement' import { navigationLogic } from '../navigationLogic' import { HelpButton } from 'lib/components/HelpButton/HelpButton' import { CommandPalette } from 'lib/components/CommandPalette' -import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' -import { InviteModal } from 'scenes/organization/Settings/InviteModal' import { Link } from 'lib/lemon-ui/Link' import { IconMenu, IconMenuOpen } from 'lib/lemon-ui/icons' -import { CreateProjectModal } from 'scenes/project/CreateProjectModal' import './TopBar.scss' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' import { UniversalSearchPopover } from 'lib/components/UniversalSearch/UniversalSearchPopover' 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 { FEATURE_FLAGS } from 'lib/constants' -import ActivationSidebarToggle from 'lib/components/ActivationSidebar/ActivationSidebarToggle' import { NotebookButton } from '~/layout/navigation/TopBar/NotebookButton' +import { ActivationSidebarToggle } from 'lib/components/ActivationSidebar/ActivationSidebarToggle' export function TopBar(): JSX.Element { - const { - isSideBarShown, - noSidebar, - minimalTopBar, - mobileLayout, - isCreateOrganizationModalShown, - isCreateProjectModalShown, - } = useValues(navigationLogic) - const { toggleSideBarBase, toggleSideBarMobile, hideCreateOrganizationModal, hideCreateProjectModal } = - useActions(navigationLogic) - const { isInviteModalShown } = useValues(inviteLogic) - const { hideInviteModal } = useActions(inviteLogic) + const { isSideBarShown, noSidebar, minimalTopBar, mobileLayout } = useValues(navigationLogic) + const { toggleSideBarBase, toggleSideBarMobile } = useActions(navigationLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) const { featureFlags } = useValues(featureFlagLogic) @@ -96,9 +82,6 @@ export function TopBar(): JSX.Element {
    - - - ) } diff --git a/frontend/src/layout/navigation/navigationLogic.ts b/frontend/src/layout/navigation/navigationLogic.ts index 5fa4fa296b86e9..2814fbb936b8e4 100644 --- a/frontend/src/layout/navigation/navigationLogic.ts +++ b/frontend/src/layout/navigation/navigationLogic.ts @@ -8,7 +8,7 @@ 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/Settings/membersLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { Scene } from 'scenes/sceneTypes' @@ -35,10 +35,6 @@ export const navigationLogic = kea([ openSitePopover: true, closeSitePopover: true, toggleSitePopover: true, - showCreateOrganizationModal: true, - hideCreateOrganizationModal: true, - showCreateProjectModal: true, - hideCreateProjectModal: true, toggleProjectSwitcher: true, hideProjectSwitcher: true, openAppSourceEditor: (id: number, pluginId: number) => ({ id, pluginId }), @@ -95,20 +91,6 @@ export const navigationLogic = kea([ toggleSitePopover: (state) => !state, }, ], - isCreateOrganizationModalShown: [ - false, - { - showCreateOrganizationModal: () => true, - hideCreateOrganizationModal: () => false, - }, - ], - isCreateProjectModalShown: [ - false, - { - showCreateProjectModal: () => true, - hideCreateProjectModal: () => false, - }, - ], isProjectSwitcherShown: [ false, { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 63e4d08fd2d8f3..805c8c3262ca5a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -19,6 +19,8 @@ import { ExportedAssetType, FeatureFlagAssociatedRoleType, FeatureFlagType, + OrganizationFeatureFlags, + OrganizationFeatureFlagsCopyBody, InsightModel, IntegrationType, MediaUploadResponse, @@ -178,6 +180,20 @@ class ApiRequest { return this.organizationResourceAccess().addPathComponent(id) } + public organizationFeatureFlags(orgId: OrganizationType['id'], featureFlagKey: FeatureFlagType['key']): ApiRequest { + return this.organizations() + .addPathComponent(orgId) + .addPathComponent('feature_flags') + .addPathComponent(featureFlagKey) + } + + public copyOrganizationFeatureFlags(orgId: OrganizationType['id']): ApiRequest { + return this.organizations() + .addPathComponent(orgId) + .addPathComponent('feature_flags') + .addPathComponent('copy_flags') + } + // # Projects public projects(): ApiRequest { return this.addPathComponent('projects') @@ -674,6 +690,21 @@ const api = { }, }, + organizationFeatureFlags: { + async get( + orgId: OrganizationType['id'] = getCurrentOrganizationId(), + featureFlagKey: FeatureFlagType['key'] + ): Promise { + return await new ApiRequest().organizationFeatureFlags(orgId, featureFlagKey).get() + }, + async copy( + orgId: OrganizationType['id'] = getCurrentOrganizationId(), + data: OrganizationFeatureFlagsCopyBody + ): Promise<{ success: FeatureFlagType[]; failed: any }> { + return await new ApiRequest().copyOrganizationFeatureFlags(orgId).create({ data }) + }, + }, + actions: { async get(actionId: ActionType['id']): Promise { return await new ApiRequest().actionsDetail(actionId).get() diff --git a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx index 7dec0282897dda..2e66f6e854a5ad 100644 --- a/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx +++ b/frontend/src/lib/components/ActivationSidebar/ActivationSidebar.tsx @@ -6,11 +6,18 @@ import { activationLogic, ActivationTaskType } from './activationLogic' import './ActivationSidebar.scss' import { Progress } from 'antd' import { IconCheckmark, IconClose } from 'lib/lemon-ui/icons' -import { SessionRecording as SessionRecordingConfig } from 'scenes/project/Settings/SessionRecording' import { ProfessorHog } from '../hedgehogs' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -const Task = ({ id, name, description, completed, canSkip, skipped, url }: ActivationTaskType): JSX.Element => { +export const ActivationTask = ({ + id, + name, + description, + completed, + canSkip, + skipped, + url, +}: ActivationTaskType): JSX.Element => { const displaySideAction = !completed && !skipped && canSkip const { runTask, skipTask } = useActions(activationLogic) const { reportActivationSideBarTaskClicked } = useActions(eventUsageLogic) @@ -60,75 +67,57 @@ const Task = ({ id, name, description, completed, canSkip, skipped, url }: Activ ) } -const ActivationSidebar = (): JSX.Element => { +export const ActivationSidebar = (): JSX.Element => { const { isActivationSideBarShown } = useValues(navigationLogic) const { hideActivationSideBar } = useActions(navigationLogic) - const { activeTasks, completedTasks, completionPercent, showSessionRecordingConfig } = useValues(activationLogic) - const { setShowSessionRecordingConfig } = useActions(activationLogic) + const { activeTasks, completedTasks, completionPercent } = useValues(activationLogic) return (
    - } - onClick={() => { - if (showSessionRecordingConfig) { - setShowSessionRecordingConfig(false) - } else { - hideActivationSideBar() - } - }} - /> + } onClick={() => hideActivationSideBar()} />
    - {showSessionRecordingConfig ? ( - - ) : ( - <> -

    Quick Start

    -

    - Use our Quick Start guide to learn about everything PostHog can do for you and your product. -

    -
    -
    - activeTasks.length} - strokeColor="#345cff" // primary-light - /> -

    still to go

    -
    -
    - -
    + <> +

    Quick Start

    +

    Use our Quick Start guide to learn about everything PostHog can do for you and your product.

    +
    +
    + activeTasks.length} + strokeColor="#345cff" // primary-light + /> +

    still to go

    - {activeTasks.length > 0 && ( -
    -
    What's next?
    -
      - {activeTasks.map((task: ActivationTaskType) => ( - - ))} -
    -
    - )} - {completedTasks.length > 0 && ( -
    -
    Completed
    -
      - {completedTasks.map((task: ActivationTaskType) => ( - - ))} -
    -
    - )} - - )} +
    + +
    +
    + {activeTasks.length > 0 && ( +
    +
    What's next?
    +
      + {activeTasks.map((task: ActivationTaskType) => ( + + ))} +
    +
    + )} + {completedTasks.length > 0 && ( +
    +
    Completed
    +
      + {completedTasks.map((task: ActivationTaskType) => ( + + ))} +
    +
    + )} +
    ) } - -export default ActivationSidebar diff --git a/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx b/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx index 8c22b7f734d5d8..f123a002471354 100644 --- a/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx +++ b/frontend/src/lib/components/ActivationSidebar/ActivationSidebarToggle.tsx @@ -4,10 +4,11 @@ import { navigationLogic } from '~/layout/navigation/navigationLogic' import { Progress } from 'antd' import { activationLogic } from './activationLogic' -const ActivationSidebarToggle = (): JSX.Element | null => { +export const ActivationSidebarToggle = (): JSX.Element | null => { const { mobileLayout } = useValues(navigationLogic) const { toggleActivationSideBar } = useActions(navigationLogic) const { activeTasks, completionPercent, isReady, hasCompletedAllTasks } = useValues(activationLogic) + if (!isReady || hasCompletedAllTasks) { return null } @@ -37,5 +38,3 @@ const ActivationSidebarToggle = (): JSX.Element | null => { ) } - -export default ActivationSidebarToggle diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts index 827d5eb5c3643c..064e9d442b6076 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts @@ -1,6 +1,6 @@ import { expectLogic } from 'kea-test-utils' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { teamLogic } from 'scenes/teamLogic' import { navigationLogic } from '~/layout/navigation/navigationLogic' diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts index 71df41f6c1305b..b2579b889df9e2 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts @@ -3,8 +3,8 @@ 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/organization/Settings/inviteLogic' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { teamLogic } from 'scenes/teamLogic' import { navigationLogic } from '~/layout/navigation/navigationLogic' @@ -13,6 +13,7 @@ import type { activationLogicType } from './activationLogicType' import { urls } from 'scenes/urls' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' import { dashboardsModel } from '~/models/dashboardsModel' +import { permanentlyMount } from 'lib/utils/kea-logic-builders' export enum ActivationTasks { IngestFirstEvent = 'ingest_first_event', @@ -77,7 +78,6 @@ export const activationLogic = kea([ runTask: (id: string) => ({ id }), skipTask: (id: string) => ({ id }), addSkippedTask: (teamId: TeamBasicType['id'], taskId: string) => ({ teamId, taskId }), - setShowSessionRecordingConfig: (value: boolean) => ({ value }), }), reducers(() => ({ skippedTasks: [ @@ -89,12 +89,6 @@ export const activationLogic = kea([ }, }, ], - showSessionRecordingConfig: [ - false, - { - setShowSessionRecordingConfig: (_, { value }) => value, - }, - ], areMembersLoaded: [ false, { @@ -143,7 +137,7 @@ export const activationLogic = kea([ 0, { loadCustomEvents: async (_, breakpoint) => { - breakpoint(200) + await breakpoint(200) const url = api.eventDefinitions.determineListEndpoint({ event_type: EventDefinitionType.EventCustom, }) @@ -345,7 +339,7 @@ export const activationLogic = kea([ router.actions.push(urls.dashboards()) break case ActivationTasks.SetupSessionRecordings: - actions.setShowSessionRecordingConfig(true) + router.actions.push(urls.replay()) break case ActivationTasks.InstallFirstApp: router.actions.push(urls.projectApps()) @@ -359,9 +353,6 @@ export const activationLogic = kea([ actions.addSkippedTask(values.currentTeam.id, id) } }, - toggleActivationSideBar: async () => { - actions.setShowSessionRecordingConfig(false) - }, showActivationSideBar: async () => { actions.reportActivationSideBarShown( values.activeTasks.length, @@ -385,4 +376,5 @@ export const activationLogic = kea([ } }, })), + permanentlyMount(), ]) diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index b42ba8047e90dd..8176646fac6e32 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -127,6 +127,7 @@ .SeriesDisplay__raw-name { display: inline-flex; + align-items: center; padding: 0.125rem 0.25rem; margin: 0 0.25rem; background: var(--primary-bg-hover); @@ -135,12 +136,12 @@ font-size: 0.6875rem; font-weight: 600; line-height: 1rem; - vertical-align: -0.3em; &.SeriesDisplay__raw-name--action, &.SeriesDisplay__raw-name--event { padding: 0.25rem; &::before { display: inline-block; + flex-shrink: 0; text-align: center; width: 1rem; border-radius: 0.25rem; @@ -169,5 +170,4 @@ margin-right: 0.25rem; color: var(--border-bold); font-size: 1.25rem; - vertical-align: middle; } diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx index ea7eeaa423f79a..15224b0c3d747d 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.stories.tsx @@ -3,21 +3,23 @@ import { useState } from 'react' import { ChartDisplayType, InsightColor, InsightModel, InsightShortId, TrendsFilterType } from '~/types' import { InsightCard as InsightCardComponent } from './index' -import EXAMPLE_TRENDS from '../../../../mocks/fixtures/api/projects/:team_id/insights/trendsLine.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_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 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_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' const examples = [ EXAMPLE_TRENDS, + EXAMPLE_TRENDS_MULTI, EXAMPLE_TRENDS_HORIZONTAL_BAR, EXAMPLE_TRENDS_TABLE, EXAMPLE_TRENDS_PIE, diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx index fd45ba5fed66dc..73d5b42ac3841e 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx @@ -18,6 +18,7 @@ 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' diff --git a/frontend/src/lib/components/CloseButton.tsx b/frontend/src/lib/components/CloseButton.tsx index c75a598bd5dcde..abd6ca7d2ef777 100644 --- a/frontend/src/lib/components/CloseButton.tsx +++ b/frontend/src/lib/components/CloseButton.tsx @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-restricted-imports import { CloseOutlined } from '@ant-design/icons' // TODO: Remove, but de-ant PropertyFilterButton and SelectGradientOverflow first diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index a29dca16342aec..d601677ab3ce45 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -8,7 +8,7 @@ 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 '../PersonalAPIKeys/personalAPIKeysLogic' +import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' import { teamLogic } from 'scenes/teamLogic' import posthog from 'posthog-js' import { debugCHQueries } from './DebugCHQueries' @@ -494,7 +494,7 @@ export const commandPaletteLogic = kea([ display: 'Go to Team members', synonyms: ['organization', 'members', 'invites', 'teammates'], executor: () => { - push(urls.organizationSettings()) + push(urls.settings('organization')) }, }, { @@ -508,7 +508,7 @@ export const commandPaletteLogic = kea([ icon: IconSettings, display: 'Go to Project settings', executor: () => { - push(urls.projectSettings()) + push(urls.settings('project')) }, }, { @@ -518,7 +518,7 @@ export const commandPaletteLogic = kea([ display: 'Go to My settings', synonyms: ['account'], executor: () => { - push(urls.mySettings()) + push(urls.settings('user')) }, }, { @@ -658,7 +658,7 @@ export const commandPaletteLogic = kea([ display: `Create Key "${argument}"`, executor: () => { personalAPIKeysLogic.actions.createKey(argument) - push(urls.mySettings(), {}, 'personal-api-keys') + push(urls.settings('user'), {}, 'personal-api-keys') }, } } diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx index 559bd573735e48..65a7711d3b635b 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx @@ -8,7 +8,7 @@ import { KeyMapping, UserBasicType, PropertyDefinition } from '~/types' import { Owner } from 'scenes/events/Owner' import { dayjs } from 'lib/dayjs' import { Divider, DividerProps, Select } from 'antd' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' diff --git a/frontend/src/lib/components/InsightLabel/index.tsx b/frontend/src/lib/components/InsightLabel/index.tsx index 0969e8ca353d8d..2f11144d7c2cf1 100644 --- a/frontend/src/lib/components/InsightLabel/index.tsx +++ b/frontend/src/lib/components/InsightLabel/index.tsx @@ -1,4 +1,3 @@ -import { Tag } from 'antd' import { ActionFilter, BreakdownKeyType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { capitalizeFirstLetter, hexToRGBA, midEllipsis } from 'lib/utils' @@ -10,6 +9,7 @@ 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' export enum IconSize { Small = 'small', @@ -54,18 +54,18 @@ function MathTag({ math, mathProperty, mathHogQL, mathGroupTypeIndex }: MathTagP const { aggregationLabel } = useValues(groupsModel) if (!math || math === 'total') { - return Total + return Total } if (math === 'dau') { - return Unique + return Unique } if (math === 'unique_group' && mathGroupTypeIndex != undefined) { - return Unique {aggregationLabel(mathGroupTypeIndex).plural} + return Unique {aggregationLabel(mathGroupTypeIndex).plural} } if (math && ['sum', 'avg', 'min', 'max', 'median', 'p90', 'p95', 'p99'].includes(math || '')) { return ( <> - {mathDefinitions[math]?.name || capitalizeFirstLetter(math)} + {mathDefinitions[math]?.name || capitalizeFirstLetter(math)} {mathProperty && ( <> of @@ -76,13 +76,9 @@ function MathTag({ math, mathProperty, mathHogQL, mathGroupTypeIndex }: MathTagP ) } if (math === 'hogql') { - return ( - - {String(mathHogQL)} - - ) + return {String(mathHogQL) || 'HogQL'} } - return {capitalizeFirstLetter(math)} + return {capitalizeFirstLetter(math)} } export function InsightLabel({ @@ -171,12 +167,12 @@ export function InsightLabel({
    {pillValues.map((pill) => ( - + {/* eslint-disable-next-line react/forbid-dom-props */} {pillMidEllipsis ? midEllipsis(String(pill), 50) : pill} - + ))}
    diff --git a/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts new file mode 100644 index 00000000000000..d2136d8d8a682a --- /dev/null +++ b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts @@ -0,0 +1,144 @@ +import { kea, props, key, path, connect, actions, reducers, listeners } from 'kea' +import { objectsEqual, dateMapping } from 'lib/utils' +import type { intervalFilterLogicType } from './intervalFilterLogicType' +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 { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + +export const intervalFilterLogic = kea([ + props({} as InsightLogicProps), + key(keyForInsightLogicProps('new')), + path((key) => ['lib', 'components', 'IntervalFilter', 'intervalFilterLogic', key]), + connect((props: InsightLogicProps) => ({ + actions: [insightVizDataLogic(props), ['updateQuerySource']], + values: [insightVizDataLogic(props), ['interval', 'querySource']], + })), + actions(() => ({ + setInterval: (interval: IntervalKeyType) => ({ interval }), + setEnabledIntervals: (enabledIntervals: Intervals) => ({ enabledIntervals }), + })), + reducers(() => ({ + enabledIntervals: [ + { ...intervals } as Intervals, + { + setEnabledIntervals: (_, { enabledIntervals }) => enabledIntervals, + }, + ], + })), + listeners(({ values, actions, selectors }) => ({ + setInterval: ({ interval }) => { + if (values.interval !== interval) { + actions.updateQuerySource({ interval } as Partial) + } + }, + updateQuerySource: ({ querySource }, _, __, previousState) => { + const { date_from, date_to } = querySource.dateRange || {} + const previousDateRange = selectors.querySource(previousState)?.dateRange || {} + + let activeUsersMath: BaseMathType.WeeklyActiveUsers | BaseMathType.MonthlyActiveUsers | null = null + + // We disallow grouping by certain intervals for weekly active users and monthly active users views + // e.g. WAUs grouped by month. Here, look for the first event/action running WAUs/MAUs math and + // pass that down to the interval filter to determine what groupings are allowed. + for (const series of (values.querySource as TrendsQuery)?.series || []) { + if (series.math === BaseMathType.WeeklyActiveUsers) { + activeUsersMath = BaseMathType.WeeklyActiveUsers + break + } + + if (series.math === BaseMathType.MonthlyActiveUsers) { + activeUsersMath = BaseMathType.MonthlyActiveUsers + break + } + } + + const enabledIntervals: Intervals = { ...intervals } + + if (activeUsersMath) { + // Disallow grouping by hour for WAUs/MAUs as it's an expensive query that produces a view that's not useful for users + enabledIntervals.hour = { + ...enabledIntervals.hour, + disabledReason: + 'Grouping by hour is not supported on insights with weekly or monthly active users series.', + } + + // Disallow grouping by month for WAUs as the resulting view is misleading to users + if (activeUsersMath === BaseMathType.WeeklyActiveUsers) { + enabledIntervals.month = { + ...enabledIntervals.month, + disabledReason: + 'Grouping by month is not supported on insights with weekly active users series.', + } + } + } + + actions.setEnabledIntervals(enabledIntervals) + + // If the user just flipped an event action to use WAUs/MAUs math and their + // current interval is unsupported by the math type, switch their interval + // to an appropriate allowed interval and inform them of the change via a toast + if ( + activeUsersMath && + (values.querySource as TrendsQuery)?.interval && + enabledIntervals[(values.querySource as TrendsQuery).interval as IntervalType].disabledReason + ) { + if (values.interval === 'hour') { + lemonToast.info( + `Switched to grouping by day, because "${BASE_MATH_DEFINITIONS[activeUsersMath].name}" does not support grouping by ${values.interval}.` + ) + actions.updateQuerySource({ interval: 'day' } as Partial) + } else { + lemonToast.info( + `Switched to grouping by week, because "${BASE_MATH_DEFINITIONS[activeUsersMath].name}" does not support grouping by ${values.interval}.` + ) + actions.updateQuerySource({ interval: 'week' } as Partial) + } + return + } + + if ( + !date_from || + (objectsEqual(date_from, previousDateRange.date_from) && + objectsEqual(date_to, previousDateRange.date_to)) + ) { + return + } + + // automatically set an interval for fixed date ranges + if ( + date_from && + date_to && + dayjs(querySource.dateRange?.date_from).isValid() && + dayjs(querySource.dateRange?.date_to).isValid() + ) { + if (dayjs(date_to).diff(dayjs(date_from), 'day') <= 3) { + actions.updateQuerySource({ interval: 'hour' } as Partial) + } else if (dayjs(date_to).diff(dayjs(date_from), 'month') <= 3) { + actions.updateQuerySource({ interval: 'day' } as Partial) + } else { + actions.updateQuerySource({ interval: 'month' } as Partial) + } + return + } + // get a defaultInterval for dateOptions that have a default value + let interval: IntervalType = 'day' + for (const { key, values, defaultInterval } of dateMapping) { + if ( + values[0] === date_from && + values[1] === (date_to || undefined) && + key !== 'Custom' && + defaultInterval + ) { + interval = defaultInterval + break + } + } + actions.updateQuerySource({ interval } as Partial) + }, + })), +]) diff --git a/frontend/src/lib/components/JSBookmarklet.tsx b/frontend/src/lib/components/JSBookmarklet.tsx index 6202320a403f89..54d87d84cc9af6 100644 --- a/frontend/src/lib/components/JSBookmarklet.tsx +++ b/frontend/src/lib/components/JSBookmarklet.tsx @@ -2,6 +2,7 @@ import { TeamBasicType } from '~/types' import { useActions } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { IconBookmarkBorder } from 'lib/lemon-ui/icons' +import { useEffect, useRef } from 'react' 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!')})` @@ -10,12 +11,18 @@ export function JSBookmarklet({ team }: { team: TeamBasicType }): JSX.Element { )}%7D%7D)()` const { reportBookmarkletDragged } = useActions(eventUsageLogic) + const ref = useRef(null) + + useEffect(() => { + // React cleverly stops js links from working, so we need to set the href manually + ref.current?.setAttribute('href', href) + }, [ref.current, href]) return ( <> {/* eslint-disable-next-line react/forbid-elements */} diff --git a/frontend/src/lib/components/JSSnippet.tsx b/frontend/src/lib/components/JSSnippet.tsx index 1c4bafcb11d80a..8458e79ad9c1bc 100644 --- a/frontend/src/lib/components/JSSnippet.tsx +++ b/frontend/src/lib/components/JSSnippet.tsx @@ -7,7 +7,7 @@ export function JSSnippet(): JSX.Element { return ( {``} ) diff --git a/frontend/src/lib/components/Map/Map.stories.tsx b/frontend/src/lib/components/Map/Map.stories.tsx index 41807351e63677..e7e120a8244a39 100644 --- a/frontend/src/lib/components/Map/Map.stories.tsx +++ b/frontend/src/lib/components/Map/Map.stories.tsx @@ -14,6 +14,11 @@ const meta: Meta = { center: coordinates, className: 'h-60', }, + parameters: { + testOptions: { + skip: true, + }, + }, } type Story = StoryObj diff --git a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx index 8d9e0d438cb3e6..00d447b71ff0c6 100644 --- a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx +++ b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx @@ -1,7 +1,8 @@ import { Tag, Select } from 'antd' import { colorForString } from 'lib/utils' import { CSSProperties, useMemo } from 'react' -import { PlusOutlined, SyncOutlined, CloseOutlined } from '@ant-design/icons' +// eslint-disable-next-line no-restricted-imports +import { SyncOutlined, CloseOutlined } from '@ant-design/icons' import { SelectGradientOverflow } from '../SelectGradientOverflow' import { useActions, useValues } from 'kea' import { objectTagsLogic } from 'lib/components/ObjectTags/objectTagsLogic' @@ -9,6 +10,7 @@ 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' interface ObjectTagsPropsBase { tags: string[] @@ -126,7 +128,7 @@ export function ObjectTags({ backgroundColor: 'var(--bg-light)', display: addingNewTag ? 'none' : 'initial', }} - icon={} + icon={} > Add tag diff --git a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx index d981c95aad5a77..d14d233f90f3b2 100644 --- a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx +++ b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx @@ -36,6 +36,7 @@ const SortableProperty = ({ className={clsx(sortable ? 'cursor-move' : 'cursor-auto')} {...attributes} {...listeners} + // eslint-disable-next-line react/forbid-dom-props style={{ transform: CSS.Translate.toString(transform), transition, diff --git a/frontend/src/lib/components/PersonalAPIKeys/index.tsx b/frontend/src/lib/components/PersonalAPIKeys/index.tsx deleted file mode 100644 index 4b79332193d2cf..00000000000000 --- a/frontend/src/lib/components/PersonalAPIKeys/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { PersonalAPIKeys } from './PersonalAPIKeys' diff --git a/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx b/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx index fb925b583c6695..484b294b29b475 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx @@ -38,7 +38,6 @@ export function ComparingPropertyFilters(): JSX.Element { propertyFilters={[...propertyFilters]} onChange={() => {}} pageKey={'pageKey'} - style={{ marginBottom: 0 }} showNestedArrow eventNames={[]} /> @@ -48,7 +47,6 @@ export function ComparingPropertyFilters(): JSX.Element { propertyFilters={[...propertyFilters]} onChange={() => {}} pageKey={'pageKey'} - style={{ marginBottom: 0 }} eventNames={[]} disablePopover={true} /> diff --git a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx index 18415dba6c362c..dc9506368a0cdb 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, useEffect } from 'react' +import React, { useEffect } from 'react' import { useValues, BindLogic, useActions } from 'kea' import { propertyFilterLogic } from './propertyFilterLogic' import { FilterRow } from './components/FilterRow' @@ -15,7 +15,6 @@ interface PropertyFiltersProps { pageKey: string showConditionBadge?: boolean disablePopover?: boolean - style?: CSSProperties taxonomicGroupTypes?: TaxonomicFilterGroupType[] hogQLTable?: string showNestedArrow?: boolean @@ -39,7 +38,6 @@ export function PropertyFilters({ disablePopover = false, // use bare PropertyFilter without popover taxonomicGroupTypes, hogQLTable, - style = {}, showNestedArrow = false, eventNames = [], orFiltering = false, @@ -62,7 +60,7 @@ export function PropertyFilters({ }, [propertyFilters]) return ( -
    +
    {showNestedArrow && !disablePopover &&
    {<>↳}
    }
    diff --git a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx index 838569a81c8ab6..817677a1f7f8ab 100644 --- a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx @@ -126,7 +126,7 @@ export function OperatorValueSelect({ />
    {!isOperatorFlag(currentOperator || PropertyOperator.Exact) && type && propkey && ( -
    +
    = ({ filters, style }: Props) => { +const PropertyFiltersDisplay = ({ filters }: { filters: AnyPropertyFilter[] }): JSX.Element => { return ( -
    +
    {filters && filters.map((item) => { return diff --git a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx index e99bcf464c9e2c..1ee7d36a6d27d4 100644 --- a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx @@ -109,7 +109,7 @@ export function TaxonomicPropertyFilter({ const { ref: wrapperRef, size } = useResizeBreakpoints({ 0: 'tiny', - 400: 'small', + 300: 'small', 550: 'medium', }) diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index 9f5b93fa7c313b..b6371b8881c312 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -79,6 +79,11 @@ export function isEventPropertyFilter(filter?: AnyFilterLike | null): filter is export function isPersonPropertyFilter(filter?: AnyFilterLike | null): filter is PersonPropertyFilter { return filter?.type === PropertyFilterType.Person } +export function isEventPropertyOrPersonPropertyFilter( + filter?: AnyFilterLike | null +): filter is EventPropertyFilter | PersonPropertyFilter { + return filter?.type === PropertyFilterType.Event || filter?.type === PropertyFilterType.Person +} export function isElementPropertyFilter(filter?: AnyFilterLike | null): filter is ElementPropertyFilter { return filter?.type === PropertyFilterType.Element } diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss index 35addf19045642..6bfa3e13ada308 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.scss @@ -77,6 +77,6 @@ } .ant-select-item-option-active:not(.ant-select-item-option-disabled) { - background: #e5ebff; + background: var(--primary-highlight); } } diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx index f0d26edd7c1a47..30d7c835ccf9f9 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx @@ -113,7 +113,6 @@ export function PropertyGroupFilters({ ? (group.values as AnyPropertyFilter[]) : null } - style={{ marginBottom: 0 }} onChange={(properties) => { setPropertyFilters(properties, propertyGroupIndex) }} diff --git a/frontend/src/lib/components/RestrictedArea.tsx b/frontend/src/lib/components/RestrictedArea.tsx index 9d86714ce8cd60..c334a3a2235e6c 100644 --- a/frontend/src/lib/components/RestrictedArea.tsx +++ b/frontend/src/lib/components/RestrictedArea.tsx @@ -18,17 +18,16 @@ export enum RestrictionScope { Project = 'project', } -export interface RestrictedAreaProps { - Component: (props: RestrictedComponentProps) => JSX.Element +export interface UseRestrictedAreaProps { minimumAccessLevel: EitherMembershipLevel scope?: RestrictionScope } -export function RestrictedArea({ - Component, - minimumAccessLevel, - scope = RestrictionScope.Organization, -}: RestrictedAreaProps): JSX.Element { +export interface RestrictedAreaProps extends UseRestrictedAreaProps { + Component: (props: RestrictedComponentProps) => JSX.Element +} + +export function useRestrictedArea({ scope, minimumAccessLevel }: UseRestrictedAreaProps): null | string { const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) @@ -59,6 +58,16 @@ export function RestrictedArea({ return null }, [currentOrganization]) + return restrictionReason +} + +export function RestrictedArea({ + Component, + minimumAccessLevel, + scope = RestrictionScope.Organization, +}: RestrictedAreaProps): JSX.Element { + const restrictionReason = useRestrictedArea({ minimumAccessLevel, scope }) + return restrictionReason ? ( diff --git a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts index 7b8709210517fa..88182d3131a78e 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts +++ b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts @@ -7,7 +7,7 @@ import { userLogic } from 'scenes/userLogic' import { useMocks } from '~/mocks/jest' import { Scene } from 'scenes/sceneTypes' -describe('sceneDashboardChoiceModalLogic ', () => { +describe('sceneDashboardChoiceModalLogic', () => { let logic: ReturnType beforeEach(async () => { diff --git a/frontend/src/lib/components/SelectGradientOverflow.tsx b/frontend/src/lib/components/SelectGradientOverflow.tsx index 253e645227d3b0..1623c08976ae35 100644 --- a/frontend/src/lib/components/SelectGradientOverflow.tsx +++ b/frontend/src/lib/components/SelectGradientOverflow.tsx @@ -1,3 +1,5 @@ +// 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' @@ -7,7 +9,6 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' import './SelectGradientOverflow.scss' import { useValues } from 'kea' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { LoadingOutlined } from '@ant-design/icons' interface DropdownGradientRendererProps { updateScrollGradient: () => void diff --git a/frontend/src/lib/components/SeriesGlyph.tsx b/frontend/src/lib/components/SeriesGlyph.tsx index a34d6337951e76..156ebcf5f367bd 100644 --- a/frontend/src/lib/components/SeriesGlyph.tsx +++ b/frontend/src/lib/components/SeriesGlyph.tsx @@ -10,6 +10,7 @@ interface SeriesGlyphProps { export function SeriesGlyph({ className, style, children, variant }: SeriesGlyphProps): JSX.Element { return ( + // eslint-disable-next-line react/forbid-dom-props
    {children}
    diff --git a/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx b/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx index 961fa82e2ef849..d21d6a9bb6808f 100644 --- a/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx +++ b/frontend/src/lib/components/SmoothingFilter/SmoothingFilter.tsx @@ -1,10 +1,11 @@ -import { Select } from 'antd' +// eslint-disable-next-line no-restricted-imports import { FundOutlined } from '@ant-design/icons' import { smoothingOptions } from './smoothings' 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' export function SmoothingFilter(): JSX.Element | null { const { insightProps } = useValues(insightLogic) @@ -31,9 +32,8 @@ export function SmoothingFilter(): JSX.Element | null { })) return options.length ? ( - } - > - {OPTIONS.map(({ value, Icon }, index) => ( - -
    - {value} + options={OPTIONS.map(({ value, Icon }) => ({ + value, + label: value, + labelInMenu: ( +
    + + {value}
    - - ))} - + ), + }))} + /> ) } @@ -107,38 +107,36 @@ export function ExperimentImplementationDetails({ experiment }: ExperimentImplem } return ( - Feature flag usage and implementation} - className="experiment-implementation-details" - > -
    -
    - Variant group - -
    -
    - +
    +
    Feature flag usage and implementation
    +
    +
    +
    + Variant group + ({ + value: variant.key, + label: variant.key, + }) + )} + /> +
    +
    + +
    -
    - Implement your experiment in code - + Implement your experiment in code + - - See the docs for more implementation information. - - + + See the docs for more implementation information. + +
    +
    ) } diff --git a/frontend/src/scenes/experiments/ExperimentPreview.tsx b/frontend/src/scenes/experiments/ExperimentPreview.tsx index 928d4e6d3a97f9..b713568b859f46 100644 --- a/frontend/src/scenes/experiments/ExperimentPreview.tsx +++ b/frontend/src/scenes/experiments/ExperimentPreview.tsx @@ -1,4 +1,4 @@ -import { Col, InputNumber, Row, Slider, Tooltip } from 'antd' +import { InputNumber, Slider, Tooltip } from 'antd' import { useValues, useActions } from 'kea' import { InsightLabel } from 'lib/components/InsightLabel' import { PropertyFilterButton } from 'lib/components/PropertyFilters/components/PropertyFilterButton' @@ -13,7 +13,7 @@ import { import { experimentLogic } from './experimentLogic' import { ExperimentWorkflow } from './ExperimentWorkflow' import { humanFriendlyNumber } from 'lib/utils' -import { LemonButton, LemonModal } from '@posthog/lemon-ui' +import { LemonButton, LemonDivider, LemonModal } from '@posthog/lemon-ui' import { Field, Form } from 'kea-forms' import { MetricSelector } from './MetricSelector' import { IconInfo } from 'lib/lemon-ui/icons' @@ -84,85 +84,85 @@ export function ExperimentPreview({ const targetingProperties = experiment.feature_flag?.filters return ( - - +
    +
    {experimentId === 'new' && ( - +
    -
    - Experiment preview -
    -
    - Here are the baseline metrics for your experiment. Adjust your minimum detectible - threshold to adjust for the smallest conversion value you'll accept, and the experiment - duration.{' '} -
    + Experiment preview +
    +
    + Here are the baseline metrics for your experiment. Adjust your minimum detectible threshold + to adjust for the smallest conversion value you'll accept, and the experiment duration.{' '}
    - + +
    )} {(experimentId === 'new' || editingExistingExperiment) && ( - - -
    - Minimum acceptable improvement - - - -
    - - - { - setExperiment({ - parameters: { - ...experiment.parameters, - minimum_detectable_effect: value, - }, - }) - }} - tipFormatter={(value) => `${value}%`} - /> - - +
    + Minimum acceptable improvement + + + +
    +
    +
    + `${value}%`} - style={{ margin: '0 16px' }} value={minimumDetectableChange} + min={1} + max={sliderMaxValue} + trackStyle={{ background: 'var(--primary)' }} + handleStyle={{ background: 'var(--primary)' }} onChange={(value) => { setExperiment({ parameters: { ...experiment.parameters, - minimum_detectable_effect: value ?? undefined, + minimum_detectable_effect: value, }, }) }} + tipFormatter={(value) => `${value}%`} /> - - - +
    + `${value}%`} + style={{ margin: '0 16px' }} + value={minimumDetectableChange} + onChange={(value) => { + setExperiment({ + parameters: { + ...experiment.parameters, + minimum_detectable_effect: value ?? undefined, + }, + }) + }} + /> +
    +
    )} - +
    {experimentInsightType === InsightType.TRENDS ? ( - <> +
    {!experiment?.start_date && ( <> - +
    Baseline Count
    {humanFriendlyNumber(trendCount || 0)}
    - - +
    +
    Minimum Acceptable Count
    {humanFriendlyNumber( @@ -170,50 +170,50 @@ export function ExperimentPreview({ 0 )}
    - +
    )} - +
    Recommended running time
    ~{humanFriendlyNumber(trendExposure || 0)} days
    - - +
    +
    ) : ( - <> +
    {!experiment?.start_date && ( <> - +
    Baseline Conversion Rate
    {funnelConversionRate.toFixed(1)}%
    - - +
    +
    Minimum Acceptable Conversion Rate
    {(funnelConversionRate + minimumDetectableChange).toFixed(1)}%
    - +
    )} - +
    Recommended Sample Size
    ~{humanFriendlyNumber(funnelSampleSize || 0)} persons
    - +
    {!experiment?.start_date && ( - +
    Recommended running time
    ~{humanFriendlyNumber(runningTime || 0)} days
    - +
    )} - +
    )} - - +
    +
    Experiment variants
      {experiment?.parameters?.feature_flag_variants?.map( @@ -222,10 +222,10 @@ export function ExperimentPreview({ ) )}
    - - +
    +
    Participants
    -
    +
    {targetingProperties ? ( <> {groupFilters(targetingProperties, undefined, aggregationLabel)} @@ -247,108 +247,104 @@ export function ExperimentPreview({ '100% of all users' )}
    - - - +
    +
    +
    {experimentId !== 'new' && !editingExistingExperiment && ( - +
    Start date
    {experiment?.start_date ? ( ) : ( Not started yet )} - +
    )} {experimentInsightType === InsightType.FUNNELS && showEndDate ? ( - +
    Expected end date
    {expectedEndDate.isAfter(dayjs()) ? expectedEndDate.format('D MMM YYYY') : dayjs().format('D MMM YYYY')} - +
    ) : null} {/* The null prevents showing a 0 while loading */} {experiment?.end_date && ( - +
    Completed date
    - +
    )} - - +
    +
    {experimentId !== 'new' && !editingExistingExperiment && ( - - -
    - {experimentInsightType === InsightType.FUNNELS ? 'Conversion goal steps' : 'Trend goal'} -
    - - {experiment?.start_date && ( - <> -
    - - Change experiment goal - -
    - {experimentInsightType === InsightType.TRENDS && - !experimentMathAggregationForTrends && ( - <> -
    - Exposure metric - +
    + {experimentInsightType === InsightType.FUNNELS ? 'Conversion goal steps' : 'Trend goal'} +
    + + {experiment?.start_date && ( + <> +
    + + Change experiment goal + +
    + {experimentInsightType === InsightType.TRENDS && + !experimentMathAggregationForTrends && ( + <> +
    + Exposure metric + + + +
    + {experiment.parameters?.custom_exposure_filter ? ( + + ) : ( + + Default via $feature_flag_called events + + )} +
    + + - - -
    - {experiment.parameters?.custom_exposure_filter ? ( - - ) : ( - - Default via $feature_flag_called events - - )} -
    - + Change exposure metric + + {experiment.parameters?.custom_exposure_filter && ( updateExperimentExposure(null)} > - Change exposure metric + Reset exposure - {experiment.parameters?.custom_exposure_filter && ( - updateExperimentExposure(null)} - > - Reset exposure - - )} - -
    - - )} - - )} - - + )} + +
    + + )} + + )} +
    )} - +
    {experimentId !== 'new' && !editingExistingExperiment && !experiment?.start_date && ( - +
    - +
    )} -
    +
    ) } @@ -451,8 +447,8 @@ export function MetricDisplay({ filters }: { filters?: FilterType }): JSX.Elemen {([...(filters?.events || []), ...(filters?.actions || [])] as ActionFilterType[]) .sort((a, b) => (a.order || 0) - (b.order || 0)) .map((event: ActionFilterType, idx: number) => ( - - +
    +
    {experimentInsightType === InsightType.FUNNELS ? (event.order || 0) + 1 : idx + 1}
    @@ -464,11 +460,11 @@ export function MetricDisplay({ filters }: { filters?: FilterType }): JSX.Elemen showEventName /> - +
    {event.properties?.map((prop: AnyPropertyFilter) => ( ))} - +
    ))} ) diff --git a/frontend/src/scenes/experiments/ExperimentResult.tsx b/frontend/src/scenes/experiments/ExperimentResult.tsx index 25777a70230c00..b5a311f815f5b7 100644 --- a/frontend/src/scenes/experiments/ExperimentResult.tsx +++ b/frontend/src/scenes/experiments/ExperimentResult.tsx @@ -1,9 +1,8 @@ -import { Col, Progress, Row, Skeleton, Tooltip } from 'antd' +import { Col, Progress, Tooltip } from 'antd' import { useValues } from 'kea' import { ChartDisplayType, FilterType, FunnelVizType, InsightShortId, InsightType } from '~/types' import './Experiment.scss' import { experimentLogic } from './experimentLogic' -import { InfoCircleOutlined } from '@ant-design/icons' import { FunnelLayout } from 'lib/constants' import { capitalizeFirstLetter } from 'lib/utils' import { getSeriesColor } from 'lib/colors' @@ -11,6 +10,8 @@ import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' import { NodeKind } from '~/queries/schema' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' +import { IconInfo } from '@posthog/icons' +import { LoadingState } from './Experiment' export function ExperimentResult(): JSX.Element { const { @@ -34,14 +35,7 @@ export function ExperimentResult(): JSX.Element { {experimentResults ? ( (experiment?.parameters?.feature_flag_variants?.length || 0) > 4 ? ( <> - +
    Variant {sortedExperimentResultVariants.map((variant, idx) => ( {capitalizeFirstLetter(variant)} ))} - - +
    +
    {experimentInsightType === InsightType.TRENDS ? 'Count' : 'Conversion Rate'} @@ -73,15 +60,8 @@ export function ExperimentResult(): JSX.Element { : `${conversionRateForVariant(variant)}%`} ))} - - +
    +
    Probability to be the best {sortedExperimentResultVariants.map((variant, idx) => ( @@ -92,24 +72,24 @@ export function ExperimentResult(): JSX.Element { ))} - +
    ) : ( - +
    { //sort by decreasing probability Object.keys(experimentResults.probability) .sort((a, b) => experimentResults.probability[b] - experimentResults.probability[a]) .map((variant, idx) => ( - +
    {capitalizeFirstLetter(variant)}
    {experimentInsightType === InsightType.TRENDS ? ( <> - +
    - +
    {'action' in experimentResults.insight[0] && ( - +
    {' '} {countDataForVariant(variant)}{' '} {areTrendResultsConfusing && idx === 0 && ( @@ -129,20 +109,22 @@ export function ExperimentResult(): JSX.Element { placement="right" title="It might seem confusing that the best variant has lower absolute count, but this can happen when fewer people are exposed to this variant, so its relative count is higher." > - + )} - +
    Exposure:{' '} {exposureCountDataForVariant(variant)}
    ) : ( - - Conversion rate:{' '} - {conversionRateForVariant(variant)}% - +
    + + Conversion rate:{' '} + + {conversionRateForVariant(variant)}% +
    )} {(experimentResults.probability[variant] * 100).toFixed(1)}%
    - +
    )) } -
    - ) - ) : ( - experimentResultsLoading && ( -
    -
    ) + ) : ( + experimentResultsLoading && )} {experimentResults ? ( // :KLUDGE: using `insights-page` for proper styling, should rather adapt styles @@ -198,7 +176,7 @@ export function ExperimentResult(): JSX.Element { ) : ( experiment.start_date && ( <> -
    +
    {!experimentResultsLoading && (
    There are no results for this experiment yet. diff --git a/frontend/src/scenes/experiments/ExperimentWorkflow.tsx b/frontend/src/scenes/experiments/ExperimentWorkflow.tsx index 359e07cd0f7f5c..430ab7f0624b90 100644 --- a/frontend/src/scenes/experiments/ExperimentWorkflow.tsx +++ b/frontend/src/scenes/experiments/ExperimentWorkflow.tsx @@ -1,60 +1,71 @@ -import { Card, Col, Row } from 'antd' import { IconCheckmark, IconRadioButtonUnchecked } from 'lib/lemon-ui/icons' import { useState } from 'react' import './Experiment.scss' +import clsx from 'clsx' export function ExperimentWorkflow(): JSX.Element { const [workflowValidateStepCompleted, setWorkflowValidateStepCompleted] = useState(false) const [workflowLaunchStepCompleted, setWorkflowLaunchStepCompleted] = useState(false) return ( - Experiment workflow}> - - - - - Create experiment - -
    Set variants, select participants, and add secondary metrics
    - -
    - - setWorkflowValidateStepCompleted(!workflowValidateStepCompleted)} - > - - {workflowValidateStepCompleted ? ( - - ) : ( - - )} - Validate experiment - -
    - Once you've written your code, it's a good idea to test that each variant behaves as you'd - expect. + <> +
    +
    Experiment workflow
    +
    +
    +
    +
    + + Create experiment +
    +
    Set variants, select participants, and add secondary metrics
    +
    - - - - setWorkflowLaunchStepCompleted(!workflowLaunchStepCompleted)} - > - - {workflowLaunchStepCompleted ? ( - - ) : ( - - )} - Launch experiment - -
    - Run your experiment, monitor results, and decide when to terminate your experiment. +
    +
    setWorkflowValidateStepCompleted(!workflowValidateStepCompleted)} + > +
    + {workflowValidateStepCompleted ? ( + + ) : ( + + )} + Validate experiment +
    +
    + Once you've written your code, it's a good idea to test that each variant behaves as + you'd expect. +
    +
    - - - +
    +
    setWorkflowLaunchStepCompleted(!workflowLaunchStepCompleted)} + > +
    + {workflowLaunchStepCompleted ? ( + + ) : ( + + )} + Launch experiment +
    +
    + Run your experiment, monitor results, and decide when to terminate your experiment. +
    +
    +
    +
    +
    + ) } diff --git a/frontend/src/scenes/experiments/Experiments.tsx b/frontend/src/scenes/experiments/Experiments.tsx index 9bc342dcffb2d9..5595ba60f5bca0 100644 --- a/frontend/src/scenes/experiments/Experiments.tsx +++ b/frontend/src/scenes/experiments/Experiments.tsx @@ -10,7 +10,6 @@ import { urls } from 'scenes/urls' import stringWithWBR from 'lib/utils/stringWithWBR' import { Link } from 'lib/lemon-ui/Link' import { dayjs } from 'lib/dayjs' -import { Tag } from 'antd' import { More } from 'lib/lemon-ui/LemonButton/More' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' @@ -22,6 +21,7 @@ import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductI import { router } from 'kea-router' import { ExperimentsHog } from 'lib/components/hedgehogs' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' +import { StatusTag } from './Experiment' export const scene: SceneExport = { component: Experiments, @@ -94,13 +94,7 @@ export function Experiments(): JSX.Element { title: 'Status', key: 'status', render: function Render(_, experiment: Experiment) { - const statusColors = { running: 'green', draft: 'default', complete: 'purple' } - const status = getExperimentStatus(experiment) - return ( - - {status.toUpperCase()} - - ) + return }, align: 'center', sorter: (a, b) => { diff --git a/frontend/src/scenes/experiments/MetricSelector.tsx b/frontend/src/scenes/experiments/MetricSelector.tsx index 7e199bbbeb9eb1..7ac3b5e84d8d92 100644 --- a/frontend/src/scenes/experiments/MetricSelector.tsx +++ b/frontend/src/scenes/experiments/MetricSelector.tsx @@ -16,7 +16,6 @@ import { Query } from '~/queries/Query/Query' import { FunnelsQuery, InsightQueryNode, TrendsQuery } from '~/queries/schema' import { AggregationSelect } from 'scenes/insights/filters/AggregationSelect' import { FunnelConversionWindowFilter } from 'scenes/insights/views/Funnels/FunnelConversionWindowFilter' -import { InfoCircleOutlined } from '@ant-design/icons' import './Experiment.scss' import { Tooltip } from 'lib/lemon-ui/Tooltip' @@ -24,6 +23,7 @@ import { Attribution } from 'scenes/insights/EditorFilters/AttributionFilter' import { TestAccountFilter } from '~/queries/nodes/InsightViz/filters/TestAccountFilter' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { DEFAULT_DURATION } from './experimentLogic' +import { IconInfo } from '@posthog/icons' export interface MetricSelectorProps { dashboardItemId: InsightShortId @@ -145,8 +145,8 @@ export function ExperimentInsightCreator({ insightProps }: { insightProps: Insig export function AttributionSelect({ insightProps }: EditorFilterProps): JSX.Element { return (
    - - Attribution type +
    + Attribution type @@ -163,9 +163,9 @@ export function AttributionSelect({ insightProps }: EditorFilterProps): JSX.Elem
    } > - + -
    +
    ) diff --git a/frontend/src/scenes/experiments/experimentsLogic.ts b/frontend/src/scenes/experiments/experimentsLogic.ts index 6a160c6a0a8c36..f76151f5e5b95d 100644 --- a/frontend/src/scenes/experiments/experimentsLogic.ts +++ b/frontend/src/scenes/experiments/experimentsLogic.ts @@ -10,6 +10,7 @@ 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' export function getExperimentStatus(experiment: Experiment): ProgressStatus { if (!experiment.start_date) { @@ -20,6 +21,17 @@ export function getExperimentStatus(experiment: Experiment): ProgressStatus { return ProgressStatus.Complete } +export function getExperimentStatusColor(status: ProgressStatus): LemonTagType { + switch (status) { + case ProgressStatus.Draft: + return 'default' + case ProgressStatus.Running: + return 'success' + case ProgressStatus.Complete: + return 'completion' + } +} + export const experimentsLogic = kea([ path(['scenes', 'experiments', 'experimentsLogic']), connect({ diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 4f12c9f925bedd..40b48cf07c94ab 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -3,12 +3,11 @@ import { Form, Group } from 'kea-forms' import { Row, Col, Radio, Popconfirm, Skeleton, Card } from 'antd' import { useActions, useValues } from 'kea' import { alphabet, capitalizeFirstLetter } from 'lib/utils' -import { LockOutlined } from '@ant-design/icons' import { featureFlagLogic } from './featureFlagLogic' import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' import { PageHeader } from 'lib/components/PageHeader' import './FeatureFlag.scss' -import { IconDelete, IconPlus, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { IconDelete, IconLock, IconPlus, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { SceneExport } from 'scenes/sceneTypes' import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets' @@ -156,6 +155,15 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { }) } + const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 + if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS] && hasMultipleProjects) { + tabs.push({ + label: 'Projects', + key: FeatureFlagsTab.PROJECTS, + content: , + }) + } + if (featureFlags[FEATURE_FLAGS.FF_DASHBOARD_TEMPLATES] && featureFlag.key && id) { tabs.push({ label: ( @@ -206,15 +214,6 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { }) } - const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 - if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS] && hasMultipleProjects) { - tabs.push({ - label: 'Projects', - key: FeatureFlagsTab.PROJECTS, - content: , - }) - } - return ( <>
    @@ -555,6 +554,8 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { ? "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 } > @@ -817,7 +818,7 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
    {!hasAvailableFeature(AvailableFeature.MULTIVARIATE_FLAGS) && ( - Variant key Description -
    +
    Payload Specify return payload when the variant key matches diff --git a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx index cdf57d7e06aaa2..d7410b7f20e84b 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx @@ -1,36 +1,63 @@ import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { LemonButton, LemonSelect } from '@posthog/lemon-ui' +import { LemonButton, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui' import { IconArrowRight, IconSync } from 'lib/lemon-ui/icons' import { useActions, useValues } from 'kea' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { featureFlagLogic } from './featureFlagLogic' import { organizationLogic } from '../organizationLogic' import { teamLogic } from 'scenes/teamLogic' +import { userLogic } from 'scenes/userLogic' import { useEffect } from 'react' const getColumns = (): LemonTableColumns> => { const { currentTeamId } = useValues(teamLogic) + const { currentOrganization } = useValues(organizationLogic) + const { updateCurrentTeam } = useActions(userLogic) return [ { title: 'Project', - dataIndex: 'project_name', - render: (dataValue, record) => - Number(record.project_id) === currentTeamId ? `${dataValue} (current)` : dataValue, + dataIndex: 'team_id', + render: (dataValue, record) => { + const team = currentOrganization?.teams?.find((t) => t.id === Number(dataValue)) + if (!team) { + return '(project does not exist)' + } + const linkText = team.id === currentTeamId ? `${team.name} (current)` : team.name + + return ( + { + updateCurrentTeam(team.id, `/feature_flags/${record.flag_id}`) + }} + > + {linkText} + + ) + }, }, { title: 'Flag status', dataIndex: 'active', render: (dataValue) => { - return dataValue ? 'active' : 'disabled' + return dataValue ? ( + + Enabled + + ) : ( + + Disabled + + ) }, }, ] } export default function FeatureFlagProjects(): JSX.Element { - const { featureFlag, copyDestinationProject, projectsWithCurrentFlag } = useValues(featureFlagLogic) - const { setCopyDestinationProject, loadProjectsWithCurrentFlag } = useActions(featureFlagLogic) + const { featureFlag, copyDestinationProject, projectsWithCurrentFlag, featureFlagCopyLoading } = + useValues(featureFlagLogic) + const { setCopyDestinationProject, loadProjectsWithCurrentFlag, copyFlag } = useActions(featureFlagLogic) const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) @@ -56,6 +83,7 @@ export default function FeatureFlagProjects(): JSX.Element {
    Destination project
    setCopyDestinationProject(id)} options={ @@ -68,14 +96,20 @@ export default function FeatureFlagProjects(): JSX.Element {
    - }> - Copy + } + onClick={() => copyFlag()} + className="w-28 max-w-28" + > + {projectsWithCurrentFlag.find((p) => Number(p.team_id) === copyDestinationProject) + ? 'Update' + : 'Copy'}
    - - By performing the copy, you may overwrite your existing Feature Flag configuration in another project. - } + // TODO: EarlyAccessFeatureType is not the correct type for featureFlag.features, hence bypassing TS check + const hasMatchingEarlyAccessFeature = featureFlag.features?.find((f: any) => f.flagKey === featureFlag.key) + return ( {index > 0 &&
    OR
    } @@ -365,6 +368,10 @@ export function FeatureFlagReleaseConditions({
    - View Early Access Feature + {hasMatchingEarlyAccessFeature ? 'View Early Access Feature' : 'No Early Access Feature'}
    diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index 3768568688784f..b021107a843791 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -216,7 +216,15 @@ export function OverViewTab({ callback: loadFeatureFlags, }) }} - disabled={!featureFlag.can_edit} + disabledReason={ + !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 + } fullWidth > Delete feature flag @@ -426,10 +434,7 @@ export function groupFilters( ) : (
    {rollout_percentage ?? 100}% of - +
    ) } else if (rollout_percentage !== null) { diff --git a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx index 5145689f520851..5fdf19a9a4cb62 100644 --- a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx +++ b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx @@ -6,11 +6,11 @@ import { InsightModel } from '~/types' import { featureFlagLogic } from './featureFlagLogic' export function RecentFeatureFlagInsights(): JSX.Element { - const { recentInsights, recentInsightsLoading, featureFlag } = useValues(featureFlagLogic) + const { relatedInsights, relatedInsightsLoading, featureFlag } = useValues(featureFlagLogic) return ( } /> ) diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index 77eec5c2ab461a..19544d46770432 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -110,6 +110,12 @@ export interface FeatureFlagLogicProps { id: number | 'new' | 'link' } +export type ProjectsWithCurrentFlagResponse = { + flag_id: number + team_id: number + active: boolean +}[] + // KLUDGE: Payloads are returned in a : mapping. // This doesn't work for forms because variant-keys can be updated too which would invalidate the dictionary entry. // If a multivariant flag is returned, the payload dictionary will be transformed to be : @@ -518,10 +524,10 @@ export const featureFlagLogic = kea([ } }, }, - recentInsights: [ + relatedInsights: [ [] as InsightModel[], { - loadRecentInsights: async () => { + loadRelatedInsights: async () => { if (props.id && props.id !== 'new' && values.featureFlag.key) { const response = await api.get( `api/projects/${values.currentTeamId}/insights/?feature_flag=${values.featureFlag.key}&order=-created_at` @@ -579,7 +585,34 @@ export const featureFlagLogic = kea([ projectsWithCurrentFlag: { __default: [] as Record[], loadProjectsWithCurrentFlag: async () => { - return [] + const orgId = values.currentOrganization?.id + const flagKey = values.featureFlag.key + + const projects = await api.organizationFeatureFlags.get(orgId, flagKey) + + // Put current project first + const currentProjectIdx = projects.findIndex((p) => p.team_id === values.currentTeamId) + if (currentProjectIdx) { + const [currentProject] = projects.splice(currentProjectIdx, 1) + const sortedProjects = [currentProject, ...projects] + return sortedProjects + } + return projects + }, + }, + featureFlagCopy: { + copyFlag: async () => { + const orgId = values.currentOrganization?.id + const featureFlagKey = values.featureFlag.key + const { copyDestinationProject, currentTeamId } = values + + if (currentTeamId && copyDestinationProject) { + return await api.organizationFeatureFlags.copy(orgId, { + feature_flag_key: featureFlagKey, + from_project: currentTeamId, + target_project_ids: [copyDestinationProject], + }) + } }, }, })), @@ -628,7 +661,7 @@ export const featureFlagLogic = kea([ } }, loadFeatureFlagSuccess: async () => { - actions.loadRecentInsights() + actions.loadRelatedInsights() actions.loadAllInsightsForFlag() }, loadInsightAtIndex: async ({ index, filters }) => { @@ -742,6 +775,21 @@ export const featureFlagLogic = kea([ featureFlagsLogic.findMounted()?.actions.updateFlag(updatedFlag) } }, + copyFlagSuccess: ({ featureFlagCopy }) => { + if (featureFlagCopy?.success.length) { + const operation = values.projectsWithCurrentFlag.find( + (p) => Number(p.team_id) === values.copyDestinationProject + ) + ? 'updated' + : 'copied' + lemonToast.success(`Feature flag ${operation} successfully!`) + } else { + lemonToast.error(`Error while saving feature flag: ${featureFlagCopy?.failed || featureFlagCopy}`) + } + + actions.loadProjectsWithCurrentFlag() + actions.setCopyDestinationProject(null) + }, })), selectors({ sentryErrorCount: [(s) => [s.sentryStats], (stats) => stats.total_count], @@ -962,7 +1010,7 @@ export const featureFlagLogic = kea([ if (foundFlag) { const formatPayloads = variantKeyToIndexFeatureFlagPayloads(foundFlag) actions.setFeatureFlag(formatPayloads) - actions.loadRecentInsights() + actions.loadRelatedInsights() actions.loadAllInsightsForFlag() } else if (props.id !== 'new') { actions.loadFeatureFlag() diff --git a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx index 56d20fa28d25de..43c0733e877e7d 100644 --- a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx +++ b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx @@ -2,7 +2,7 @@ import { actions, afterMount, connect, kea, key, path, props, reducers, selector import { loaders } from 'kea-loaders' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { rolesLogic } from 'scenes/organization/Settings/Permissions/Roles/rolesLogic' +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/funnels/FunnelBarGraph/MetricRow.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/MetricRow.tsx index c99395631187e6..7863e7770d3b3e 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/MetricRow.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/MetricRow.tsx @@ -1,6 +1,6 @@ export function MetricRow({ title, value }: { title: string; value: string | number }): JSX.Element { return ( -
    +
    {title}
    {value} diff --git a/frontend/src/scenes/groups/Groups.tsx b/frontend/src/scenes/groups/Groups.tsx index f6fc1d48a090c6..ec50e9950543ce 100644 --- a/frontend/src/scenes/groups/Groups.tsx +++ b/frontend/src/scenes/groups/Groups.tsx @@ -2,13 +2,11 @@ import { useActions, useValues } from 'kea' import { Group, PropertyDefinitionType } from '~/types' import { groupsListLogic } from './groupsListLogic' import { PropertiesTable } from 'lib/components/PropertiesTable' -import { PersonPageHeader } from 'scenes/persons/PersonPageHeader' 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 { SceneExport } from 'scenes/sceneTypes' import { GroupsIntroduction } from 'scenes/groups/GroupsIntroduction' import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' @@ -18,22 +16,14 @@ import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { capitalizeFirstLetter } from 'lib/utils' -export const scene: SceneExport = { - component: Groups, - logic: groupsListLogic, - paramsToProps: ({ params: { groupTypeIndex } }) => ({ - groupTypeIndex: parseInt(groupTypeIndex), - }), -} - -export function Groups({ groupTypeIndex }: { groupTypeIndex?: string } = {}): JSX.Element { +export function Groups({ groupTypeIndex }: { groupTypeIndex: number }): JSX.Element { const { groupTypeName: { singular, plural }, groups, groupsLoading, search, - } = useValues(groupsListLogic) - const { loadGroups, setSearch } = useActions(groupsListLogic) + } = useValues(groupsListLogic({ groupTypeIndex })) + const { loadGroups, setSearch } = useActions(groupsListLogic({ groupTypeIndex })) const { groupsAccessStatus } = useValues(groupsAccessLogic) if (groupTypeIndex === undefined) { @@ -47,7 +37,6 @@ export function Groups({ groupTypeIndex }: { groupTypeIndex?: string } = {}): JS ) { return ( <> - ) @@ -76,7 +65,6 @@ export function Groups({ groupTypeIndex }: { groupTypeIndex?: string } = {}): JS return ( <> - - ({ - label: capitalizeFirstLetter(aggregationLabel(groupType.group_type_index).plural), - key: groupType.group_type_index, - link: urls.groups(groupType.group_type_index), - } as LemonTab) - )), - ]} - /> - ) -} diff --git a/frontend/src/scenes/groups/RelatedGroups.tsx b/frontend/src/scenes/groups/RelatedGroups.tsx index da77d147a845ed..396ff4150be9f8 100644 --- a/frontend/src/scenes/groups/RelatedGroups.tsx +++ b/frontend/src/scenes/groups/RelatedGroups.tsx @@ -2,11 +2,11 @@ import { useValues } from 'kea' import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { ActorType } from '~/types' import { groupsModel } from '~/models/groupsModel' -import UserOutlined from '@ant-design/icons/lib/icons/UserOutlined' 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' interface Props { groupTypeIndex: number | null @@ -27,7 +27,7 @@ export function RelatedGroups({ groupTypeIndex, id }: Props): JSX.Element { } else { return ( <> - Person + Person ) } diff --git a/frontend/src/scenes/groups/groupsListLogic.ts b/frontend/src/scenes/groups/groupsListLogic.ts index 07a7d0c4b77b96..639aecd2ee007b 100644 --- a/frontend/src/scenes/groups/groupsListLogic.ts +++ b/frontend/src/scenes/groups/groupsListLogic.ts @@ -1,13 +1,11 @@ import { loaders } from 'kea-loaders' -import { kea, props, key, path, connect, actions, reducers, selectors, listeners, events } from 'kea' +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 { urls } from 'scenes/urls' import { Noun, groupsModel } from '~/models/groupsModel' -import { Breadcrumb, Group } from '~/types' +import { Group } from '~/types' import type { groupsListLogicType } from './groupsListLogicType' -import { capitalizeFirstLetter } from 'lib/utils' export interface GroupsPaginatedResponse { next: string | null @@ -69,15 +67,6 @@ export const groupsListLogic = kea([ (groupTypeIndex, aggregationLabel): Noun => groupTypeIndex === -1 ? { singular: 'person', plural: 'persons' } : aggregationLabel(groupTypeIndex), ], - breadcrumbs: [ - (s, p) => [s.groupTypeName, p.groupTypeIndex], - (groupTypeName, groupTypeIndex): Breadcrumb[] => [ - { - name: capitalizeFirstLetter(groupTypeName.plural), - path: urls.groups(groupTypeIndex), - }, - ], - ], }), listeners(({ actions }) => ({ setSearch: async ({ debounce }, breakpoint) => { @@ -87,9 +76,7 @@ export const groupsListLogic = kea([ actions.loadGroups() }, })), - events(({ actions }) => ({ - afterMount: () => { - actions.loadGroups() - }, - })), + afterMount(({ actions }) => { + actions.loadGroups() + }), ]) diff --git a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx index 43370a7d7f86c4..439390bd83164f 100644 --- a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx +++ b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx @@ -2,7 +2,7 @@ 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/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' export function IngestionInviteMembersButton(): JSX.Element { const { showInviteModal } = useActions(inviteLogic) diff --git a/frontend/src/scenes/ingestion/IngestionWizard.tsx b/frontend/src/scenes/ingestion/IngestionWizard.tsx index 9c5e22cb38794a..80c8d8c8690a2f 100644 --- a/frontend/src/scenes/ingestion/IngestionWizard.tsx +++ b/frontend/src/scenes/ingestion/IngestionWizard.tsx @@ -12,8 +12,8 @@ import { GeneratingDemoDataPanel } from './panels/GeneratingDemoDataPanel' import { ThirdPartyPanel } from './panels/ThirdPartyPanel' import { BillingPanel } from './panels/BillingPanel' import { Sidebar } from './Sidebar' -import { InviteModal } from 'scenes/organization/Settings/InviteModal' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +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' diff --git a/frontend/src/scenes/ingestion/ingestionLogic.ts b/frontend/src/scenes/ingestion/ingestionLogic.ts index 224949320e52bb..cf64522b8e27e5 100644 --- a/frontend/src/scenes/ingestion/ingestionLogic.ts +++ b/frontend/src/scenes/ingestion/ingestionLogic.ts @@ -11,7 +11,7 @@ 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/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import api from 'lib/api' import { loaders } from 'kea-loaders' import type { ingestionLogicType } from './ingestionLogicType' diff --git a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx index a8ded3f51bd97a..c6a30c5cf484f2 100644 --- a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx +++ b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx @@ -4,7 +4,7 @@ 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/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { DemoProjectButton } from './PanelComponents' diff --git a/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx b/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx index 2118f0202c060f..150222f6a00130 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsAdvanced.tsx @@ -12,6 +12,7 @@ import { IconSettings } from 'lib/lemon-ui/icons' 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)) @@ -104,7 +105,10 @@ export function PathsAdvanced({ insightProps, ...rest }: EditorFilterProps): JSX > Path Cleaning Rules - + Configure Project Rules diff --git a/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx b/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx index 187c1744951ded..6690f3e142e2f8 100644 --- a/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx +++ b/frontend/src/scenes/insights/EditorFilters/RetentionSummary.tsx @@ -1,5 +1,4 @@ import { useActions, useValues } from 'kea' -import { InfoCircleOutlined } from '@ant-design/icons' import { dateOptionPlurals, dateOptions, @@ -15,6 +14,7 @@ import { MathAvailability } from '../filters/ActionFilter/ActionFilterRow/Action 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' export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Element { const { showGroupsOptions } = useValues(groupsModel) @@ -63,7 +63,7 @@ export function RetentionSummary({ insightProps }: EditorFilterProps): JSX.Eleme <> {value} - + ), diff --git a/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx b/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx index a3b1878733b795..37200a5dc3baf9 100644 --- a/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/ValueOnSeriesFilter.tsx @@ -1,18 +1,20 @@ import { useActions, useValues } from 'kea' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { insightLogic } from '../insightLogic' -import { valueOnSeriesFilterLogic } from './valueOnSeriesFilterLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' export function ValueOnSeriesFilter(): JSX.Element { const { insightProps } = useValues(insightLogic) - const { valueOnSeries } = useValues(valueOnSeriesFilterLogic(insightProps)) - const { setValueOnSeries } = useActions(valueOnSeriesFilterLogic(insightProps)) + const { valueOnSeries } = useValues(insightVizDataLogic(insightProps)) + const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) return ( { + updateInsightFilter({ show_values_on_series: checked }) + }} label={Show values on series} size="small" /> diff --git a/frontend/src/scenes/insights/EditorFilters/valueOnSeriesFilterLogic.ts b/frontend/src/scenes/insights/EditorFilters/valueOnSeriesFilterLogic.ts deleted file mode 100644 index ce952907dad2ce..00000000000000 --- a/frontend/src/scenes/insights/EditorFilters/valueOnSeriesFilterLogic.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { actions, connect, kea, key, listeners, path, props, selectors } from 'kea' -import { ChartDisplayType, InsightLogicProps, TrendsFilterType } from '~/types' -import { insightVizDataLogic } from '../insightVizDataLogic' -import { keyForInsightLogicProps } from '../sharedUtils' - -import type { valueOnSeriesFilterLogicType } from './valueOnSeriesFilterLogicType' - -export const valueOnSeriesFilterLogic = kea([ - props({} as InsightLogicProps), - key(keyForInsightLogicProps('new')), - path((key) => ['scenes', 'insights', 'EditorFilters', 'valueOnSeriesFilterLogic', key]), - - connect((props: InsightLogicProps) => ({ - values: [insightVizDataLogic(props), ['isTrends', 'isStickiness', 'isLifecycle', 'insightFilter']], - actions: [insightVizDataLogic(props), ['updateInsightFilter']], - })), - - actions({ - setValueOnSeries: (checked: boolean) => ({ checked }), - }), - - selectors({ - valueOnSeries: [ - (s) => [s.isTrends, s.isStickiness, s.isLifecycle, s.insightFilter], - (isTrends, isStickiness, isLifecycle, insightFilter) => { - return !!( - ((isTrends || isStickiness || isLifecycle) && - (insightFilter as TrendsFilterType)?.show_values_on_series) || - // pie charts have value checked by default - (isTrends && - (insightFilter as TrendsFilterType)?.display === ChartDisplayType.ActionsPie && - (insightFilter as TrendsFilterType)?.show_values_on_series === undefined) - ) - }, - ], - }), - - listeners(({ actions }) => ({ - setValueOnSeries: ({ checked }) => { - actions.updateInsightFilter({ show_values_on_series: checked }) - }, - })), -]) diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx index 243ec146dd046d..ab3e7206c558f1 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx @@ -3,7 +3,7 @@ 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 insight from '../../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json' import { InsightShortId } from '~/types' import { createInsightStory } from 'scenes/insights/__mocks__/createInsightScene' import { App } from 'scenes/App' diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx index 603be6ed7422e1..747a093413c068 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx @@ -1,5 +1,6 @@ import { useActions, useValues } from 'kea' -import { PlusCircleOutlined, ThunderboltFilled, WarningOutlined } from '@ant-design/icons' +// eslint-disable-next-line no-restricted-imports +import { PlusCircleOutlined, ThunderboltFilled } from '@ant-design/icons' import { IconErrorOutline, IconInfo, IconOpenInNew, IconPlus } from 'lib/lemon-ui/icons' import { entityFilterLogic } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { Button, Empty } from 'antd' @@ -21,6 +22,7 @@ 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' export function InsightEmptyState({ heading = 'There are no matching events for this query', @@ -240,7 +242,7 @@ export function FunnelInvalidExclusionState(): JSX.Element {
    - +

    Invalid exclusion filters

    diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index b07540564eade7..500e4920031328 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -36,9 +36,9 @@ import { } from '~/queries/utils' import { examples, TotalEventsTable } from '~/queries/examples' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { filterTestAccountsDefaultsLogic } from 'scenes/project/Settings/filterTestAccountDefaultsLogic' import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { getDisplay, getShowPercentStackView, getShowValueOnSeries } from '~/queries/nodes/InsightViz/utils' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' export interface Tab { label: string | JSX.Element diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx index 6213bb6785eacb..e6ba4ea5b9f2fe 100644 --- a/frontend/src/scenes/insights/InsightPageHeader.tsx +++ b/frontend/src/scenes/insights/InsightPageHeader.tsx @@ -286,7 +286,12 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss index 3715a1a2706124..2dba11b4efb7eb 100644 --- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss +++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.scss @@ -59,7 +59,7 @@ font-weight: 600; } .tag-pill { - background-color: rgba(0, 0, 0, 0.08); + background-color: var(--border-3000); margin-right: 0; border: 0; color: var(--primary-alt); diff --git a/frontend/src/scenes/insights/Insights.stories.tsx b/frontend/src/scenes/insights/Insights.stories.tsx index cc48ef2becd138..e21a3342167e49 100644 --- a/frontend/src/scenes/insights/Insights.stories.tsx +++ b/frontend/src/scenes/insights/Insights.stories.tsx @@ -22,7 +22,16 @@ const meta: Meta = { '/api/projects/:team_id/persons/retention': sampleRetentionPeopleResponse, '/api/projects/:team_id/persons/properties': samplePersonProperties, '/api/projects/:team_id/groups_types': [], - '/api/projects/:team_id/notebooks?contains=query': {}, + '/api/projects/:team_id/notebooks': () => { + // this was matching on `?contains=query` but that made MSW unhappy and seems unnecessary + return [ + 200, + { + count: 0, + results: [], + }, + ] + }, }, post: { '/api/projects/:team_id/cohorts/': { id: 1 }, @@ -34,13 +43,27 @@ export default meta /* eslint-disable @typescript-eslint/no-var-requires */ // Trends export const TrendsLine: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsLine.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json') ) TrendsLine.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsLineEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsLine.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLine.json'), + 'edit' +) +TrendsLineEdit.parameters = { + testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, +} + +export const TrendsLineMulti: Story = createInsightStory( + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLineMulti.json') +) +TrendsLine.parameters = { + testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, +} +export const TrendsLineMultiEdit: Story = createInsightStory( + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLineMulti.json'), 'edit' ) TrendsLineEdit.parameters = { @@ -48,13 +71,13 @@ TrendsLineEdit.parameters = { } export const TrendsLineBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsLineBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLineBreakdown.json') ) TrendsLineBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsLineBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsLineBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsLineBreakdown.json'), 'edit' ) TrendsLineBreakdownEdit.parameters = { @@ -62,13 +85,13 @@ TrendsLineBreakdownEdit.parameters = { } export const TrendsBar: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsBar.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsBar.json') ) TrendsBar.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsBarEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsBar.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsBar.json'), 'edit' ) TrendsBarEdit.parameters = { @@ -76,13 +99,13 @@ TrendsBarEdit.parameters = { } export const TrendsBarBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsBarBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsBarBreakdown.json') ) TrendsBarBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsBarBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsBarBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsBarBreakdown.json'), 'edit' ) TrendsBarBreakdownEdit.parameters = { @@ -90,13 +113,13 @@ TrendsBarBreakdownEdit.parameters = { } export const TrendsValue: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsValue.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsValue.json') ) TrendsValue.parameters = { testOptions: { waitForSelector: '[data-attr=trend-bar-value-graph] > canvas' }, } export const TrendsValueEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsValue.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsValue.json'), 'edit' ) TrendsValueEdit.parameters = { @@ -104,13 +127,13 @@ TrendsValueEdit.parameters = { } export const TrendsValueBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsValueBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsValueBreakdown.json') ) TrendsValueBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-bar-value-graph] > canvas' }, } export const TrendsValueBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsValueBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsValueBreakdown.json'), 'edit' ) TrendsValueBreakdownEdit.parameters = { @@ -118,13 +141,13 @@ TrendsValueBreakdownEdit.parameters = { } export const TrendsArea: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsArea.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsArea.json') ) TrendsArea.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsAreaEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsArea.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsArea.json'), 'edit' ) TrendsAreaEdit.parameters = { @@ -132,13 +155,13 @@ TrendsAreaEdit.parameters = { } export const TrendsAreaBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsAreaBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsAreaBreakdown.json') ) TrendsAreaBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const TrendsAreaBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsAreaBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsAreaBreakdown.json'), 'edit' ) TrendsAreaBreakdownEdit.parameters = { @@ -146,31 +169,31 @@ TrendsAreaBreakdownEdit.parameters = { } export const TrendsNumber: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsNumber.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsNumber.json') ) TrendsNumber.parameters = { testOptions: { waitForSelector: '.BoldNumber__value' } } export const TrendsNumberEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsNumber.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsNumber.json'), 'edit' ) TrendsNumberEdit.parameters = { testOptions: { waitForSelector: '.BoldNumber__value' } } export const TrendsTable: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsTable.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsTable.json') ) TrendsTable.parameters = { testOptions: { waitForSelector: '[data-attr=insights-table-graph] td' } } export const TrendsTableEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsTable.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsTable.json'), 'edit' ) TrendsTableEdit.parameters = { testOptions: { waitForSelector: '[data-attr=insights-table-graph] td' } } export const TrendsTableBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsTableBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsTableBreakdown.json') ) TrendsTableBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=insights-table-graph] td' } } export const TrendsTableBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsTableBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsTableBreakdown.json'), 'edit' ) TrendsTableBreakdownEdit.parameters = { @@ -178,21 +201,21 @@ TrendsTableBreakdownEdit.parameters = { } export const TrendsPie: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsPie.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsPie.json') ) TrendsPie.parameters = { testOptions: { waitForSelector: '[data-attr=trend-pie-graph] > canvas' } } export const TrendsPieEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsPie.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsPie.json'), 'edit' ) TrendsPieEdit.parameters = { testOptions: { waitForSelector: '[data-attr=trend-pie-graph] > canvas' } } export const TrendsPieBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsPieBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsPieBreakdown.json') ) TrendsPieBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-pie-graph] > canvas' } } export const TrendsPieBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsPieBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsPieBreakdown.json'), 'edit' ) TrendsPieBreakdownEdit.parameters = { @@ -200,11 +223,11 @@ TrendsPieBreakdownEdit.parameters = { } export const TrendsWorldMap: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsWorldMap.json') + require('../../mocks/fixtures/api/projects/team_id/insights/trendsWorldMap.json') ) TrendsWorldMap.parameters = { testOptions: { waitForSelector: '.WorldMap' } } export const TrendsWorldMapEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/trendsWorldMap.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/trendsWorldMap.json'), 'edit' ) TrendsWorldMapEdit.parameters = { testOptions: { waitForSelector: '.WorldMap' } } @@ -212,11 +235,11 @@ TrendsWorldMapEdit.parameters = { testOptions: { waitForSelector: '.WorldMap' } // Funnels export const FunnelLeftToRight: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRight.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRight.json') ) FunnelLeftToRight.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-bar-graph] .StepBar' } } export const FunnelLeftToRightEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRight.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRight.json'), 'edit' ) FunnelLeftToRightEdit.parameters = { @@ -224,13 +247,13 @@ FunnelLeftToRightEdit.parameters = { } export const FunnelLeftToRightBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRightBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRightBreakdown.json') ) FunnelLeftToRightBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-bar-graph] .StepBar' }, } export const FunnelLeftToRightBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelLeftToRightBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelLeftToRightBreakdown.json'), 'edit' ) FunnelLeftToRightBreakdownEdit.parameters = { @@ -238,13 +261,13 @@ FunnelLeftToRightBreakdownEdit.parameters = { } export const FunnelTopToBottom: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottom.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottom.json') ) FunnelTopToBottom.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-bar-graph] .funnel-bar' }, } export const FunnelTopToBottomEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottom.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottom.json'), 'edit' ) FunnelTopToBottomEdit.parameters = { @@ -252,13 +275,13 @@ FunnelTopToBottomEdit.parameters = { } export const FunnelTopToBottomBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottomBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottomBreakdown.json') ) FunnelTopToBottomBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-bar-graph] .funnel-bar' }, } export const FunnelTopToBottomBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTopToBottomBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTopToBottomBreakdown.json'), 'edit' ) FunnelTopToBottomBreakdownEdit.parameters = { @@ -266,13 +289,13 @@ FunnelTopToBottomBreakdownEdit.parameters = { } export const FunnelHistoricalTrends: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelHistoricalTrends.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelHistoricalTrends.json') ) FunnelHistoricalTrends.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph-funnel] > canvas' }, } export const FunnelHistoricalTrendsEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelHistoricalTrends.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelHistoricalTrends.json'), 'edit' ) FunnelHistoricalTrendsEdit.parameters = { @@ -280,11 +303,11 @@ FunnelHistoricalTrendsEdit.parameters = { } export const FunnelTimeToConvert: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTimeToConvert.json') + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTimeToConvert.json') ) FunnelTimeToConvert.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-histogram] svg' } } export const FunnelTimeToConvertEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/funnelTimeToConvert.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/funnelTimeToConvert.json'), 'edit' ) FunnelTimeToConvertEdit.parameters = { testOptions: { waitForSelector: '[data-attr=funnel-histogram] svg' } } @@ -292,13 +315,13 @@ FunnelTimeToConvertEdit.parameters = { testOptions: { waitForSelector: '[data-at // Retention export const Retention: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/retention.json') + require('../../mocks/fixtures/api/projects/team_id/insights/retention.json') ) Retention.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const RetentionEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/retention.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/retention.json'), 'edit' ) RetentionEdit.parameters = { @@ -306,13 +329,13 @@ RetentionEdit.parameters = { } export const RetentionBreakdown: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/retentionBreakdown.json') + require('../../mocks/fixtures/api/projects/team_id/insights/retentionBreakdown.json') ) RetentionBreakdown.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const RetentionBreakdownEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/retentionBreakdown.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/retentionBreakdown.json'), 'edit' ) RetentionBreakdownEdit.parameters = { @@ -322,13 +345,13 @@ RetentionBreakdownEdit.parameters = { // Lifecycle export const Lifecycle: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/lifecycle.json') + require('../../mocks/fixtures/api/projects/team_id/insights/lifecycle.json') ) Lifecycle.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const LifecycleEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/lifecycle.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/lifecycle.json'), 'edit' ) LifecycleEdit.parameters = { @@ -338,13 +361,13 @@ LifecycleEdit.parameters = { // Stickiness export const Stickiness: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/stickiness.json') + require('../../mocks/fixtures/api/projects/team_id/insights/stickiness.json') ) Stickiness.parameters = { testOptions: { waitForSelector: '[data-attr=trend-line-graph] > canvas' }, } export const StickinessEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/stickiness.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/stickiness.json'), 'edit' ) StickinessEdit.parameters = { @@ -354,11 +377,11 @@ StickinessEdit.parameters = { // User Paths export const UserPaths: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/userPaths.json') + require('../../mocks/fixtures/api/projects/team_id/insights/userPaths.json') ) UserPaths.parameters = { testOptions: { waitForSelector: '[data-attr=paths-viz] > svg' } } export const UserPathsEdit: Story = createInsightStory( - require('../../mocks/fixtures/api/projects/:team_id/insights/userPaths.json'), + require('../../mocks/fixtures/api/projects/team_id/insights/userPaths.json'), 'edit' ) UserPathsEdit.parameters = { testOptions: { waitForSelector: '[data-attr=paths-viz] > svg' } } diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 921f18b586f223..80b792f3e4dd26 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -321,6 +321,7 @@ export function ActionFilterRow({ className={'ActionFilterRow'} ref={setNodeRef} {...attributes} + // eslint-disable-next-line react/forbid-dom-props style={{ position: 'relative', zIndex: isDragging ? 1 : undefined, @@ -458,7 +459,6 @@ export function ActionFilterRow({ pageKey={`${index}-${value}-${typeKey}-filter`} propertyFilters={filter.properties} onChange={(properties) => updateFilterProperty({ properties, index })} - style={{ margin: 0 }} showNestedArrow={showNestedArrow} disablePopover={!propertyFiltersPopover} taxonomicGroupTypes={propertiesTaxonomicGroupTypes} diff --git a/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx b/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx index 1412a7d7a0e3dc..f54d98df2e1261 100644 --- a/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx +++ b/frontend/src/scenes/insights/filters/InsightDateFilter/InsightDateFilter.tsx @@ -1,8 +1,8 @@ import { useValues, useActions } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { insightLogic } from 'scenes/insights/insightLogic' -import { CalendarOutlined, InfoCircleOutlined } from '@ant-design/icons' import { Tooltip } from 'antd' +import { IconCalendar, IconInfo } from '@posthog/icons' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' type InsightDateFilterProps = { @@ -17,17 +17,17 @@ export function InsightDateFilter({ disabled }: InsightDateFilterProps): JSX.Ele return ( { updateDateRange({ date_from, date_to }) }} makeLabel={(key) => ( <> - {key} + {key} {key == 'All time' && ( - + )} diff --git a/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts b/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts new file mode 100644 index 00000000000000..76c8b4da0321ce --- /dev/null +++ b/frontend/src/scenes/insights/filters/InsightDateFilter/insightDateFilterLogic.ts @@ -0,0 +1,37 @@ +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 { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + +export const insightDateFilterLogic = kea([ + props({} as InsightLogicProps), + key(keyForInsightLogicProps('new')), + path((key) => ['scenes', 'insights', 'InsightDateFilter', 'insightDateFilterLogic', key]), + connect((props: InsightLogicProps) => ({ + actions: [insightVizDataLogic(props), ['updateQuerySource']], + values: [insightVizDataLogic(props), ['dateRange']], + })), + actions(() => ({ + setDates: (dateFrom: string | undefined | null, dateTo: string | undefined | null) => ({ + dateFrom, + dateTo, + }), + })), + selectors({ + dates: [ + (s) => [s.dateRange], + (dateRange) => ({ dateFrom: dateRange?.date_from || null, dateTo: dateRange?.date_to || null }), + ], + }), + listeners(({ actions }) => ({ + setDates: ({ dateFrom, dateTo }) => { + actions.updateQuerySource({ + dateRange: { + date_from: dateFrom || null, + date_to: dateTo || null, + }, + }) + }, + })), +]) diff --git a/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx b/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx index be670f9abc8c5d..a5f9d72f2b649d 100644 --- a/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx +++ b/frontend/src/scenes/insights/filters/RetentionReferencePicker.tsx @@ -1,4 +1,5 @@ import { Select } from 'antd' +// eslint-disable-next-line no-restricted-imports import { PercentageOutlined } from '@ant-design/icons' import { insightLogic } from 'scenes/insights/insightLogic' import { useActions, useValues } from 'kea' diff --git a/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx b/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx index 715e5f59932aca..19eeb45ef73475 100644 --- a/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx +++ b/frontend/src/scenes/insights/filters/TestAccountFilter/TestAccountFilter.tsx @@ -4,7 +4,8 @@ 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 { filterTestAccountsDefaultsLogic } from 'scenes/project/Settings/filterTestAccountDefaultsLogic' +import { urls } from 'scenes/urls' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' export function TestAccountFilter({ filters, @@ -33,7 +34,7 @@ export function TestAccountFilter({ Filter out internal and test users } - to="/project/settings#internal-users-filtering" + to={urls.settings('project-product-analytics', 'internal-user-filtering')} status="stealth" size="small" noPadding diff --git a/frontend/src/scenes/insights/insightCommandLogic.ts b/frontend/src/scenes/insights/insightCommandLogic.ts index 7a46c062c51d28..c94449d881249e 100644 --- a/frontend/src/scenes/insights/insightCommandLogic.ts +++ b/frontend/src/scenes/insights/insightCommandLogic.ts @@ -2,11 +2,11 @@ import { Command, commandPaletteLogic } from 'lib/components/CommandPalette/comm import { kea, props, key, path, connect, events } from 'kea' import type { insightCommandLogicType } from './insightCommandLogicType' import { compareFilterLogic } from 'lib/components/CompareFilter/compareFilterLogic' -import { RiseOutlined } from '@ant-design/icons' import { dateMapping } from 'lib/utils' import { InsightLogicProps } from '~/types' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { insightVizDataLogic } from './insightVizDataLogic' +import { IconTrendingUp } from 'lib/lemon-ui/icons' const INSIGHT_COMMAND_SCOPE = 'insights' @@ -23,14 +23,14 @@ export const insightCommandLogic = kea([ key: 'insight-graph', resolver: [ { - icon: RiseOutlined, + icon: IconTrendingUp, display: 'Toggle "Compare Previous" on Graph', executor: () => { compareFilterLogic(props).actions.toggleCompare() }, }, ...dateMapping.map(({ key, values }) => ({ - icon: RiseOutlined, + icon: IconTrendingUp, display: `Set Time Range to ${key}`, executor: () => { insightVizDataLogic(props).actions.updateDateRange({ diff --git a/frontend/src/scenes/insights/insightDataLogic.ts b/frontend/src/scenes/insights/insightDataLogic.ts index e2ac23f175ed2e..89e2dec570f328 100644 --- a/frontend/src/scenes/insights/insightDataLogic.ts +++ b/frontend/src/scenes/insights/insightDataLogic.ts @@ -15,9 +15,9 @@ import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' import { queryExportContext } from '~/queries/query' import { objectsEqual } from 'lib/utils' import { compareFilters } from './utils/compareFilters' -import { filterTestAccountsDefaultsLogic } from 'scenes/project/Settings/filterTestAccountDefaultsLogic' 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/insightLogic.ts b/frontend/src/scenes/insights/insightLogic.ts index a38becc02327b6..825d1a1237265a 100644 --- a/frontend/src/scenes/insights/insightLogic.ts +++ b/frontend/src/scenes/insights/insightLogic.ts @@ -51,6 +51,7 @@ import { isInsightVizNode } from '~/queries/utils' import { userLogic } from 'scenes/userLogic' import { transformLegacyHiddenLegendKeys } from 'scenes/funnels/funnelUtils' import { summarizeInsight } from 'scenes/insights/summarizeInsight' +import { InsightVizNode } from '~/queries/schema' const IS_TEST_MODE = process.env.NODE_ENV === 'test' export const UNSAVED_INSIGHT_MIN_REFRESH_INTERVAL_MINUTES = 3 @@ -540,6 +541,7 @@ export const insightLogic = kea([ ) }, ], + showPersonsModal: [() => [(_, p) => p.query], (query?: InsightVizNode) => !query || !query.hidePersonsModal], }), listeners(({ actions, selectors, values }) => ({ setFiltersMerge: ({ filters }) => { diff --git a/frontend/src/scenes/insights/insightSceneLogic.tsx b/frontend/src/scenes/insights/insightSceneLogic.tsx index 4df5586a291bc4..3ff71b3e53720a 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.tsx +++ b/frontend/src/scenes/insights/insightSceneLogic.tsx @@ -13,12 +13,13 @@ import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { teamLogic } from 'scenes/teamLogic' import { insightDataLogic } from './insightDataLogic' import { insightDataLogicType } from './insightDataLogicType' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' export const insightSceneLogic = kea([ path(['scenes', 'insights', 'insightSceneLogic']), connect({ logic: [eventUsageLogic], - values: [teamLogic, ['currentTeam'], sceneLogic, ['activeScene']], + values: [teamLogic, ['currentTeam'], sceneLogic, ['activeScene'], preflightLogic, ['isDev']], }), actions({ setInsightId: (insightId: InsightShortId) => ({ insightId }), @@ -236,6 +237,10 @@ export const insightSceneLogic = kea([ return false } + if (values.isDev) { + return false + } + return ( values.insightMode === ItemMode.Edit && (!!values.insightLogicRef?.logic.values.insightChanged || diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index e56d57def84a30..d732592eb2be20 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -1,6 +1,6 @@ import posthog from 'posthog-js' import { actions, connect, kea, key, listeners, path, props, selectors, reducers } from 'kea' -import { BaseMathType, ChartDisplayType, InsightLogicProps, IntervalType } from '~/types' +import { BaseMathType, ChartDisplayType, InsightLogicProps, IntervalType, TrendsFilterType } from '~/types' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { BreakdownFilter, @@ -50,7 +50,7 @@ import { sceneLogic } from 'scenes/sceneLogic' import type { insightVizDataLogicType } from './insightVizDataLogicType' import { parseProperties } from 'lib/components/PropertyFilters/utils' -import { filterTestAccountsDefaultsLogic } from 'scenes/project/Settings/filterTestAccountDefaultsLogic' +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' @@ -171,7 +171,11 @@ export const insightVizDataLogic = kea([ ) }, ], - + shouldShowSessionAnalysisWarning: [ + (s) => [s.isUsingSessionAnalysis, s.query], + (isUsingSessionAnalysis, query) => + isUsingSessionAnalysis && !(isInsightVizNode(query) && query.suppressSessionAnalysisWarning), + ], isNonTimeSeriesDisplay: [ (s) => [s.display], (display) => !!display && NON_TIME_SERIES_DISPLAY_TYPES.includes(display), @@ -184,6 +188,20 @@ export const insightVizDataLogic = kea([ }, ], + valueOnSeries: [ + (s) => [s.isTrends, s.isStickiness, s.isLifecycle, s.insightFilter], + (isTrends, isStickiness, isLifecycle, insightFilter): boolean => { + return !!( + ((isTrends || isStickiness || isLifecycle) && + (insightFilter as TrendsFilterType)?.show_values_on_series) || + // pie charts have value checked by default + (isTrends && + (insightFilter as TrendsFilterType)?.display === ChartDisplayType.ActionsPie && + (insightFilter as TrendsFilterType)?.show_values_on_series === undefined) + ) + }, + ], + hasLegend: [ (s) => [s.isTrends, s.isStickiness, s.display], (isTrends, isStickiness, display) => diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx index f9411cfddd8b23..a3971c891a7ded 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx @@ -2,11 +2,11 @@ import { useActions, useValues } from 'kea' import { BIN_COUNT_AUTO } from 'lib/constants' import { InputNumber, Select } from 'antd' import { BinCountValue } from '~/types' -import { BarChartOutlined } from '@ant-design/icons' import clsx from 'clsx' 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' // Constraints as defined in funnel_time_to_convert.py:34 const MIN = 1 @@ -99,7 +99,7 @@ export function FunnelBinsPicker({ disabled }: FunnelBinsPickerProps): JSX.Eleme value={option.value} label={ <> - {option.label} + {option.label} } > diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx index 68fd13590e4fe7..59deda68014805 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx @@ -1,4 +1,3 @@ -import { InfoCircleOutlined } from '@ant-design/icons' import { capitalizeFirstLetter, pluralize } from 'lib/utils' import { useState } from 'react' import { EditorFilterProps, FunnelConversionWindow, FunnelConversionWindowTimeUnit } from '~/types' @@ -8,6 +7,7 @@ import { LemonInput, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { FunnelsFilter } from '~/queries/schema' +import { IconInfo } from '@posthog/icons' const TIME_INTERVAL_BOUNDS: Record = { [FunnelConversionWindowTimeUnit.Second]: [1, 3600], @@ -50,8 +50,8 @@ export function FunnelConversionWindowFilter({ insightProps }: Pick - - Conversion window limit{' '} + + Conversion window limit @@ -63,7 +63,7 @@ export function FunnelConversionWindowFilter({ insightProps }: Pick } > - +

    diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx index 59d499953c1648..d2838054e3e803 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationFeedbackForm.tsx @@ -5,8 +5,7 @@ import { insightLogic } from 'scenes/insights/insightLogic' import { funnelCorrelationFeedbackLogic } from 'scenes/funnels/funnelCorrelationFeedbackLogic' import { LemonButton, LemonTextArea } from '@posthog/lemon-ui' -import { IconClose } from 'lib/lemon-ui/icons' -import { CommentOutlined } from '@ant-design/icons' +import { IconClose, IconComment } from 'lib/lemon-ui/icons' export const FunnelCorrelationFeedbackForm = (): JSX.Element | null => { const { insightProps } = useValues(insightLogic) @@ -30,7 +29,7 @@ export const FunnelCorrelationFeedbackForm = (): JSX.Element | null => {

    - + Was this correlation analysis report useful?

    diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx index 3b62284e9ec2cd..587c33aefa5081 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationSkewWarning.tsx @@ -4,6 +4,7 @@ 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 { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx index eda03e199dd28e..1c3a0222493235 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelCorrelationTable.tsx @@ -2,8 +2,7 @@ import { useEffect } from 'react' import { ConfigProvider, Table, Empty } from 'antd' import Column from 'antd/lib/table/Column' import { useActions, useValues } from 'kea' -import { RiseOutlined, FallOutlined, InfoCircleOutlined } from '@ant-design/icons' -import { IconSelectEvents, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +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' @@ -24,6 +23,7 @@ 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' export function FunnelCorrelationTable(): JSX.Element | null { const { insightProps } = useValues(insightLogic) @@ -85,7 +85,11 @@ export function FunnelCorrelationTable(): JSX.Element | null { return ( <>

    - {is_success ? : }{' '} + {is_success ? ( + + ) : ( + + )}{' '} {second_value !== undefined && ( <> @@ -321,7 +325,7 @@ export function FunnelCorrelationTable(): JSX.Element | null { querySource?.aggregation_group_type_index != undefined ? 'that' : 'who' } performed the event and completed the entire funnel.`} > - +

    } @@ -345,7 +349,7 @@ export function FunnelCorrelationTable(): JSX.Element | null { } > - +
    } diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx index a06bac784f4258..3325b103ba438f 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelPropertyCorrelationTable.tsx @@ -3,12 +3,11 @@ import { Link } from 'lib/lemon-ui/Link' import { Col, ConfigProvider, Row, Table, Empty } from 'antd' import Column from 'antd/lib/table/Column' import { useActions, useValues } from 'kea' -import { RiseOutlined, FallOutlined, InfoCircleOutlined } from '@ant-design/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 { IconSelectProperties } from 'lib/lemon-ui/icons' +import { IconSelectProperties, IconTrendingDown, IconTrendingUp } from 'lib/lemon-ui/icons' import './FunnelCorrelationTable.scss' import { VisibilitySensor } from 'lib/components/VisibilitySensor/VisibilitySensor' import { Tooltip } from 'lib/lemon-ui/Tooltip' @@ -23,6 +22,7 @@ import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/Person import { useState } from 'react' import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' import { funnelPersonsModalLogic } from 'scenes/funnels/funnelPersonsModalLogic' +import { IconInfo } from '@posthog/icons' export function FunnelPropertyCorrelationTable(): JSX.Element | null { const { insightProps } = useValues(insightLogic) @@ -103,9 +103,9 @@ export function FunnelPropertyCorrelationTable(): JSX.Element | null { <>

    {is_success ? ( - + ) : ( - + )}{' '} {second_value !== undefined && ( @@ -264,7 +264,7 @@ export function FunnelPropertyCorrelationTable(): JSX.Element | null { querySource?.aggregation_group_type_index != undefined ? 'that' : 'who' } have this property and completed the entire funnel.`} > - +

    } @@ -288,7 +288,7 @@ export function FunnelPropertyCorrelationTable(): JSX.Element | null { } > - +
    } diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx index b53ba5ac1b304c..d66db5a0855f95 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx @@ -1,11 +1,13 @@ import { useActions, useValues } from 'kea' -import { ClockCircleOutlined, LineChartOutlined, FunnelPlotOutlined } from '@ant-design/icons' +// eslint-disable-next-line no-restricted-imports +import { ClockCircleOutlined, LineChartOutlined } from '@ant-design/icons' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { FunnelVizType as VizType, EditorFilterProps } from '~/types' import { DropdownSelector } from 'lib/components/DropdownSelector/DropdownSelector' import { FunnelsFilter } from '~/queries/schema' +import { IconFunnels } from '@posthog/icons' export function FunnelVizType({ insightProps }: Pick): JSX.Element | null { const { aggregationTargetLabel } = useValues(funnelDataLogic(insightProps)) @@ -19,7 +21,7 @@ export function FunnelVizType({ insightProps }: Pick, + icon: , }, { key: VizType.TimeToConvert, diff --git a/frontend/src/scenes/insights/views/Histogram/Histogram.tsx b/frontend/src/scenes/insights/views/Histogram/Histogram.tsx index d3abb1548470c0..11671e289ecaee 100644 --- a/frontend/src/scenes/insights/views/Histogram/Histogram.tsx +++ b/frontend/src/scenes/insights/views/Histogram/Histogram.tsx @@ -260,5 +260,6 @@ export function Histogram({ /* minWidth required to enforce d3's width calculations on the div wrapping the svg so that scrolling horizontally works */ + // eslint-disable-next-line react/forbid-dom-props return
    } diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx index 6bc37549ef62e1..5b3283417f019b 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.stories.tsx @@ -25,7 +25,7 @@ const Template: StoryFn = (props, { parameters }) => { const [dashboardItemId] = useState(() => `InsightTableStory.${uniqueNode++}`) // eslint-disable-next-line @typescript-eslint/no-var-requires - const insight = require('../../../../mocks/fixtures/api/projects/:team_id/insights/trendsLineBreakdown.json') + const insight = require('../../../../mocks/fixtures/api/projects/team_id/insights/trendsLineBreakdown.json') const filters = { ...insight.filters, ...parameters.mergeFilters } const cachedInsight = { ...insight, short_id: dashboardItemId, filters } diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx index 901839a9601a26..ba2ec9f5393299 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/columns/AggregationColumn.tsx @@ -1,5 +1,6 @@ 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' diff --git a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx index b5a5d6e39aca81..51576c2496ec96 100644 --- a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx +++ b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx @@ -733,7 +733,7 @@ export function LineGraph_({ }) setMyLineChart(newChart) return () => newChart.destroy() - }, [datasets, hiddenLegendKeys, isDarkModeOn]) + }, [datasets, hiddenLegendKeys, isDarkModeOn, trendsFilter, formula, showValueOnSeries, showPercentStackView]) return (
    void hideTooltip: () => void updateTooltipCoordinates: (x: number, y: number) => void + worldMapCountryProps?: ( + countryCode: string, + countrySeries: TrendResult | undefined + ) => Omit, 'key'> } const WorldMapSVG = React.memo( @@ -116,6 +120,7 @@ const WorldMapSVG = React.memo( showTooltip, hideTooltip, updateTooltipCoordinates, + worldMapCountryProps, }, ref ) => { @@ -139,15 +144,23 @@ const WorldMapSVG = React.memo( const fill = aggregatedValue ? gradateColor(BRAND_BLUE_HSL, aggregatedValue / maxAggregatedValue, SATURATION_FLOOR) : undefined - return React.cloneElement(countryElement, { - key: countryCode, - style: { color: fill, cursor: showPersonsModal && countrySeries ? 'pointer' : undefined }, - onMouseEnter: () => showTooltip(countryCode, countrySeries || null), - onMouseLeave: () => hideTooltip(), - onMouseMove: (e: MouseEvent) => { - updateTooltipCoordinates(e.clientX, e.clientY) - }, - onClick: () => { + + const { + onClick: propsOnClick, + style, + ...props + } = worldMapCountryProps + ? worldMapCountryProps(countryCode, countrySeries) + : { onClick: undefined, style: undefined } + + let onClick: typeof propsOnClick + if (propsOnClick) { + onClick = (e) => { + propsOnClick(e) + hideTooltip() + } + } else if (showPersonsModal && countrySeries) { + onClick = () => { if (showPersonsModal && countrySeries) { if (countrySeries.persons?.url) { openPersonsModal({ @@ -167,7 +180,19 @@ const WorldMapSVG = React.memo( }) } } + } + } + + return React.cloneElement(countryElement, { + key: countryCode, + style: { color: fill, cursor: onClick ? 'pointer' : undefined, ...style }, + onMouseEnter: () => showTooltip(countryCode, countrySeries || null), + onMouseLeave: () => hideTooltip(), + onMouseMove: (e: MouseEvent) => { + updateTooltipCoordinates(e.clientX, e.clientY) }, + onClick, + ...props, }) })} @@ -176,10 +201,11 @@ const WorldMapSVG = React.memo( ) ) -export function WorldMap({ showPersonsModal = true }: ChartParams): JSX.Element { +export function WorldMap({ showPersonsModal = true, context }: ChartParams): JSX.Element { const { insightProps } = useValues(insightLogic) const { countryCodeToSeries, maxAggregatedValue } = useValues(worldMapLogic(insightProps)) const { showTooltip, hideTooltip, updateTooltipCoordinates } = useActions(worldMapLogic(insightProps)) + const renderingMetadata = context?.chartRenderingMetadata?.[ChartDisplayType.WorldMap] const svgRef = useWorldMapTooltip(showPersonsModal) @@ -192,6 +218,7 @@ export function WorldMap({ showPersonsModal = true }: ChartParams): JSX.Element hideTooltip={hideTooltip} updateTooltipCoordinates={updateTooltipCoordinates} ref={svgRef} + worldMapCountryProps={renderingMetadata?.countryProps} /> ) } diff --git a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx index 6c3fbb8b995437..dc1be09924ebc0 100644 --- a/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx +++ b/frontend/src/scenes/instance/AsyncMigrations/AsyncMigrations.tsx @@ -3,7 +3,6 @@ import { PageHeader } from 'lib/components/PageHeader' import { SceneExport } from 'scenes/sceneTypes' import { Button, Progress } from 'antd' import { useActions, useValues } from 'kea' -import { PlayCircleOutlined } from '@ant-design/icons' import { AsyncMigration, migrationStatusNumberToMessage, @@ -21,7 +20,7 @@ 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 { IconRefresh, IconReplay } from 'lib/lemon-ui/icons' +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' @@ -153,7 +152,7 @@ export function AsyncMigrations(): JSX.Element { + ) } diff --git a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx index f60b11353b7e8e..0372b12afd83bc 100644 --- a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx +++ b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx @@ -1,5 +1,4 @@ import { useMemo } from 'react' -import { PlayCircleOutlined, CheckOutlined, CloseOutlined, SettingOutlined } from '@ant-design/icons' import { Tooltip, Radio, InputNumber } from 'antd' import { ChildFunctionProps, Form } from 'kea-forms' import { Field } from 'lib/forms/Field' @@ -15,6 +14,8 @@ import { dayjs } from 'lib/dayjs' 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' // keep in sync with plugin-server's export-historical-events.ts export const HISTORICAL_EXPORT_JOB_NAME = 'Export historical events' @@ -38,11 +39,11 @@ export function PluginJobConfiguration(props: InterfaceJobsProps): JSX.Element { playButtonOnClick(jobHasEmptyPayload)}> {jobHasEmptyPayload ? ( - ) : ( - )} @@ -125,10 +126,10 @@ function FieldInput({ onChange={(e) => onChange(e.target.value)} > - True + True - False + False ) diff --git a/frontend/src/scenes/plugins/plugin/PluginImage.tsx b/frontend/src/scenes/plugins/plugin/PluginImage.tsx index 67120a11074d89..9fec8b6275e9e5 100644 --- a/frontend/src/scenes/plugins/plugin/PluginImage.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginImage.tsx @@ -42,6 +42,7 @@ export function PluginImage({ ) : (
    {type} + return {type} } const columns: LemonTableColumns> = [ diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index 0b2ba5fd675b11..0d016a06a7ba1f 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -80,7 +80,7 @@ export function PluginSource({ title={pluginSourceLoading ? 'Loading...' : `Edit App: ${name}`} placement={placement ?? 'left'} footer={ -
    +
    @@ -126,7 +126,7 @@ export function PluginSource({ }} /> {!value && createDefaultPluginSource(name)[currentFile] ? ( -
    +
    diff --git a/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx index 6324af5a536b87..4d0bc00644da67 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx @@ -1,6 +1,6 @@ import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { IconCheckmark, IconCloudDownload } from 'lib/lemon-ui/icons' +import { IconCheckmark, IconCloudDownload, IconDelete, IconReplay, IconWeb } from 'lib/lemon-ui/icons' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { PluginTypeWithConfig, PluginRepositoryEntry, PluginInstallationType } from 'scenes/plugins/types' @@ -8,7 +8,6 @@ import { PluginType } from '~/types' import { PluginTags } from './components' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { Popconfirm } from 'antd' -import { DeleteOutlined, GlobalOutlined, RollbackOutlined } from '@ant-design/icons' import { canGloballyManagePlugins } from 'scenes/plugins/access' import { userLogic } from 'scenes/userLogic' @@ -71,7 +70,7 @@ export function AppManagementView({ type="primary" status="danger" size="small" - icon={} + icon={} disabledReason={ unusedPlugins.includes(plugin.id) ? undefined : 'This app is still in use.' } @@ -93,7 +92,7 @@ export function AppManagementView({ } + icon={} onClick={() => patchPlugin(plugin.id, { is_global: false })} > Make local @@ -111,7 +110,7 @@ export function AppManagementView({ } + icon={} onClick={() => patchPlugin(plugin.id, { is_global: true })} > Make global diff --git a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx index 1e5e0ad81b897e..382a157bbbf6f9 100644 --- a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx @@ -16,6 +16,7 @@ const MinimalAppView = ({ plugin, order }: { plugin: PluginTypeWithConfig; order
    -

    - Access Control -

    -

    - {projectPermissioningEnabled ? ( - <> - This project is{' '} - - - private - - . Only members listed below are allowed to access it. - - ) : ( - <> - This project is{' '} - - - open - - . Any member of the organization can access it. To enable granular access control, make it - private. - - )} -

    - { - guardAvailableFeature( - AvailableFeature.PROJECT_BASED_PERMISSIONING, - 'project-based permissioning', - 'Set permissions granularly for each project. Make sure only the right people have access to protected data.', - () => updateCurrentTeam({ access_control: checked }) - ) - }} - checked={!!projectPermissioningEnabled} - disabled={ - isRestricted || - !currentOrganization || - !currentTeam || - currentOrganizationLoading || - currentTeamLoading - } - bordered - label="Make project private" - /> -
    - ) -} diff --git a/frontend/src/scenes/project/Settings/AutocaptureSettings.tsx b/frontend/src/scenes/project/Settings/AutocaptureSettings.tsx deleted file mode 100644 index 61ad7d9363db93..00000000000000 --- a/frontend/src/scenes/project/Settings/AutocaptureSettings.tsx +++ /dev/null @@ -1,101 +0,0 @@ -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 { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' -import clsx from 'clsx' -import { autocaptureExceptionsLogic } from 'scenes/project/Settings/autocaptureExceptionsLogic' -import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' - -export function AutocaptureSettings(): JSX.Element { - const { userLoading } = useValues(userLogic) - const { currentTeam } = useValues(teamLogic) - const { updateCurrentTeam } = useActions(teamLogic) - const { reportIngestionAutocaptureToggled, reportIngestionAutocaptureExceptionsToggled } = - useActions(eventUsageLogic) - - const { errorsToIgnoreRules, rulesCharacters } = useValues(autocaptureExceptionsLogic) - const { setErrorsToIgnoreRules } = useActions(autocaptureExceptionsLogic) - - return ( - <> -

    Autocapture

    -

    - Automagically capture front-end interactions like pageviews, clicks, and more when using our web - JavaScript SDK.{' '} -

    -

    - Autocapture is also available for React Native, where it has to be{' '} - - configured directly in code - - . -

    -
    - { - updateCurrentTeam({ - autocapture_opt_out: !checked, - }) - reportIngestionAutocaptureToggled(!checked) - }} - checked={!currentTeam?.autocapture_opt_out} - disabled={userLoading} - label="Enable autocapture for web" - bordered - /> - -
    - { - updateCurrentTeam({ - autocapture_exceptions_opt_in: checked, - }) - reportIngestionAutocaptureExceptionsToggled(checked) - }} - checked={!!currentTeam?.autocapture_exceptions_opt_in} - disabled={userLoading} - label={ - <> - Enable exception autocapture ALPHA - - } - bordered - /> -

    Ignore errors

    -

    - If you're experiencing a high volume of unhelpful errors, add regular expressions here to - ignore them. This will ignore all errors that match, including those that are not - autocaptured. -

    -

    - 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 all errors that are exactly "bot", you can enter "^bot$". -

    -

    Only up to 300 characters of config are allowed here.

    - -
    300 ? 'text-danger' : 'text-muted' - )} - > - {rulesCharacters} / 300 characters -
    -
    -
    -
    - - ) -} diff --git a/frontend/src/scenes/project/Settings/ExtraTeamSettings.tsx b/frontend/src/scenes/project/Settings/ExtraTeamSettings.tsx deleted file mode 100644 index 7c8be42a75ede8..00000000000000 --- a/frontend/src/scenes/project/Settings/ExtraTeamSettings.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { LemonButton, LemonDivider, LemonInput, LemonSwitch, Link } from '@posthog/lemon-ui' -import { useState } from 'react' - -export enum SettingValueType { - Boolean = 'boolean', - Text = 'text', - Number = 'number', -} - -export interface ExtraSettingType { - name: string - description: string - key: string - moreInfo: string - valueType: SettingValueType -} - -const AVAILABLE_EXTRA_SETTINGS: ExtraSettingType[] = [ - { - name: 'Person on Events (Beta)', - description: `We have updated our data model to also store person properties directly on events, making queries significantly faster. This means that person properties will no longer be "timeless", but rather point-in-time i.e. on filters we'll consider a person's properties at the time of the event, rather than at present time. This may cause data to change on some of your insights, but will be the default way we handle person properties going forward. For now, you can control whether you want this on or not, and should feel free to let us know of any concerns you might have. If you do enable this, you should see speed improvements of around 3-5x on average on most of your insights.`, - moreInfo: - 'https://github.com/PostHog/posthog/blob/75a2111f2c4f9183dd45f85c7b103c7b0524eabf/plugin-server/src/worker/ingestion/PoE.md', - key: 'poe_v2_enabled', - valueType: SettingValueType.Boolean, - }, -] - -function ExtraSettingInput({ - defaultValue, - type, - settingKey, -}: { - defaultValue?: string | number - type: 'number' | 'text' - settingKey: string -}): JSX.Element { - const { currentTeam, currentTeamLoading } = useValues(teamLogic) - const { updateCurrentTeam } = useActions(teamLogic) - - const [value, setValue] = useState(defaultValue) - - return ( -
    - - - updateCurrentTeam({ extra_settings: { ...currentTeam?.extra_settings, [settingKey]: value } }) - } - loading={currentTeamLoading} - > - Update - -
    - ) -} - -export function ExtraTeamSettings(): JSX.Element { - const { updateCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - - return ( - <> - {AVAILABLE_EXTRA_SETTINGS.map((setting) => ( - <> -

    - {setting.name} -

    -
    -

    - {setting.description} - {setting.moreInfo ? More info. : null} -

    - {setting.valueType === SettingValueType.Boolean ? ( - { - updateCurrentTeam({ - extra_settings: { ...currentTeam?.extra_settings, [setting.key]: checked }, - }) - }} - label={`Enable ${setting.name}`} - checked={!!currentTeam?.extra_settings?.[setting.key]} - bordered - /> - ) : ( - - )} -
    - - - ))} - - ) -} diff --git a/frontend/src/scenes/project/Settings/IngestionInfo.tsx b/frontend/src/scenes/project/Settings/IngestionInfo.tsx deleted file mode 100644 index 73c62f32539ef7..00000000000000 --- a/frontend/src/scenes/project/Settings/IngestionInfo.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useActions, useValues } from 'kea' -import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' -import { JSSnippet } from 'lib/components/JSSnippet' -import { JSBookmarklet } from 'lib/components/JSBookmarklet' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { CodeSnippet } from 'lib/components/CodeSnippet' -import { IconRefresh } from 'lib/lemon-ui/icons' -import { Link } from 'lib/lemon-ui/Link' -import { AutocaptureSettings } from './AutocaptureSettings' - -export function IngestionInfo({ loadingComponent }: { loadingComponent: JSX.Element }): JSX.Element { - const { currentTeam, currentTeamLoading, isTeamTokenResetAvailable } = useValues(teamLogic) - const { resetToken } = useActions(teamLogic) - - if (currentTeam?.is_demo) { - return ( - <> -

    - Event ingestion -

    -

    - PostHog can ingest events from almost anywhere - JavaScript, Android, iOS, React Native, Node.js, - Ruby, Go, and more. -

    -

    - Demo projects like this one can't ingest events, but you can{' '} - - read about ingestion in our Docs - {' '} - and use a non-demo project to ingest your own events. -

    - - ) - } - - return ( - <> -

    - Web snippet -

    -

    - PostHog's configurable web snippet allows you to (optionally) autocapture events, record user sessions, - and more with no extra work. Place the following snippet in your website's HTML, ideally just above the{' '} - {''} tag. -

    -

    - For more guidance, including on identifying users,{' '} - see PostHog Docs. -

    - {currentTeamLoading && !currentTeam ? loadingComponent : } - - - -

    Need to test PostHog on a live site without changing any code?

    -

    - Just drag the bookmarklet below to your bookmarks bar, open the website you want to test PostHog on and - click it. This will enable our tracking, on the currently loaded page only. The data will show up in - this project. -

    -
    {isAuthenticatedTeam(currentTeam) && }
    - -

    - Send custom events -

    - To send custom events visit PostHog Docs and - integrate the library for the specific language or platform you're using. We support Python, Ruby, Node, Go, - PHP, iOS, Android, and more. - -

    - Project Variables -

    -

    - Project API Key -

    -

    - You can use this write-only key in any one of{' '} - our libraries. -

    - , - title: 'Reset project API key', - popconfirmProps: { - title: ( - <> - Reset the project's API key?{' '} - This will invalidate the current API key and cannot be undone. - - ), - okText: 'Reset key', - okType: 'danger', - placement: 'left', - }, - callback: resetToken, - }, - ] - : [] - } - thing="project API key" - > - {currentTeam?.api_token || ''} - -

    - Write-only means it can only create new events. It can't read events or any of your other data stored - with PostHog, so it's safe to use in public apps. -

    -

    - Project ID -

    -

    - You can use this ID to reference your project in our API. -

    - {String(currentTeam?.id || '')} - - ) -} diff --git a/frontend/src/scenes/project/Settings/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/project/Settings/PathCleaningFiltersConfig.tsx deleted file mode 100644 index d19fcf987d7865..00000000000000 --- a/frontend/src/scenes/project/Settings/PathCleaningFiltersConfig.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useActions, useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' -import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' - -export function PathCleaningFiltersConfig(): JSX.Element | null { - const { updateCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - - if (!currentTeam) { - return null - } - - return ( - { - updateCurrentTeam({ path_cleaning_filters: filters }) - }} - /> - ) -} diff --git a/frontend/src/scenes/project/Settings/SessionRecording.tsx b/frontend/src/scenes/project/Settings/SessionRecording.tsx deleted file mode 100644 index 91f0cf8691db62..00000000000000 --- a/frontend/src/scenes/project/Settings/SessionRecording.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Link } from '@posthog/lemon-ui' -import { urls } from 'scenes/urls' -import { SessionRecordingSettings } from 'scenes/session-recordings/settings/SessionRecordingSettings' - -export function SessionRecording(): JSX.Element { - return ( - <> -

    - Recordings -

    -

    - Watch recordings of how users interact with your web app to see what can be improved. Recordings are - found in the recordings page. -

    - - - - ) -} diff --git a/frontend/src/scenes/project/Settings/index.tsx b/frontend/src/scenes/project/Settings/index.tsx deleted file mode 100644 index 927d296c47f947..00000000000000 --- a/frontend/src/scenes/project/Settings/index.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { useState } from 'react' -import { BindLogic, useActions, useValues } from 'kea' -import { IPCapture } from './IPCapture' -import { SessionRecording } from './SessionRecording' -import { WebhookIntegration } from './WebhookIntegration' -import { useAnchor } from 'lib/hooks/useAnchor' -import { router } from 'kea-router' -import { teamLogic } from 'scenes/teamLogic' -import { DangerZone } from './DangerZone' -import { PageHeader } from 'lib/components/PageHeader' -import { Link } from 'lib/lemon-ui/Link' -import { RestrictedArea, RestrictionScope } from 'lib/components/RestrictedArea' -import { OrganizationMembershipLevel } from 'lib/constants' -import { TestAccountFiltersConfig } from './TestAccountFiltersConfig' -import { TimezoneConfig } from './TimezoneConfig' -import { DataAttributes } from 'scenes/project/Settings/DataAttributes' -import { AvailableFeature, InsightType, TeamType } from '~/types' -import { TeamMembers } from './TeamMembers' -import { teamMembersLogic } from './teamMembersLogic' -import { AccessControl } from './AccessControl' -import { PathCleaningFiltersConfig } from './PathCleaningFiltersConfig' -import { userLogic } from 'scenes/userLogic' -import { SceneExport } from 'scenes/sceneTypes' -import { CorrelationConfig } from './CorrelationConfig' -import { urls } from 'scenes/urls' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' -import { GroupAnalytics } from 'scenes/project/Settings/GroupAnalytics' -import { PersonDisplayNameProperties } from './PersonDisplayNameProperties' -import { SlackIntegration } from './SlackIntegration' -import { LemonButton, LemonDivider, LemonInput, LemonLabel } from '@posthog/lemon-ui' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { IngestionInfo } from './IngestionInfo' -import { ExtraTeamSettings } from './ExtraTeamSettings' -import { WeekStartConfig } from './WeekStartConfig' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { SurveySettings } from './Survey' - -export const scene: SceneExport = { - component: ProjectSettings, -} - -function DisplayName(): JSX.Element { - const { currentTeam, currentTeamLoading } = useValues(teamLogic) - const { updateCurrentTeam } = useActions(teamLogic) - - const [name, setName] = useState(currentTeam?.name || '') - - if (currentTeam?.is_demo) { - return ( -

    - The demo project cannot be renamed. -

    - ) - } - - return ( -
    - - updateCurrentTeam({ name })} - disabled={!name || !currentTeam || name === currentTeam.name} - loading={currentTeamLoading} - > - Rename Project - -
    - ) -} - -export function ProjectSettings(): JSX.Element { - const { currentTeam, currentTeamLoading } = useValues(teamLogic) - const { location } = useValues(router) - const { user, hasAvailableFeature } = useValues(userLogic) - const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED) - - useAnchor(location.hash) - - const LoadingComponent = (): JSX.Element => ( -
    - - -
    - ) - - return ( -
    - -
    -

    - Display name -

    - {currentTeamLoading && !currentTeam ? : } - - {currentTeamLoading && !currentTeam ? ( - - ) : ( - } /> - )} - -

    - Date and time -

    -

    - These settings affect how PostHog displays, buckets, and filters time-series data. You may need to - refresh insights for new settings to apply. -

    -
    - Time zone - - Week starts on - -
    - -

    - Filter out internal and test users -

    -

    - Increase the quality of your analytics results by filtering out events from internal sources, such - as team members, test accounts, or development environments.{' '} - - The filters you apply here are added as extra filters when the toggle is switched on. - {' '} - So, if you apply a cohort, it means you will only match users in that cohort. -

    - - Events and recordings will still be ingested and saved, but they will be excluded from any queries - where the "Filter out internal and test users" toggle is set. You can learn how to{' '} - - capture fewer events - {' '} - or how to{' '} - - capture fewer recordings - {' '} - in our docs. - -
    - Example filters -
      -
    • - "Email does not contain yourcompany.com" to exclude all - events from your company's team members. -
    • -
    • - "Host does not contain localhost" to exclude all events - from local development environments. -
    • -
    -
    - - - - {hasAdvancedPaths && ( - <> - -

    - Path cleaning rules - - Beta - -

    -

    - Make your Paths clearer by - aliasing one or multiple URLs.{' '} - - Example: http://tenant-one.mydomain.com/accounts and{' '} - http://tenant-two.mydomain.com/accounts can become a single{' '} - /accounts path. - -

    -

    - Each rule is composed of an alias and a regex pattern. Any pattern in a URL or event name - that matches the regex will be replaced with the alias. Rules are applied in the order that - they're listed. -

    -

    - - Rules that you set here will be applied before wildcarding and other regex replacement - if the toggle is switched on. - -

    - - - )} - -
    -

    - Authorized URLs -

    -

    - These are the URLs where the{' '} - - Toolbar will automatically launch - {' '} - (if you're logged in). -

    -

    - Domains and wildcard subdomains are allowed (example: https://*.example.com). - However, wildcarded top-level domains cannot be used (for security reasons). -

    - - -

    - Data attributes -

    - - -

    - Person display name -

    - - -

    - Webhook integration -

    - - - <> -

    - Slack integration -

    - - - -

    - Data capture configuration -

    - - - - - - - - - - {currentTeam?.access_control && hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && ( - - {user && } - - - )} - -
    -
    - ) -} diff --git a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx index 7c01bb6ca7a44b..3b4ee1eb9a1633 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.stories.tsx @@ -7,9 +7,9 @@ import { useEffect } from 'react' import { router } from 'kea-router' import { mswDecorator, useStorybookMocks } from '~/mocks/browser' -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 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' const insights = [trendsBarBreakdown, trendsPieBreakdown, funnelTopToBottom] diff --git a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx index 2edb70bf0f233b..33369ddf00218c 100644 --- a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx @@ -1,13 +1,13 @@ import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { CalendarOutlined } from '@ant-design/icons' import { SavedInsightsTabs } from '~/types' 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/Settings/membersLogic' +import { membersLogic } from 'scenes/organization/membersLogic' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { IconCalendar } from '@posthog/icons' export function SavedInsightsFilters(): JSX.Element { const { nameSortedDashboards } = useValues(dashboardsModel) @@ -66,7 +66,7 @@ export function SavedInsightsFilters(): JSX.Element { } makeLabel={(key) => ( <> - + {key} )} diff --git a/frontend/src/scenes/sceneLogic.test.tsx b/frontend/src/scenes/sceneLogic.test.tsx index edf4c4b4d6f3be..478b3047d70eda 100644 --- a/frontend/src/scenes/sceneLogic.test.tsx +++ b/frontend/src/scenes/sceneLogic.test.tsx @@ -15,8 +15,8 @@ export const logic = kea([path(['scenes', 'sceneLogic', 'test'])]) const sceneImport = (): any => ({ scene: { component: Component, logic: logic } }) const testScenes: Record any> = { - [Scene.Annotations]: sceneImport, - [Scene.MySettings]: sceneImport, + [Scene.DataManagement]: sceneImport, + [Scene.Settings]: sceneImport, } describe('sceneLogic', () => { @@ -26,7 +26,7 @@ describe('sceneLogic', () => { initKeaTests() await expectLogic(teamLogic).toDispatchActions(['loadCurrentTeamSuccess']) featureFlagLogic.mount() - router.actions.push(urls.annotations()) + router.actions.push(urls.eventDefinitions()) logic = sceneLogic({ scenes: testScenes }) logic.mount() }) @@ -43,45 +43,48 @@ describe('sceneLogic', () => { it('changing URL runs openScene, loadScene and setScene', async () => { await expectLogic(logic).toDispatchActions(['openScene', 'loadScene', 'setScene']).toMatchValues({ - scene: Scene.Annotations, + scene: Scene.DataManagement, }) - router.actions.push(urls.mySettings()) + router.actions.push(urls.settings('user')) await expectLogic(logic).toDispatchActions(['openScene', 'loadScene', 'setScene']).toMatchValues({ - scene: Scene.MySettings, + scene: Scene.Settings, }) }) it('persists the loaded scenes', async () => { const expectedAnnotation = partial({ - name: Scene.Annotations, + name: Scene.DataManagement, component: expect.any(Function), logic: expect.any(Function), sceneParams: { hashParams: {}, params: {}, searchParams: {} }, lastTouch: expect.any(Number), }) - const expectedMySettings = partial({ - name: Scene.MySettings, + const expectedSettings = partial({ + name: Scene.Settings, component: expect.any(Function), - sceneParams: { hashParams: {}, params: {}, searchParams: {} }, + sceneParams: { + hashParams: {}, + params: { + section: 'user', + }, + searchParams: {}, + }, + logic: expect.any(Function), lastTouch: expect.any(Number), }) - await expectLogic(logic) - .delay(1) - .toMatchValues({ - loadedScenes: partial({ - [Scene.Annotations]: expectedAnnotation, - }), - }) - router.actions.push(urls.mySettings()) - await expectLogic(logic) - .delay(1) - .toMatchValues({ - loadedScenes: partial({ - [Scene.Annotations]: expectedAnnotation, - [Scene.MySettings]: expectedMySettings, - }), - }) + await expectLogic(logic).delay(1) + + expect(logic.values.loadedScenes).toMatchObject({ + [Scene.DataManagement]: expectedAnnotation, + }) + router.actions.push(urls.settings('user')) + await expectLogic(logic).delay(1) + + expect(logic.values.loadedScenes).toMatchObject({ + [Scene.DataManagement]: expectedAnnotation, + [Scene.Settings]: expectedSettings, + }) }) }) diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index 8068f4f053ac27..bd5c46206d001c 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -19,17 +19,12 @@ import { FEATURE_FLAGS } from 'lib/constants' /** Mapping of some scenes that aren't directly accessible from the sidebar to ones that are - for the sidebar. */ const sceneNavAlias: Partial> = { [Scene.Action]: Scene.DataManagement, - [Scene.Actions]: Scene.DataManagement, - [Scene.EventDefinitions]: Scene.DataManagement, - [Scene.PropertyDefinitions]: Scene.DataManagement, [Scene.EventDefinition]: Scene.DataManagement, [Scene.PropertyDefinition]: Scene.DataManagement, - [Scene.IngestionWarnings]: Scene.DataManagement, - [Scene.Person]: Scene.Persons, - [Scene.Cohort]: Scene.Cohorts, - [Scene.Groups]: Scene.Persons, + [Scene.Person]: Scene.PersonsManagement, + [Scene.Cohort]: Scene.PersonsManagement, [Scene.Experiment]: Scene.Experiments, - [Scene.Group]: Scene.Persons, + [Scene.Group]: Scene.PersonsManagement, [Scene.Dashboard]: Scene.Dashboards, [Scene.FeatureFlag]: Scene.FeatureFlags, [Scene.EarlyAccessFeature]: Scene.EarlyAccessFeatures, @@ -39,6 +34,7 @@ const sceneNavAlias: Partial> = { [Scene.DataWarehouseExternal]: Scene.DataWarehouse, [Scene.DataWarehouseSavedQueries]: Scene.DataWarehouse, [Scene.DataWarehouseSettings]: Scene.DataWarehouse, + [Scene.DataWarehouseTable]: Scene.DataWarehouse, [Scene.AppMetrics]: Scene.Apps, [Scene.ReplaySingle]: Scene.Replay, [Scene.ReplayPlaylist]: Scene.ReplayPlaylist, @@ -257,7 +253,7 @@ export const sceneLogic = kea([ !location.pathname.startsWith('/ingestion') && !location.pathname.startsWith('/onboarding') && !location.pathname.startsWith('/products') && - !location.pathname.startsWith('/project/settings') + !location.pathname.startsWith('/settings') ) { if ( featureFlagLogic.values.featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] === diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index dc17598a8ee811..5d5ed7a89c3ffe 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -9,29 +9,21 @@ export enum Scene { ErrorProjectUnavailable = 'ProjectUnavailable', Dashboards = 'Dashboards', Dashboard = 'Dashboard', - Database = 'Database', Insight = 'Insight', WebAnalytics = 'WebAnalytics', - Cohorts = 'Cohorts', Cohort = 'Cohort', Events = 'Events', DataManagement = 'DataManagement', - EventDefinitions = 'EventDefinitionsTable', EventDefinition = 'EventDefinition', - PropertyDefinitions = 'PropertyDefinitionsTable', PropertyDefinition = 'PropertyDefinition', - DataManagementHistory = 'DataManagementHistory', - IngestionWarnings = 'IngestionWarnings', Replay = 'Replay', ReplaySingle = 'ReplaySingle', ReplayPlaylist = 'ReplayPlaylist', + PersonsManagement = 'PersonsManagement', Person = 'Person', - Persons = 'Persons', Pipeline = 'Pipeline', - Groups = 'Groups', Group = 'Group', Action = 'Action', - Actions = 'ActionsTable', Experiments = 'Experiments', Experiment = 'Experiment', BatchExports = 'BatchExports', @@ -48,17 +40,14 @@ export enum Scene { DataWarehousePosthog = 'DataWarehousePosthog', DataWarehouseExternal = 'DataWarehouseExternal', DataWarehouseSavedQueries = 'DataWarehouseSavedQueries', + DataWarehouseTable = 'DataWarehouseTable', DataWarehouseSettings = 'DataWarehouseSettings', - OrganizationSettings = 'OrganizationSettings', OrganizationCreateFirst = 'OrganizationCreate', ProjectHomepage = 'ProjectHomepage', - ProjectSettings = 'ProjectSettings', ProjectCreateFirst = 'ProjectCreate', SystemStatus = 'SystemStatus', AsyncMigrations = 'AsyncMigrations', DeadLetterQueue = 'DeadLetterQueue', - MySettings = 'MySettings', - Annotations = 'Annotations', Billing = 'Billing', Apps = 'Apps', FrontendAppScene = 'FrontendAppScene', @@ -86,6 +75,7 @@ export enum Scene { Canvas = 'Canvas', Products = 'Products', Onboarding = 'Onboarding', + Settings = 'Settings', } export type SceneProps = Record diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 3d3b3baa214db4..dc5aa42bb885e9 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -48,10 +48,6 @@ export const sceneConfigurations: Partial> = { name: 'Web Analytics', layout: 'app-container', }, - [Scene.Cohorts]: { - projectBased: true, - name: 'Cohorts', - }, [Scene.Cohort]: { projectBased: true, name: 'Cohort', @@ -76,38 +72,14 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Data Management', }, - [Scene.Actions]: { - projectBased: true, - name: 'Data Management', - }, - [Scene.EventDefinitions]: { - projectBased: true, - name: 'Data Management', - }, [Scene.EventDefinition]: { projectBased: true, name: 'Data Management', }, - [Scene.PropertyDefinitions]: { - projectBased: true, - name: 'Data Management', - }, [Scene.PropertyDefinition]: { projectBased: true, name: 'Data Management', }, - [Scene.DataManagementHistory]: { - projectBased: true, - name: 'Data Management', - }, - [Scene.IngestionWarnings]: { - projectBased: true, - name: 'Data Management', - }, - [Scene.Database]: { - projectBased: true, - name: 'Data Management', - }, [Scene.Replay]: { projectBased: true, name: 'Session Replay', @@ -124,7 +96,7 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Person', }, - [Scene.Persons]: { + [Scene.PersonsManagement]: { projectBased: true, name: 'Persons & Groups', }, @@ -132,10 +104,6 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Action', }, - [Scene.Groups]: { - projectBased: true, - name: 'Persons & Groups', - }, [Scene.Group]: { projectBased: true, name: 'Persons & Groups', @@ -191,15 +159,15 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Data Warehouse Settings', }, - [Scene.EarlyAccessFeatures]: { + [Scene.DataWarehouseTable]: { projectBased: true, + name: 'Data Warehouse Table', }, - [Scene.EarlyAccessFeature]: { + [Scene.EarlyAccessFeatures]: { projectBased: true, }, - [Scene.Annotations]: { + [Scene.EarlyAccessFeature]: { projectBased: true, - name: 'Annotations', }, [Scene.Apps]: { projectBased: true, @@ -221,11 +189,6 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Homepage', }, - [Scene.ProjectSettings]: { - projectBased: true, - hideProjectNotice: true, - name: 'Project settings', - }, [Scene.IntegrationsRedirect]: { name: 'Integrations Redirect', }, @@ -258,9 +221,6 @@ export const sceneConfigurations: Partial> = { name: 'Confirm organization creation', onlyUnauthenticated: true, }, - [Scene.OrganizationSettings]: { - organizationBased: true, - }, [Scene.ProjectCreateFirst]: { name: 'Project creation', organizationBased: true, @@ -299,10 +259,6 @@ export const sceneConfigurations: Partial> = { [Scene.DeadLetterQueue]: { instanceLevel: true, }, - // Personal routes - [Scene.MySettings]: { - personal: true, - }, // Cloud-only routes [Scene.Billing]: { hideProjectNotice: true, @@ -310,6 +266,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.Unsubscribe]: { allowUnauthenticated: true, + layout: 'app-raw', }, [Scene.DebugQuery]: { projectBased: true, @@ -336,6 +293,10 @@ export const sceneConfigurations: Partial> = { name: 'Canvas', layout: 'app-raw', }, + [Scene.Settings]: { + projectBased: true, + name: 'Settings', + }, } const preserveParams = (url: string) => (_params: Params, searchParams: Params, hashParams: Params) => { @@ -353,12 +314,12 @@ export const redirects: Record< '/dashboards': urls.dashboards(), '/plugins': urls.projectApps(), '/project/plugins': urls.projectApps(), - '/actions': urls.actions(), // TODO: change to urls.eventDefinitions() when "simplify-actions" FF is released - '/organization/members': urls.organizationSettings(), + '/actions': urls.actions(), + '/organization/members': urls.settings('organization'), '/i/:shortId': ({ shortId }) => urls.insightView(shortId), '/action/:id': ({ id }) => urls.action(id), '/action': urls.createAction(), - '/events/actions': urls.actions(), // TODO: change to urls.eventDefinitions() when "simplify-actions" FF is released + '/events/actions': urls.actions(), '/events/stats': urls.eventDefinitions(), '/events/stats/:id': ({ id }) => urls.eventDefinition(id), '/events/:id/*': ({ id, _ }) => { @@ -381,6 +342,8 @@ export const redirects: Record< }, '/events/properties': urls.propertyDefinitions(), '/events/properties/:id': ({ id }) => urls.propertyDefinition(id), + '/annotations': () => urls.annotations(), + '/annotations/:id': ({ id }) => urls.annotation(id), '/recordings/:id': ({ id }) => urls.replaySingle(id), '/recordings/playlists/:id': ({ id }) => urls.replayPlaylist(id), '/recordings': (_params, _searchParams, hashParams) => { @@ -392,6 +355,10 @@ export const redirects: Record< }, '/replay': urls.replay(), '/exports': urls.batchExports(), + '/settings': urls.settings(), + '/project/settings': urls.settings('project'), + '/organization/settings': urls.settings('organization'), + '/me/settings': urls.settings('user'), } export const routes: Record = { @@ -404,7 +371,7 @@ export const routes: Record = { [urls.createAction()]: Scene.Action, [urls.copyAction(null)]: Scene.Action, [urls.action(':id')]: Scene.Action, - [urls.ingestionWarnings()]: Scene.IngestionWarnings, + [urls.ingestionWarnings()]: Scene.DataManagement, [urls.insightNew()]: Scene.Insight, [urls.insightEdit(':shortId' as InsightShortId)]: Scene.Insight, [urls.insightView(':shortId' as InsightShortId)]: Scene.Insight, @@ -413,17 +380,17 @@ export const routes: Record = { [urls.insightSharing(':shortId' as InsightShortId)]: Scene.Insight, [urls.savedInsights()]: Scene.SavedInsights, [urls.webAnalytics()]: Scene.WebAnalytics, - [urls.actions()]: Scene.Actions, // TODO: remove when "simplify-actions" FF is released - [urls.eventDefinitions()]: Scene.EventDefinitions, + [urls.actions()]: Scene.DataManagement, + [urls.eventDefinitions()]: Scene.DataManagement, [urls.eventDefinition(':id')]: Scene.EventDefinition, [urls.batchExports()]: Scene.BatchExports, [urls.batchExportNew()]: Scene.BatchExportEdit, [urls.batchExport(':id')]: Scene.BatchExport, [urls.batchExportEdit(':id')]: Scene.BatchExportEdit, - [urls.propertyDefinitions()]: Scene.PropertyDefinitions, + [urls.propertyDefinitions()]: Scene.DataManagement, [urls.propertyDefinition(':id')]: Scene.PropertyDefinition, - [urls.dataManagementHistory()]: Scene.DataManagementHistory, - [urls.database()]: Scene.Database, + [urls.dataManagementHistory()]: Scene.DataManagement, + [urls.database()]: Scene.DataManagement, [urls.events()]: Scene.Events, [urls.replay()]: Scene.Replay, // One entry for every available tab @@ -435,18 +402,18 @@ export const routes: Record = { [urls.replayPlaylist(':id')]: Scene.ReplayPlaylist, [urls.personByDistinctId('*', false)]: Scene.Person, [urls.personByUUID('*', false)]: Scene.Person, - [urls.persons()]: Scene.Persons, + [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), - [urls.groups(':groupTypeIndex')]: Scene.Groups, + [urls.groups(':groupTypeIndex')]: Scene.PersonsManagement, [urls.group(':groupTypeIndex', ':groupKey', false)]: Scene.Group, [urls.group(':groupTypeIndex', ':groupKey', false, ':groupTab')]: Scene.Group, [urls.cohort(':id')]: Scene.Cohort, - [urls.cohorts()]: Scene.Cohorts, + [urls.cohorts()]: Scene.PersonsManagement, [urls.experiments()]: Scene.Experiments, [urls.experiment(':id')]: Scene.Experiment, [urls.earlyAccessFeatures()]: Scene.EarlyAccessFeatures, @@ -455,16 +422,16 @@ export const routes: Record = { [urls.survey(':id')]: Scene.Survey, [urls.surveyTemplates()]: Scene.SurveyTemplates, [urls.dataWarehouse()]: Scene.DataWarehouse, + [urls.dataWarehouseTable()]: Scene.DataWarehouseTable, [urls.dataWarehousePosthog()]: Scene.DataWarehousePosthog, [urls.dataWarehouseExternal()]: Scene.DataWarehouseExternal, [urls.dataWarehouseSavedQueries()]: Scene.DataWarehouseSavedQueries, [urls.dataWarehouseSettings()]: Scene.DataWarehouseSettings, [urls.featureFlags()]: Scene.FeatureFlags, [urls.featureFlag(':id')]: Scene.FeatureFlag, - [urls.annotations()]: Scene.Annotations, - [urls.annotation(':id')]: Scene.Annotations, + [urls.annotations()]: Scene.DataManagement, + [urls.annotation(':id')]: Scene.DataManagement, [urls.projectHomepage()]: Scene.ProjectHomepage, - [urls.projectSettings()]: Scene.ProjectSettings, [urls.projectApps()]: Scene.Apps, [urls.projectApp(':id')]: Scene.Apps, [urls.projectAppLogs(':id')]: Scene.Apps, @@ -475,7 +442,6 @@ export const routes: Record = { [urls.appHistory(':pluginConfigId')]: Scene.AppMetrics, [urls.appLogs(':pluginConfigId')]: Scene.AppMetrics, [urls.projectCreateFirst()]: Scene.ProjectCreateFirst, - [urls.organizationSettings()]: Scene.OrganizationSettings, [urls.organizationBilling()]: Scene.Billing, [urls.organizationCreateFirst()]: Scene.OrganizationCreateFirst, [urls.organizationCreationConfirm()]: Scene.OrganizationCreationConfirm, @@ -488,7 +454,6 @@ export const routes: Record = { [urls.asyncMigrationsFuture()]: Scene.AsyncMigrations, [urls.asyncMigrationsSettings()]: Scene.AsyncMigrations, [urls.deadLetterQueue()]: Scene.DeadLetterQueue, - [urls.mySettings()]: Scene.MySettings, [urls.toolbarLaunch()]: Scene.ToolbarLaunch, [urls.site(':url')]: Scene.Site, // Onboarding / setup routes @@ -514,4 +479,5 @@ export const routes: Record = { [urls.notebook(':shortId')]: Scene.Notebook, [urls.notebooks()]: Scene.Notebooks, [urls.canvas()]: Scene.Canvas, + [urls.settings(':section' as any)]: Scene.Settings, } diff --git a/frontend/src/scenes/session-recordings/SessionRecordings.tsx b/frontend/src/scenes/session-recordings/SessionRecordings.tsx index 7e44843bf4d926..fa3277a7283e16 100644 --- a/frontend/src/scenes/session-recordings/SessionRecordings.tsx +++ b/frontend/src/scenes/session-recordings/SessionRecordings.tsx @@ -11,7 +11,6 @@ import { humanFriendlyTabName, sessionRecordingsLogic } from './sessionRecording import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { IconSettings } from 'lib/lemon-ui/icons' import { router } from 'kea-router' -import { openSessionRecordingSettingsDialog } from './settings/SessionRecordingSettings' import { SessionRecordingFilePlayback } from './file-playback/SessionRecordingFilePlayback' import { createPlaylist } from './playlist/playlistUtils' import { useAsyncHandler } from 'lib/hooks/useAsyncHandler' @@ -25,6 +24,7 @@ import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/Au import { SessionRecordingsPlaylist } from './playlist/SessionRecordingsPlaylist' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' export function SessionsRecordings(): JSX.Element { const { currentTeam } = useValues(teamLogic) @@ -34,6 +34,7 @@ export function SessionsRecordings(): JSX.Element { const { guardAvailableFeature } = useActions(sceneLogic) const playlistsLogic = savedSessionRecordingPlaylistsLogic({ tab: ReplayTabs.Recent }) const { playlists } = useValues(playlistsLogic) + const { openSettingsPanel } = useActions(sidePanelSettingsLogic) const theAuthorizedUrlsLogic = authorizedUrlListLogic({ actionId: null, @@ -67,10 +68,8 @@ export function SessionsRecordings(): JSX.Element { <> @@ -101,7 +100,7 @@ export function SessionsRecordings(): JSX.Element { } - onClick={() => openSessionRecordingSettingsDialog()} + onClick={() => openSettingsPanel({ sectionId: 'project-replay' })} > Configure @@ -147,7 +146,7 @@ export function SessionsRecordings(): JSX.Element { action={{ type: 'secondary', icon: , - onClick: () => openSessionRecordingSettingsDialog(), + onClick: () => openSettingsPanel({ sectionId: 'project-replay' }), children: 'Configure', }} > @@ -161,7 +160,7 @@ export function SessionsRecordings(): JSX.Element { action={{ type: 'secondary', icon: , - onClick: () => openSessionRecordingSettingsDialog(), + onClick: () => openSettingsPanel({ sectionId: 'project-replay' }), children: 'Configure', }} dismissKey={`session-recordings-authorized-domains-warning/${suggestions.join(',')}`} diff --git a/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx b/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx index c39a26afd90f86..721a17c735e9c4 100644 --- a/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx +++ b/frontend/src/scenes/session-recordings/detail/SessionRecordingDetail.tsx @@ -31,7 +31,7 @@ export function SessionRecordingDetail({ id }: SessionRecordingDetailLogicProps
    Session recordings are currently disabled for this project. To use this feature, please go to - your project settings and enable it. + your project settings and enable it.
    ) : null} diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts index 8d4d1ebe5c878a..54ef82ab8da18b 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts @@ -77,6 +77,40 @@ export const parseExportedSessionRecording = (fileData: string): ExportedSession } } +/** + * There's a race between loading the file causing the React component to be rendered that mounts the dataLogic + * and this logic loading the file and wanting to tell the logic about it + * + * This method waits for the dataLogic to be mounted and returns it + * + * in practice, it will only wait for 1-2 retries + * but a timeout is provided to avoid waiting forever when something breaks + */ +const waitForDataLogic = async (playerKey: string): Promise> => { + const maxRetries = 20 // 2 seconds / 100 ms per retry + let retries = 0 + let dataLogic = null + + while (retries < maxRetries) { + dataLogic = sessionRecordingDataLogic.findMounted({ + sessionRecordingId: '', + playerKey: playerKey, + }) + + if (dataLogic !== null) { + // eslint-disable-next-line no-console + console.log('found after retries', retries) + return dataLogic + } + + // Wait for a short period before trying again + await new Promise((resolve) => setTimeout(resolve, 1)) + retries++ + } + + throw new Error('Timeout reached: dataLogic is still null after 2 seconds') +} + export const sessionRecordingFilePlaybackLogic = kea([ path(['scenes', 'session-recordings', 'detail', 'sessionRecordingDetailLogic']), connect({ @@ -125,12 +159,8 @@ export const sessionRecordingFilePlaybackLogic = kea ({ - loadFromFileSuccess: () => { - // Once we loaded the file we set the logic - const dataLogic = sessionRecordingDataLogic.findMounted({ - sessionRecordingId: '', - playerKey: values.playerKey, - }) + loadFromFileSuccess: async () => { + const dataLogic = await waitForDataLogic(values.playerKey) if (!dataLogic || !values.sessionRecording) { return diff --git a/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx b/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx index e943988808d69d..372e813f6cbdde 100644 --- a/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx +++ b/frontend/src/scenes/session-recordings/filters/SimpleSessionRecordingsFilters.tsx @@ -86,7 +86,7 @@ export const SimpleSessionRecordingsFilters = ({ }} /> {displayNameProperties.length === 0 && ( - + }> Add person properties diff --git a/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx b/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx index 5715722ccb1f9c..d3b067a32eac43 100644 --- a/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx +++ b/frontend/src/scenes/session-recordings/player/RecordingNotFound.tsx @@ -10,7 +10,7 @@ export function RecordingNotFound(): JSX.Element { <> The requested recording doesn't seem to exist. The recording may still be processing, deleted due to age or have not been enabled. Please check your{' '} - project settings that recordings is turned on and enabled + project settings that recordings is turned on and enabled for the domain in question. } diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx index d95fc4bdaee0a6..483ba76debca29 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx @@ -129,7 +129,9 @@ export function SessionRecordingPlayer(props: SessionRecordingPlayerProps): JSX. 400: 'small', 1000: 'medium', }, - playerRef + { + ref: playerRef, + } ) const [inspectorFocus, setInspectorFocus] = useState(false) diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx index d999f28bb83c95..97ca3071033274 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx @@ -8,6 +8,7 @@ import { useKeyHeld } from 'lib/hooks/useKeyHeld' import { IconSkipBackward } from 'lib/lemon-ui/icons' import clsx from 'clsx' import { dayjs } from 'lib/dayjs' +import { TZLabel } from '@posthog/apps-common' export function Timestamp(): JSX.Element { const { logicProps, currentPlayerTime, currentTimestamp, sessionPlayerData } = @@ -21,9 +22,9 @@ export function Timestamp(): JSX.Element { return (
    - - {colonDelimitedDuration(startTimeSeconds, fixedUnits)} - {' '} + + {colonDelimitedDuration(startTimeSeconds, fixedUnits)} + {' '} / {colonDelimitedDuration(endTimeSeconds, fixedUnits)}
    ) diff --git a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx index c2ed3c6ef3bc1a..56e771c9531d7f 100644 --- a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx @@ -63,9 +63,9 @@ export function Seekbar(): JSX.Element { ))}
    - {/* eslint-disable-next-line react/forbid-dom-props */}
    {/* eslint-disable-next-line react/forbid-dom-props */} diff --git a/frontend/src/scenes/session-recordings/player/icons.tsx b/frontend/src/scenes/session-recordings/player/icons.tsx index 6fff14ba753b5d..fa8a79d631150e 100644 --- a/frontend/src/scenes/session-recordings/player/icons.tsx +++ b/frontend/src/scenes/session-recordings/player/icons.tsx @@ -10,7 +10,7 @@ export function IconWindowOld({ value, className = '', size = 'medium' }: IconWi const shortValue = typeof value === 'number' ? value : String(value).charAt(0) return (
    - + {shortValue} - openSessionRecordingSettingsDialog()} targetBlank> + openSettingsPanel({ sectionId: 'project-replay' })} + targetBlank + > Configure in settings
    @@ -57,6 +62,8 @@ function EmptyNetworkTab({ } function EmptyConsoleTab({ captureConsoleLogOptIn }: { captureConsoleLogOptIn: boolean }): JSX.Element { + const { openSettingsPanel } = useActions(sidePanelSettingsLogic) + return captureConsoleLogOptIn ? ( <>No results found in this recording. ) : ( @@ -67,7 +74,11 @@ function EmptyConsoleTab({ captureConsoleLogOptIn }: { captureConsoleLogOptIn: b Capture all console logs during the browser recording to get technical information on what was occurring.

    - openSessionRecordingSettingsDialog()} targetBlank> + openSettingsPanel({ sectionId: 'project-replay' })} + targetBlank + > Configure in settings
    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 9df94720ef5413..49bb97485ad82d 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -169,7 +169,7 @@ export function ItemPerformanceEvent({ {performanceSummaryCards.map(({ label, description, key, scoreBenchmarks }, index) => ( {index !== 0 && } - +
    {label}
    diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts index 9c55ee262de76e..534b324194276d 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts @@ -84,6 +84,7 @@ export const playerMetaLogic = kea([ currentWindowIndex: [ (s) => [s.windowIds, s.currentSegment], (windowIds, currentSegment) => { + // eslint-disable-next-line no-constant-binary-expression const index = windowIds.findIndex((windowId) => windowId === currentSegment?.windowId ?? -1) return index === -1 ? 0 : index diff --git a/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap b/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap index ecae917dffa9b3..96d29cfbaaff86 100644 --- a/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap +++ b/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap @@ -1,9 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CorsPlugin should replace font urls in links 1`] = `"https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.woff2?t=1234"`; - -exports[`CorsPlugin should replace font urls in links 2`] = `"https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.ttf"`; - exports[`CorsPlugin should replace font urls in stylesheets 1`] = `"@font-face { font-display: fallback; font-family: "Roboto Condensed"; font-weight: 400; font-style: normal; src: url("https://replay.ph-proxy.com/proxy?url=https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff2?11012022") format("woff2"), url("https://replay.ph-proxy.com/proxy?url=https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff?11012022")"`; exports[`CorsPlugin should replace font urls in stylesheets 2`] = `"url("https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.woff2")"`; diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts index da2684cf38e5af..ae4694b15112fb 100644 --- a/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts @@ -1,6 +1,9 @@ import { CorsPlugin } from '.' describe('CorsPlugin', () => { + it.each(['https://some-external.js'])('should replace JS urls', (jsUrl) => { + expect(CorsPlugin._replaceJSUrl(jsUrl)).toEqual(`https://replay.ph-proxy.com/proxy?url=${jsUrl}`) + }) it.each([ `@font-face { font-display: fallback; font-family: "Roboto Condensed"; font-weight: 400; font-style: normal; src: url("https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff2?11012022") format("woff2"), url("https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff?11012022")`, `url("https://app.posthog.com/fonts/my-font.woff2")`, @@ -11,7 +14,7 @@ describe('CorsPlugin', () => { it.each(['https://app.posthog.com/fonts/my-font.woff2?t=1234', 'https://app.posthog.com/fonts/my-font.ttf'])( 'should replace font urls in links', (content: string) => { - expect(CorsPlugin._replaceFontUrl(content)).toMatchSnapshot() + expect(CorsPlugin._replaceFontUrl(content)).toEqual(`https://replay.ph-proxy.com/proxy?url=${content}`) } ) @@ -21,4 +24,12 @@ describe('CorsPlugin', () => { expect(CorsPlugin._replaceFontUrl(content)).toEqual(content) } ) + + it('can replace a modulepreload js link', () => { + const el = document.createElement('link') + el.setAttribute('rel', 'modulepreload') + el.href = 'https://app.posthog.com/my-image.js' + CorsPlugin.onBuild?.(el, { id: 1, replayer: null as unknown as any }) + expect(el.href).toEqual(`https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/my-image.js`) + }) }) diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.ts index f2032d070d4a0a..72f65e69c9551b 100644 --- a/frontend/src/scenes/session-recordings/player/rrweb/index.ts +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.ts @@ -5,6 +5,7 @@ const PROXY_URL = 'https://replay.ph-proxy.com' as const export const CorsPlugin: ReplayPlugin & { _replaceFontCssUrls: (value: string) => string _replaceFontUrl: (value: string) => string + _replaceJSUrl: (value: string) => string } = { _replaceFontCssUrls: (value: string): string => { return value.replace( @@ -17,6 +18,10 @@ export const CorsPlugin: ReplayPlugin & { return value.replace(/^(https:\/\/\S*(?:.eot|.woff2|.ttf|.woff)\S*)$/i, `${PROXY_URL}/proxy?url=$1`) }, + _replaceJSUrl: (value: string): string => { + return value.replace(/^(https:\/\/\S*(?:.js)\S*)$/i, `${PROXY_URL}/proxy?url=$1`) + }, + onBuild: (node) => { if (node.nodeName === 'STYLE') { const styleElement = node as HTMLStyleElement @@ -25,7 +30,20 @@ export const CorsPlugin: ReplayPlugin & { if (node.nodeName === 'LINK') { const linkElement = node as HTMLLinkElement - linkElement.href = CorsPlugin._replaceFontUrl(linkElement.href) + const href = linkElement.href + if (!href) { + return + } + if (linkElement.getAttribute('rel') == 'modulepreload') { + linkElement.href = CorsPlugin._replaceJSUrl(href) + } else { + linkElement.href = CorsPlugin._replaceFontUrl(href) + } + } + + if (node.nodeName === 'SCRIPT') { + const scriptElement = node as HTMLScriptElement + scriptElement.src = CorsPlugin._replaceJSUrl(scriptElement.src) } }, } diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index 5607d4865a4439..d9028d9034c1c3 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -21,7 +21,7 @@ import { SessionRecordingUsageType, } from '~/types' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { eventWithTime } from '@rrweb/types' +import { EventType, eventWithTime } from '@rrweb/types' import { Dayjs, dayjs } from 'lib/dayjs' import type { sessionRecordingDataLogicType } from './sessionRecordingDataLogicType' import { chainToElements } from 'lib/utils/elements-chain' @@ -311,7 +311,7 @@ export const sessionRecordingDataLogic = kea([ if (!values.sessionPlayerMetaData) { return null } - breakpoint(100) + await breakpoint(100) await api.recordings.persist(props.sessionRecordingId) return { @@ -600,6 +600,42 @@ export const sessionRecordingDataLogic = kea([ }, ], + snapshotsInvalid: [ + (s, p) => [s.snapshotsByWindowId, s.fullyLoaded, p.sessionRecordingId], + (snapshotsByWindowId, fullyLoaded, sessionRecordingId): boolean => { + if (!fullyLoaded) { + return false + } + + const windowsHaveFullSnapshot = Object.entries(snapshotsByWindowId).reduce( + (acc, [windowId, events]) => { + acc[`window-id-${windowId}-has-full-snapshot`] = events.some( + (event) => event.type === EventType.FullSnapshot + ) + return acc + }, + {} + ) + const anyWindowMissingFullSnapshot = !Object.values(windowsHaveFullSnapshot).some((x) => x) + const everyWindowMissingFullSnapshot = !Object.values(windowsHaveFullSnapshot).every((x) => x) + + if (everyWindowMissingFullSnapshot) { + // video is definitely unplayable + posthog.capture('recording_has_no_full_snapshot', { + ...windowsHaveFullSnapshot, + sessionId: sessionRecordingId, + }) + } else if (anyWindowMissingFullSnapshot) { + posthog.capture('recording_window_missing_full_snapshot', { + ...windowsHaveFullSnapshot, + sessionId: sessionRecordingId, + }) + } + + return everyWindowMissingFullSnapshot + }, + ], + bufferedToTime: [ (s) => [s.segments], (segments): number | null => { diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx index ebbdfa03727f8f..e5a8a07ba2eec3 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx @@ -6,7 +6,7 @@ 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/Settings/membersLogic' +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' diff --git a/frontend/src/scenes/settings/Settings.scss b/frontend/src/scenes/settings/Settings.scss new file mode 100644 index 00000000000000..84bc262bb70161 --- /dev/null +++ b/frontend/src/scenes/settings/Settings.scss @@ -0,0 +1,42 @@ +.Settings { + display: flex; + gap: 2rem; + align-items: start; + + .Settings__sections { + flex-shrink: 0; + position: sticky; + max-width: 20rem; + min-width: 14rem; + width: 20%; + top: 0.5rem; + + .posthog-3000 & { + top: 4rem; + } + + .SidePanel3000 & { + top: 0; + } + } + + &--compact { + gap: 0; + + flex-direction: column; + + .Settings__sections { + width: 100%; + min-width: 100%; + max-width: 100%; + position: relative; + } + } + + margin-top: 1rem; + + .posthog-3000 &, + .LemonModal & { + margin-top: 0; + } +} diff --git a/frontend/src/scenes/settings/Settings.tsx b/frontend/src/scenes/settings/Settings.tsx new file mode 100644 index 00000000000000..80097630a04869 --- /dev/null +++ b/frontend/src/scenes/settings/Settings.tsx @@ -0,0 +1,142 @@ +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 { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' +import { teamLogic } from 'scenes/teamLogic' + +import './Settings.scss' +import { NotFound } from 'lib/components/NotFound' + +export function Settings({ + hideSections = false, + ...props +}: SettingsLogicProps & { hideSections?: boolean }): JSX.Element { + const { selectedSectionId, selectedSection, selectedLevel, sections, isCompactNavigationOpen } = useValues( + settingsLogic(props) + ) + const { selectSection, selectLevel, openCompactNavigation } = useActions(settingsLogic(props)) + const { currentTeam } = useValues(teamLogic) + + const { ref, size } = useResizeBreakpoints( + { + 0: 'small', + 700: 'medium', + }, + { + initialSize: 'medium', + } + ) + + const isCompact = size === 'small' + + const showSections = isCompact ? isCompactNavigationOpen : true + + return ( +
    + {hideSections ? null : ( + <> + {showSections ? ( +
    +
      + {SettingLevelIds.map((level) => ( +
    • + selectLevel(level)} + size="small" + fullWidth + active={selectedLevel === level && !selectedSectionId} + > + + {capitalizeFirstLetter(level)} + + + +
        + {sections + .filter((x) => x.level === level) + .map((section) => ( +
      • + selectSection(section.id)} + size="small" + fullWidth + active={selectedSectionId === section.id} + > + {section.title} + +
      • + ))} +
      +
    • + ))} +
    +
    + ) : ( + } onClick={() => openCompactNavigation()}> + {capitalizeFirstLetter(selectedLevel)} + {selectedSection ? ` / ${selectedSection.title}` : null} + + )} + {isCompact ? : null} + + )} + +
    + {!hideSections && selectedLevel === 'project' && ( + + These settings only apply to the current project{' '} + {currentTeam?.name ? ( + <> + ({currentTeam.name}) + + ) : null} + . + + )} + + +
    +
    + ) +} + +function SettingsRenderer(props: SettingsLogicProps): JSX.Element { + const { settings } = useValues(settingsLogic(props)) + const { selectSetting } = useActions(settingsLogic(props)) + + return ( +
    + {settings.length ? ( + settings.map((x) => ( +
    +
    +

    + {x.title}{' '} + } size="small" onClick={() => selectSetting?.(x.id)} /> +

    + {x.description &&

    {x.description}

    } + + {x.component} +
    + )) + ) : ( + + )} +
    + ) +} diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx new file mode 100644 index 00000000000000..f5015ea7f56811 --- /dev/null +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -0,0 +1,355 @@ +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 { PermissionsGrid } from './organization/Permissions/PermissionsGrid' +import { + Bookmarklet, + ProjectDisplayName, + ProjectTimezone, + ProjectToolbarURLs, + 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 { 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 { SettingSection } from './types' + +export const SettingsMap: SettingSection[] = [ + // PROJECT + { + level: 'project', + id: 'project-details', + title: 'General', + settings: [ + { + id: 'display-name', + title: 'Display name', + component: , + }, + { + id: 'snippet', + title: 'Web snippet', + component: , + }, + { + id: 'bookmarklet', + title: 'Bookmarklet', + component: , + }, + { + id: 'variables', + title: 'Project ID', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-autocapture', + title: 'Autocapture', + + settings: [ + { + id: 'autocapture', + title: 'Autocapture', + component: , + }, + { + id: 'exception-autocapture', + title: 'Exception Autocapture', + component: , + }, + { + id: 'autocapture-data-attributes', + title: 'Data attributes', + component: , + }, + ], + }, + + { + level: 'project', + id: 'project-product-analytics', + title: 'Product Analytics', + settings: [ + { + id: 'date-and-time', + title: 'Date & Time', + component: , + }, + { + id: 'internal-user-filtering', + title: 'Filter our internal and test users', + component: , + }, + { + id: 'correlation-analysis', + title: 'Correlation analysis exclusions', + component: , + }, + { + id: 'person-display-name', + title: 'Person display name', + component: , + }, + { + id: 'path-cleaning', + title: 'Path cleaning rules', + component: , + }, + { + id: 'datacapture', + title: 'IP Data capture configuration', + component: , + }, + { + id: 'group-analytics', + title: 'Group Analytics', + component: , + }, + { + id: 'persons-on-events', + title: 'Persons on events (beta)', + component: , + }, + ], + }, + + { + level: 'project', + id: 'project-replay', + title: 'Session Replay', + settings: [ + { + id: 'replay', + title: 'Session Replay', + component: , + }, + { + id: 'replay-authorized-domains', + title: 'Authorized Domains for Replay', + component: , + }, + { + id: 'replay-ingestion', + title: 'Ingestion controls', + component: , + flag: 'SESSION_RECORDING_SAMPLING', + }, + ], + }, + { + level: 'project', + id: 'project-surveys', + title: 'Surveys', + settings: [ + { + id: 'surveys-interface', + title: 'Surveys web interface', + component: , + }, + ], + }, + + { + level: 'project', + id: 'project-toolbar', + title: 'Toolbar', + settings: [ + { + id: 'authorized-toolbar-urls', + title: 'Authorized Toolbar URLs', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-integrations', + title: 'Integrations', + settings: [ + { + id: 'integration-webhooks', + title: 'Webhook integration', + component: , + }, + { + id: 'integration-slack', + title: 'Slack integration', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-rbac', + title: 'Access control', + settings: [ + { + id: 'project-rbac', + title: 'Access Control', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-danger-zone', + title: 'Danger zone', + settings: [ + { + id: 'project-delete', + title: 'Delete project', + component: , + }, + ], + }, + + // ORGANIZATION + { + level: 'organization', + id: 'organization-details', + title: 'General', + settings: [ + { + id: 'organization-display-name', + title: 'Display name', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-members', + title: 'Members', + settings: [ + { + id: 'invites', + title: 'Pending Invites', + component: , + }, + { + id: 'members', + title: 'Members', + component: , + }, + { + id: 'email-members', + title: 'Notification preferences', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-authentication', + title: 'Authentication Domains & SSO', + settings: [ + { + id: 'authentication-domains', + title: 'Authentication Domains', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-rbac', + title: 'Role-based access', + flag: 'ROLE_BASED_ACCESS', + settings: [ + { + id: 'organization-rbac', + title: 'Role-based access', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-danger-zone', + title: 'Danger zone', + settings: [ + { + id: 'organization-delete', + title: 'Delete organization', + component: , + }, + ], + }, + + // USER + { + level: 'user', + id: 'user-profile', + title: 'Profile', + settings: [ + { + id: 'details', + title: 'Details', + component: , + }, + { + id: 'change-password', + title: 'Change password', + component: , + }, + { + id: '2fa', + title: 'Two-factor authentication', + component: , + }, + ], + }, + { + level: 'user', + id: 'user-api-keys', + title: 'Personal API Keys', + settings: [ + { + id: 'personal-api-keys', + title: 'Personal API keys', + component: , + }, + ], + }, + { + level: 'user', + id: 'user-notifications', + title: 'Notifications', + settings: [ + { + id: 'notifications', + title: 'Notifications', + component: , + }, + { + id: 'optout', + title: 'Anonymize Data Collection', + component: , + }, + ], + }, +] diff --git a/frontend/src/scenes/settings/SettingsScene.stories.tsx b/frontend/src/scenes/settings/SettingsScene.stories.tsx new file mode 100644 index 00000000000000..bcfb160b24badd --- /dev/null +++ b/frontend/src/scenes/settings/SettingsScene.stories.tsx @@ -0,0 +1,49 @@ +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 { urls } from 'scenes/urls' + +const meta: Meta = { + title: 'Scenes-Other/Settings', + parameters: { + layout: 'fullscreen', + viewMode: 'story', + mockDate: '2023-05-25', + }, + decorators: [ + mswDecorator({ + get: { + '/_preflight': { + ...preflightJson, + cloud: true, + realm: 'cloud', + }, + }, + }), + ], +} +export default meta + +export function SettingsProject(): JSX.Element { + useEffect(() => { + router.actions.push(urls.settings('project')) + }, []) + return +} + +export function SettingsUser(): JSX.Element { + useEffect(() => { + router.actions.push(urls.settings('user')) + }, []) + return +} + +export function SettingsOrganization(): JSX.Element { + useEffect(() => { + router.actions.push(urls.settings('organization')) + }, []) + return +} diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx new file mode 100644 index 00000000000000..29695feeefa370 --- /dev/null +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -0,0 +1,18 @@ +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 { Settings } from './Settings' + +export const scene: SceneExport = { + component: SettingsScene, + logic: settingsSceneLogic, +} + +export function SettingsScene(): JSX.Element { + const { location } = useValues(router) + useAnchor(location.hash) + + return +} diff --git a/frontend/src/scenes/organization/Settings/InviteModal.scss b/frontend/src/scenes/settings/organization/InviteModal.scss similarity index 100% rename from frontend/src/scenes/organization/Settings/InviteModal.scss rename to frontend/src/scenes/settings/organization/InviteModal.scss diff --git a/frontend/src/scenes/organization/Settings/InviteModal.tsx b/frontend/src/scenes/settings/organization/InviteModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/InviteModal.tsx rename to frontend/src/scenes/settings/organization/InviteModal.tsx diff --git a/frontend/src/scenes/organization/Settings/Invites.tsx b/frontend/src/scenes/settings/organization/Invites.tsx similarity index 92% rename from frontend/src/scenes/organization/Settings/Invites.tsx rename to frontend/src/scenes/settings/organization/Invites.tsx index 4fe50084b5b951..41be87d491cf6a 100644 --- a/frontend/src/scenes/organization/Settings/Invites.tsx +++ b/frontend/src/scenes/settings/organization/Invites.tsx @@ -97,23 +97,19 @@ export function Invites(): JSX.Element { ] return ( -
    -

    - Pending Invites - - Invite team member - -

    +
    {!preflight?.email_service_available && } + + Invite team member +
    ) } diff --git a/frontend/src/scenes/organization/Settings/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx similarity index 96% rename from frontend/src/scenes/organization/Settings/Members.tsx rename to frontend/src/scenes/settings/organization/Members.tsx index 077eeb24ab8198..ea468523c95e29 100644 --- a/frontend/src/scenes/organization/Settings/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -1,7 +1,6 @@ import { useValues, useActions } from 'kea' -import { membersLogic } from './membersLogic' import { OrganizationMembershipLevel } from 'lib/constants' -import { OrganizationMemberType, UserType } from '~/types' +import { OrganizationMemberType } from '~/types' import { organizationLogic } from 'scenes/organizationLogic' import { userLogic } from 'scenes/userLogic' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' @@ -18,11 +17,11 @@ 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 { Row } from 'antd' 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' function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element | null { const { user } = useValues(userLogic) @@ -133,18 +132,18 @@ function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element | ) } -export interface MembersProps { - /** Currently logged-in user. */ - user: UserType -} - -export function Members({ user }: MembersProps): JSX.Element { +export function Members(): JSX.Element | null { const { filteredMembers, membersLoading, search } = useValues(membersLogic) const { currentOrganization } = useValues(organizationLogic) const { setSearch } = useActions(membersLogic) const { updateOrganization } = useActions(organizationLogic) const [is2FAModalVisible, set2FAModalVisible] = useState(false) const { preflight } = useValues(preflightLogic) + const { user } = useValues(userLogic) + + if (!user) { + return null + } const columns: LemonTableColumns = [ { @@ -262,8 +261,7 @@ export function Members({ user }: MembersProps): JSX.Element { return ( <> -

    Members

    - +
    updateOrganization({ enforce_2fa })} /> - +
    + + { + e.preventDefault() + updateOrganization({ name }) + }} + disabled={isRestricted || !name || !currentOrganization || name === currentOrganization.name} + loading={currentOrganizationLoading} + > + Rename Organization + +
    + ) +} diff --git a/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx new file mode 100644 index 00000000000000..e2e4a1524e9e8d --- /dev/null +++ b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx @@ -0,0 +1,25 @@ +import { LemonSwitch } from '@posthog/lemon-ui' +import { useValues, useActions } from 'kea' +import { useRestrictedArea } from 'lib/components/RestrictedArea' +import { OrganizationMembershipLevel } from 'lib/constants' +import { organizationLogic } from 'scenes/organizationLogic' + +export function OrganizationEmailPreferences(): JSX.Element { + const { currentOrganization } = useValues(organizationLogic) + const { updateOrganization } = useActions(organizationLogic) + + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) + + return ( + { + updateOrganization({ is_member_join_email_enabled: checked }) + }} + checked={!!currentOrganization?.is_member_join_email_enabled} + disabled={isRestricted || !currentOrganization} + label="Email all current members when a new member joins" + bordered + /> + ) +} diff --git a/frontend/src/scenes/organization/Settings/DangerZone.tsx b/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx similarity index 74% rename from frontend/src/scenes/organization/Settings/DangerZone.tsx rename to frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx index a4fa581b337e90..5425d9b67c2980 100644 --- a/frontend/src/scenes/organization/Settings/DangerZone.tsx +++ b/frontend/src/scenes/settings/organization/OrganizationDangerZone.tsx @@ -1,9 +1,10 @@ import { useActions, useValues } from 'kea' import { organizationLogic } from 'scenes/organizationLogic' -import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +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' export function DeleteOrganizationModal({ isOpen, @@ -62,32 +63,29 @@ export function DeleteOrganizationModal({ ) } -export function DangerZone({ isRestricted }: RestrictedComponentProps): JSX.Element { +export function OrganizationDangerZone(): JSX.Element { const { currentOrganization } = useValues(organizationLogic) - const [isModalVisible, setIsModalVisible] = useState(false) + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) return ( <>
    -

    Danger Zone

    -
    - {!isRestricted && ( -

    - This is irreversible. Please be certain. -

    - )} - setIsModalVisible(true)} - data-attr="delete-organization-button" - icon={} - disabled={isRestricted} - > - Delete {currentOrganization?.name || 'the current organization'} - -
    + {!isRestricted && ( +

    + This is irreversible. Please be certain. +

    + )} + setIsModalVisible(true)} + data-attr="delete-organization-button" + icon={} + disabled={isRestricted} + > + Delete {currentOrganization?.name || 'the current organization'} +
    diff --git a/frontend/src/scenes/organization/Settings/Permissions/Permissions.tsx b/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx similarity index 98% rename from frontend/src/scenes/organization/Settings/Permissions/Permissions.tsx rename to frontend/src/scenes/settings/organization/Permissions/Permissions.tsx index 65213ed1284815..7adcbed6a36213 100644 --- a/frontend/src/scenes/organization/Settings/Permissions/Permissions.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx @@ -54,7 +54,7 @@ export function Permissions({ isRestricted }: RestrictedComponentProps): JSX.Ele return ( <>
    -
    +

    Permission Defaults

    diff --git a/frontend/src/scenes/organization/Settings/Permissions/PermissionsGrid.tsx b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx similarity index 73% rename from frontend/src/scenes/organization/Settings/Permissions/PermissionsGrid.tsx rename to frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx index 418510115015a4..5f97a4c778e313 100644 --- a/frontend/src/scenes/organization/Settings/Permissions/PermissionsGrid.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx @@ -2,22 +2,25 @@ 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 { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { useRestrictedArea } from 'lib/components/RestrictedArea' import { TitleWithIcon } from 'lib/components/TitleWithIcon' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { organizationLogic } from 'scenes/organizationLogic' -import { AccessLevel, Resource, RoleType } from '~/types' +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({ isRestricted }: RestrictedComponentProps): JSX.Element { +export function PermissionsGrid(): JSX.Element { const { resourceRolesAccess, organizationResourcePermissionsLoading } = useValues(permissionsLogic) const { updatePermission } = useActions(permissionsLogic) const { roles, rolesLoading } = useValues(rolesLogic) const { setRoleInFocus, openCreateRoleModal } = useActions(rolesLogic) const { isAdminOrOwner } = useValues(organizationLogic) + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) // TODO: check if this is correct const columns: LemonTableColumns = [ { @@ -95,24 +98,26 @@ export function PermissionsGrid({ isRestricted }: RestrictedComponentProps): JSX ] return ( - <> -
    -
    - Edit organizational default permission levels for posthog resources. Use roles to apply permissions - to specific sets of users. + + <> +
    +
    + Edit organizational default permission levels for posthog resources. Use roles to apply + permissions to specific sets of users. +
    + {!isRestricted && ( + + Create role + + )}
    - {!isRestricted && ( - - Create role - - )} -
    - - - + + + + ) } diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/CreateRoleModal.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/CreateRoleModal.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/Roles.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx similarity index 99% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/Roles.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx index 50ca6b37b62147..915433298a2d39 100644 --- a/frontend/src/scenes/organization/Settings/Permissions/Roles/Roles.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx @@ -58,7 +58,7 @@ export function Roles({ isRestricted }: RestrictedComponentProps): JSX.Element { return ( <>
    -
    +

    Roles

    diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/rolesLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx similarity index 98% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/rolesLogic.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx index 37acd4fa6b8070..32276a71df837d 100644 --- a/frontend/src/scenes/organization/Settings/Permissions/Roles/rolesLogic.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx @@ -2,9 +2,9 @@ import { actions, kea, reducers, path, connect, selectors, afterMount, listeners import { loaders } from 'kea-loaders' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamMembersLogic } from 'scenes/project/Settings/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']), diff --git a/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx rename to frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/utils.ts b/frontend/src/scenes/settings/organization/Permissions/utils.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/utils.ts rename to frontend/src/scenes/settings/organization/Permissions/utils.ts diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/AddDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/AddDomainModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/ConfigureSAMLModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/ConfigureSAMLModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.stories.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.stories.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx similarity index 91% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx index 62610b74d34615..548a89ad5ca5a1 100644 --- a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx @@ -5,7 +5,6 @@ import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { OrganizationDomainType } from '~/types' import { verifiedDomainsLogic } from './verifiedDomainsLogic' -import { InfoCircleOutlined } from '@ant-design/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' import { AddDomainModal } from './AddDomainModal' @@ -17,6 +16,7 @@ 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 } @@ -26,25 +26,20 @@ export function VerifiedDomains(): JSX.Element { return ( <> -
    -
    -

    - Authentication domains -

    -

    - Enable users to sign up automatically with an email address on verified domains and enforce SSO - for accounts under your domains. -

    -
    - setAddModalShown(true)} - disabled={verifiedDomainsLoading || updatingDomainLoading} - > - Add domain - -
    +

    + Enable users to sign up automatically with an email address on verified domains and enforce SSO for + accounts under your domains. +

    + + setAddModalShown(true)} + className="mt-4" + disabledReason={verifiedDomainsLoading || updatingDomainLoading ? 'loading...' : null} + > + Add domain + ) } @@ -79,7 +74,7 @@ function VerifiedDomainsTable(): JSX.Element { <> Verification - + ), @@ -111,7 +106,7 @@ function VerifiedDomainsTable(): JSX.Element { currentOrganization?.name || 'this organization' } if it does not exist.`} > - + ), @@ -138,7 +133,7 @@ function VerifiedDomainsTable(): JSX.Element { <> Enforce SSO{' '} - + ), diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifyDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/VerifyDomainModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap rename to frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.test.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.test.ts rename to frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.ts rename to frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts diff --git a/frontend/src/scenes/organization/Settings/inviteLogic.ts b/frontend/src/scenes/settings/organization/inviteLogic.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/inviteLogic.ts rename to frontend/src/scenes/settings/organization/inviteLogic.ts diff --git a/frontend/src/scenes/organization/Settings/invitesLogic.tsx b/frontend/src/scenes/settings/organization/invitesLogic.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/invitesLogic.tsx rename to frontend/src/scenes/settings/organization/invitesLogic.tsx diff --git a/frontend/src/scenes/project/Settings/AddMembersModal.tsx b/frontend/src/scenes/settings/project/AddMembersModal.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/AddMembersModal.tsx rename to frontend/src/scenes/settings/project/AddMembersModal.tsx diff --git a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx new file mode 100644 index 00000000000000..e65c24e6f76b50 --- /dev/null +++ b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx @@ -0,0 +1,99 @@ +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 { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +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) + + return ( + <> +

    + Automagically capture front-end interactions like pageviews, clicks, and more when using our web + JavaScript SDK.{' '} +

    +

    + Autocapture is also available for React Native, where it has to be{' '} + + configured directly in code + + . +

    +
    + { + updateCurrentTeam({ + autocapture_opt_out: !checked, + }) + reportIngestionAutocaptureToggled(!checked) + }} + checked={!currentTeam?.autocapture_opt_out} + disabled={userLoading} + label="Enable autocapture for web" + bordered + /> +
    + + ) +} + +export function ExceptionAutocaptureSettings(): JSX.Element { + const { userLoading } = useValues(userLogic) + const { currentTeam } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const { reportIngestionAutocaptureExceptionsToggled } = useActions(eventUsageLogic) + + const { errorsToIgnoreRules, rulesCharacters } = useValues(autocaptureExceptionsLogic) + const { setErrorsToIgnoreRules } = useActions(autocaptureExceptionsLogic) + + return ( + <> + { + updateCurrentTeam({ + autocapture_exceptions_opt_in: checked, + }) + reportIngestionAutocaptureExceptionsToggled(checked) + }} + checked={!!currentTeam?.autocapture_exceptions_opt_in} + disabled={userLoading} + label={ + <> + Enable exception autocapture ALPHA + + } + bordered + /> +

    Ignore errors

    +

    + If you're experiencing a high volume of unhelpful errors, add regular expressions here to ignore them. + This will ignore all errors that match, including those that are not autocaptured. +

    +

    + 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 + all errors that are exactly "bot", you can enter "^bot$". +

    +

    Only up to 300 characters of config are allowed here.

    + +
    300 ? 'text-danger' : 'text-muted')}> + {rulesCharacters} / 300 characters +
    + + ) +} diff --git a/frontend/src/scenes/project/Settings/CorrelationConfig.tsx b/frontend/src/scenes/settings/project/CorrelationConfig.tsx similarity index 96% rename from frontend/src/scenes/project/Settings/CorrelationConfig.tsx rename to frontend/src/scenes/settings/project/CorrelationConfig.tsx index fd357cd23e0f56..83bf547ca39135 100644 --- a/frontend/src/scenes/project/Settings/CorrelationConfig.tsx +++ b/frontend/src/scenes/settings/project/CorrelationConfig.tsx @@ -35,9 +35,6 @@ export function CorrelationConfig(): JSX.Element { return ( <> -

    - Correlation analysis exclusions -

    Globally exclude events or properties that do not provide relevant signals for your conversions.

    diff --git a/frontend/src/scenes/project/Settings/DataAttributes.tsx b/frontend/src/scenes/settings/project/DataAttributes.tsx similarity index 94% rename from frontend/src/scenes/project/Settings/DataAttributes.tsx rename to frontend/src/scenes/settings/project/DataAttributes.tsx index 4efdb3c1ca7635..52786527115195 100644 --- a/frontend/src/scenes/project/Settings/DataAttributes.tsx +++ b/frontend/src/scenes/settings/project/DataAttributes.tsx @@ -1,5 +1,4 @@ -import { LemonButton, Link } from '@posthog/lemon-ui' -import { Skeleton } from 'antd' +import { LemonButton, LemonSkeleton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { useEffect, useState } from 'react' @@ -13,7 +12,7 @@ export function DataAttributes(): JSX.Element { useEffect(() => setValue(currentTeam?.data_attributes || []), [currentTeam]) if (!currentTeam) { - return + return } return ( diff --git a/frontend/src/scenes/project/Settings/GroupAnalytics.tsx b/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx similarity index 89% rename from frontend/src/scenes/project/Settings/GroupAnalytics.tsx rename to frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx index 4d563af695bdbf..ea893db7132b74 100644 --- a/frontend/src/scenes/project/Settings/GroupAnalytics.tsx +++ b/frontend/src/scenes/settings/project/GroupAnalyticsConfig.tsx @@ -2,11 +2,11 @@ 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 { groupAnalyticsConfigLogic } from 'scenes/project/Settings/groupAnalyticsConfigLogic' -import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' +import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { groupAnalyticsConfigLogic } from './groupAnalyticsConfigLogic' -export function GroupAnalytics(): JSX.Element | null { +export function GroupAnalyticsConfig(): JSX.Element | null { const { groupTypes, groupTypesLoading, singularChanges, pluralChanges, hasChanges } = useValues(groupAnalyticsConfigLogic) const { setSingular, setPlural, reset, save } = useActions(groupAnalyticsConfigLogic) @@ -63,8 +63,7 @@ export function GroupAnalytics(): JSX.Element | null { ] return ( -
    -

    Group Analytics

    + <>

    This project has access to group analytics. Below you can configure how various group types are displayed throughout the app. @@ -91,7 +90,6 @@ export function GroupAnalytics(): JSX.Element | null { Cancel

    - -
    + ) } diff --git a/frontend/src/scenes/project/Settings/IPCapture.tsx b/frontend/src/scenes/settings/project/IPCapture.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/IPCapture.tsx rename to frontend/src/scenes/settings/project/IPCapture.tsx diff --git a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx new file mode 100644 index 00000000000000..076ac078020a0d --- /dev/null +++ b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx @@ -0,0 +1,52 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' +import { Link } from '@posthog/lemon-ui' +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) + const { currentTeam } = useValues(teamLogic) + const { user } = useValues(userLogic) + const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED) + + if (!currentTeam) { + return null + } + + if (!hasAdvancedPaths) { + return

    Advanced path cleaning is a premium feature.

    + } + + return ( + <> +

    + Make your Paths clearer by aliasing + one or multiple URLs.{' '} + + Example: http://tenant-one.mydomain.com/accounts and{' '} + http://tenant-two.mydomain.com/accounts can become a single /accounts{' '} + path. + +

    +

    + Each rule is composed of an alias and a regex pattern. Any pattern in a URL or event name that matches + the regex will be replaced with the alias. Rules are applied in the order that they're listed. +

    +

    + + Rules that you set here will be applied before wildcarding and other regex replacement if the toggle + is switched on. + +

    + { + updateCurrentTeam({ path_cleaning_filters: filters }) + }} + /> + + ) +} diff --git a/frontend/src/scenes/project/Settings/PersonDisplayNameProperties.tsx b/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/PersonDisplayNameProperties.tsx rename to frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx diff --git a/frontend/src/scenes/project/Settings/TeamMembers.tsx b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx similarity index 67% rename from frontend/src/scenes/project/Settings/TeamMembers.tsx rename to frontend/src/scenes/settings/project/ProjectAccessControl.tsx index bd2a76793aff1c..edfdb92742a044 100644 --- a/frontend/src/scenes/project/Settings/TeamMembers.tsx +++ b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx @@ -1,9 +1,10 @@ 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 { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' -import { TeamType, UserType, FusedTeamMemberType } from '~/types' +import { FusedTeamMemberType, AvailableFeature } from '~/types' import { userLogic } from 'scenes/userLogic' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' @@ -13,11 +14,14 @@ import { teamMembershipLevelIntegers, } from 'lib/utils/permissioning' import { AddMembersModalWithButton } from './AddMembersModal' -import { RestrictedArea, RestrictionScope } from 'lib/components/RestrictedArea' -import { LemonButton, LemonSelect, LemonSelectOption, LemonTable } from '@posthog/lemon-ui' +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' function LevelComponent(member: FusedTeamMemberType): JSX.Element | null { const { user } = useValues(userLogic) @@ -122,14 +126,14 @@ function ActionsComponent(member: FusedTeamMemberType): JSX.Element | null { ) : null } -export interface MembersProps { - user: UserType - team: TeamType -} - -export function TeamMembers({ user }: MembersProps): JSX.Element { +export function ProjectTeamMembers(): JSX.Element | null { + const { user } = useValues(userLogic) const { allMembers, allMembersLoading } = useValues(teamMembersLogic) + if (!user) { + return null + } + const columns: LemonTableColumns = [ { key: 'user_profile_picture', @@ -177,14 +181,14 @@ export function TeamMembers({ user }: MembersProps): JSX.Element { return ( <> -

    +

    Members with Project Access -

    + ) } + +export function ProjectAccessControl(): JSX.Element { + const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic) + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const { guardAvailableFeature } = useActions(sceneLogic) + const { hasAvailableFeature } = useValues(userLogic) + + const projectPermissioningEnabled = + hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && currentTeam?.access_control + const isRestricted = !!useRestrictedArea({ + minimumAccessLevel: OrganizationMembershipLevel.Admin, + }) + + return ( + <> +

    + {projectPermissioningEnabled ? ( + <> + This project is{' '} + + + private + + . Only members listed below are allowed to access it. + + ) : ( + <> + This project is{' '} + + + open + + . Any member of the organization can access it. To enable granular access control, make it + private. + + )} +

    + { + guardAvailableFeature( + AvailableFeature.PROJECT_BASED_PERMISSIONING, + 'project-based permissioning', + 'Set permissions granularly for each project. Make sure only the right people have access to protected data.', + () => updateCurrentTeam({ access_control: checked }) + ) + }} + checked={!!projectPermissioningEnabled} + disabled={ + isRestricted || + !currentOrganization || + !currentTeam || + currentOrganizationLoading || + currentTeamLoading + } + bordered + label="Make project private" + /> + + {currentTeam?.access_control && hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && ( + + )} + + ) +} diff --git a/frontend/src/scenes/project/Settings/DangerZone.tsx b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx similarity index 87% rename from frontend/src/scenes/project/Settings/DangerZone.tsx rename to frontend/src/scenes/settings/project/ProjectDangerZone.tsx index d188618bdd57f9..82dd00ba309ef9 100644 --- a/frontend/src/scenes/project/Settings/DangerZone.tsx +++ b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx @@ -1,10 +1,11 @@ import { Dispatch, SetStateAction, useState } from 'react' import { useActions, useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' -import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' import { IconDelete } from 'lib/lemon-ui/icons' import { TeamType } from '~/types' +import { OrganizationMembershipLevel } from 'lib/constants' export function DeleteProjectModal({ isOpen, @@ -59,17 +60,20 @@ export function DeleteProjectModal({ ) } -export function DangerZone({ isRestricted }: RestrictedComponentProps): JSX.Element { +export function ProjectDangerZone(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const [isModalVisible, setIsModalVisible] = useState(false) + const restrictedReason = useRestrictedArea({ + minimumAccessLevel: OrganizationMembershipLevel.Admin, + scope: RestrictionScope.Project, + }) + return ( <>
    -

    Danger Zone

    - {!isRestricted && ( + {!restrictedReason && (

    This is irreversible. Please be certain.

    @@ -80,7 +84,7 @@ export function DangerZone({ isRestricted }: RestrictedComponentProps): JSX.Elem onClick={() => setIsModalVisible(true)} data-attr="delete-project-button" icon={} - disabled={isRestricted} + disabledReason={restrictedReason} > Delete {currentTeam?.name || 'the current project'} diff --git a/frontend/src/scenes/settings/project/ProjectSettings.tsx b/frontend/src/scenes/settings/project/ProjectSettings.tsx new file mode 100644 index 00000000000000..ac6d40c2849a57 --- /dev/null +++ b/frontend/src/scenes/settings/project/ProjectSettings.tsx @@ -0,0 +1,181 @@ +import { useActions, useValues } from 'kea' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { JSSnippet } from 'lib/components/JSSnippet' +import { JSBookmarklet } from 'lib/components/JSBookmarklet' +import { CodeSnippet } from 'lib/components/CodeSnippet' +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 { 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) + const { updateCurrentTeam } = useActions(teamLogic) + + const [name, setName] = useState(currentTeam?.name || '') + + if (currentTeam?.is_demo) { + return ( +

    + The demo project cannot be renamed. +

    + ) + } + + return ( +
    + + updateCurrentTeam({ name })} + disabled={!name || !currentTeam || name === currentTeam.name} + loading={currentTeamLoading} + > + Rename Project + +
    + ) +} + +export function WebSnippet(): JSX.Element { + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + + return ( + <> +

    + PostHog's configurable web snippet allows you to (optionally) autocapture events, record user sessions, + and more with no extra work. Place the following snippet in your website's HTML, ideally just above the{' '} + {''} tag. +

    +

    + For more guidance, including on identifying users,{' '} + see PostHog Docs. +

    + {currentTeamLoading && !currentTeam ? ( +
    + + +
    + ) : ( + + )} + + ) +} + +export function Bookmarklet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + <> +

    Need to test PostHog on a live site without changing any code?

    +

    + Just drag the bookmarklet below to your bookmarks bar, open the website you want to test PostHog on and + click it. This will enable our tracking, on the currently loaded page only. The data will show up in + this project. +

    +
    {isAuthenticatedTeam(currentTeam) && }
    + + ) +} + +export function ProjectVariables(): JSX.Element { + const { currentTeam, isTeamTokenResetAvailable } = useValues(teamLogic) + const { resetToken } = useActions(teamLogic) + + return ( +
    +
    +

    + Project API Key +

    +

    + You can use this write-only key in any one of{' '} + our libraries. +

    + , + title: 'Reset project API key', + popconfirmProps: { + title: ( + <> + Reset the project's API key?{' '} + This will invalidate the current API key and cannot be undone. + + ), + okText: 'Reset key', + okType: 'danger', + placement: 'left', + }, + callback: resetToken, + }, + ] + : [] + } + thing="project API key" + > + {currentTeam?.api_token || ''} + +

    + Write-only means it can only create new events. It can't read events or any of your other data + stored with PostHog, so it's safe to use in public apps. +

    +
    +
    +

    + Project ID +

    +

    + You can use this ID to reference your project in our{' '} + API. +

    + {String(currentTeam?.id || '')} +
    +
    + ) +} + +export function ProjectTimezone(): JSX.Element { + return ( + <> +

    + These settings affect how PostHog displays, buckets, and filters time-series data. You may need to + refresh insights for new settings to apply. +

    +
    + Time zone + + Week starts on + +
    + + ) +} + +export function ProjectToolbarURLs(): JSX.Element { + return ( + <> +

    + These are the URLs where the{' '} + + Toolbar will automatically launch + {' '} + (if you're logged in). +

    +

    + Domains and wildcard subdomains are allowed (example: https://*.example.com). + However, wildcarded top-level domains cannot be used (for security reasons). +

    + + + ) +} diff --git a/frontend/src/scenes/session-recordings/settings/SessionRecordingSettings.tsx b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx similarity index 84% rename from frontend/src/scenes/session-recordings/settings/SessionRecordingSettings.tsx rename to frontend/src/scenes/settings/project/SessionRecordingSettings.tsx index 7ff2582193790d..4b19878b74146f 100644 --- a/frontend/src/scenes/session-recordings/settings/SessionRecordingSettings.tsx +++ b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx @@ -1,30 +1,117 @@ import { useActions, useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' -import { LemonBanner, LemonButton, LemonSelect, LemonSwitch, LemonTag, Link } from '@posthog/lemon-ui' +import { LemonBanner, LemonButton, LemonSelect, LemonSwitch, Link } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' -import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' import { IconCancel } from 'lib/lemon-ui/icons' import { FlagSelector } from 'lib/components/FlagSelector' -export type SessionRecordingSettingsProps = { - inModal?: boolean +export function ReplayGeneral(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + + const { currentTeam } = useValues(teamLogic) + + return ( +
    +

    Watch recordings of how users interact with your web app to see what can be improved.

    + +
    + { + updateCurrentTeam({ + session_recording_opt_in: checked, + capture_console_log_opt_in: checked, + capture_performance_opt_in: checked, + }) + }} + label="Record user sessions" + bordered + checked={!!currentTeam?.session_recording_opt_in} + /> + +

    + Please note your website needs to have the{' '} + PostHog snippet or the latest version of{' '} + + posthog-js + {' '} + directly installed. For more details, check out our{' '} + + docs + + . +

    +
    +
    + { + updateCurrentTeam({ capture_console_log_opt_in: checked }) + }} + label="Capture console logs" + bordered + checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_console_log_opt_in : false} + disabled={!currentTeam?.session_recording_opt_in} + /> +

    + This setting controls if browser console logs will be captured as a part of recordings. The console + logs will be shown in the recording player to help you debug any issues. +

    +
    +
    + { + updateCurrentTeam({ capture_performance_opt_in: checked }) + }} + label="Capture network performance" + bordered + checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_performance_opt_in : false} + disabled={!currentTeam?.session_recording_opt_in} + /> +

    + This setting controls if performance and network information will be captured alongside recordings. + The network requests and timings will be shown in the recording player to help you debug any issues. +

    +
    +
    + ) } -function ReplayCostControl(): JSX.Element { +export function ReplayAuthorizedDomains(): JSX.Element { + return ( +
    +

    + Use the settings below to restrict the domains where recordings will be captured. If no domains are + selected, then there will be no domain restriction. +

    +

    + Domains and wildcard subdomains are allowed (e.g. https://*.example.com). However, + wildcarded top-level domains cannot be used (for security reasons). +

    + +
    + ) +} + +export function ReplayCostControl(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) const { currentTeam } = useValues(teamLogic) return ( <> -

    - Replay ingestion controls BETA -

    PostHog offers several tools to let you control the number of recordings you collect and which users you collect recordings for.{' '} @@ -35,7 +122,9 @@ function ReplayCostControl(): JSX.Element { Learn more in our docs

    - Requires posthog-js version 1.85.0 or greater + + Requires posthog-js version 1.88.2 or greater +
    Sampling ) } - -export function SessionRecordingSettings({ inModal = false }: SessionRecordingSettingsProps): JSX.Element { - const { updateCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - - return ( -
    -
    - { - updateCurrentTeam({ - session_recording_opt_in: checked, - capture_console_log_opt_in: checked, - capture_performance_opt_in: checked, - }) - }} - label="Record user sessions" - bordered={!inModal} - fullWidth={inModal} - labelClassName={inModal ? 'text-base font-semibold' : ''} - checked={!!currentTeam?.session_recording_opt_in} - /> - -

    - Please note your website needs to have the{' '} - PostHog snippet or the latest version of{' '} - - posthog-js - {' '} - directly installed. For more details, check out our{' '} - - docs - - . -

    -
    -
    - { - updateCurrentTeam({ capture_console_log_opt_in: checked }) - }} - label="Capture console logs" - labelClassName={inModal ? 'text-base font-semibold' : ''} - bordered={!inModal} - fullWidth={inModal} - checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_console_log_opt_in : false} - disabled={!currentTeam?.session_recording_opt_in} - /> -

    - This setting controls if browser console logs will be captured as a part of recordings. The console - logs will be shown in the recording player to help you debug any issues. -

    -
    -
    - { - updateCurrentTeam({ capture_performance_opt_in: checked }) - }} - label="Capture network performance" - labelClassName={inModal ? 'text-base font-semibold' : ''} - bordered={!inModal} - fullWidth={inModal} - checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_performance_opt_in : false} - disabled={!currentTeam?.session_recording_opt_in} - /> -

    - This setting controls if performance and network information will be captured alongside recordings. - The network requests and timings will be shown in the recording player to help you debug any issues. -

    -
    -
    - Authorized domains for recordings - -

    - Use the settings below to restrict the domains where recordings will be captured. If no domains are - selected, then there will be no domain restriction. -

    -

    - Domains and wildcard subdomains are allowed (e.g. https://*.example.com). However, - wildcarded top-level domains cannot be used (for security reasons). -

    - -
    - -
    - ) -} - -export function openSessionRecordingSettingsDialog(): void { - LemonDialog.open({ - title: 'Session recording settings', - content: , - width: 600, - primaryButton: { - children: 'Done', - }, - }) -} diff --git a/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx b/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx new file mode 100644 index 00000000000000..031def32411097 --- /dev/null +++ b/frontend/src/scenes/settings/project/SettingPersonsOnEvents.tsx @@ -0,0 +1,37 @@ +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) + const { currentTeam } = useValues(teamLogic) + + return ( + <> +

    + We have updated our data model to also store person properties directly on events, making queries + significantly faster. This means that person properties will no longer be "timeless", but rather + point-in-time i.e. on filters we'll consider a person's properties at the time of the event, rather than + at present time. This may cause data to change on some of your insights, but will be the default way we + handle person properties going forward. For now, you can control whether you want this on or not, and + should feel free to let us know of any concerns you might have. If you do enable this, you should see + speed improvements of around 3-5x on average on most of your insights. + + More info. + +

    + + { + updateCurrentTeam({ + extra_settings: { ...currentTeam?.extra_settings, ['poe_v2_enabled']: checked }, + }) + }} + label={`Enable Person on Events (Beta)`} + checked={!!currentTeam?.extra_settings?.['poe_v2_enabled']} + bordered + /> + + ) +} diff --git a/frontend/src/scenes/project/Settings/SlackIntegration.stories.tsx b/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/SlackIntegration.stories.tsx rename to frontend/src/scenes/settings/project/SlackIntegration.stories.tsx diff --git a/frontend/src/scenes/project/Settings/SlackIntegration.tsx b/frontend/src/scenes/settings/project/SlackIntegration.tsx similarity index 98% rename from frontend/src/scenes/project/Settings/SlackIntegration.tsx rename to frontend/src/scenes/settings/project/SlackIntegration.tsx index 93732b838cbf85..e74ef84a8fa011 100644 --- a/frontend/src/scenes/project/Settings/SlackIntegration.tsx +++ b/frontend/src/scenes/settings/project/SlackIntegration.tsx @@ -47,9 +47,9 @@ export function SlackIntegration(): JSX.Element { .

    -

    +

    {slackIntegration ? ( -
    +
    @@ -120,7 +120,7 @@ export function SlackIntegration(): JSX.Element { configure it.

    )} -

    +
    ) } diff --git a/frontend/src/scenes/project/Settings/Survey.tsx b/frontend/src/scenes/settings/project/SurveySettings.tsx similarity index 70% rename from frontend/src/scenes/project/Settings/Survey.tsx rename to frontend/src/scenes/settings/project/SurveySettings.tsx index a33f4cdd9cb134..81d536ece76a80 100644 --- a/frontend/src/scenes/project/Settings/Survey.tsx +++ b/frontend/src/scenes/settings/project/SurveySettings.tsx @@ -1,20 +1,16 @@ -import { LemonDivider, Link } from '@posthog/lemon-ui' +import { Link } from '@posthog/lemon-ui' import { SurveySettings as BasicSurveySettings } from 'scenes/surveys/SurveySettings' import { urls } from 'scenes/urls' export function SurveySettings(): JSX.Element { return ( <> -

    - Surveys -

    Get qualitative and quantitative data on how your users are doing. Surveys are found in the{' '} surveys page.

    - ) } diff --git a/frontend/src/scenes/project/Settings/TestAccountFiltersConfig.tsx b/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx similarity index 67% rename from frontend/src/scenes/project/Settings/TestAccountFiltersConfig.tsx rename to frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx index f905ce9759a2fe..58cd23f6288e1d 100644 --- a/frontend/src/scenes/project/Settings/TestAccountFiltersConfig.tsx +++ b/frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx @@ -5,11 +5,11 @@ import { teamLogic } from 'scenes/teamLogic' import { AnyPropertyFilter } from '~/types' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { groupsModel } from '~/models/groupsModel' -import { LemonSwitch } from '@posthog/lemon-ui' +import { LemonSwitch, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { filterTestAccountsDefaultsLogic } from './filterTestAccountDefaultsLogic' -export function TestAccountFiltersConfig(): JSX.Element { +function TestAccountFiltersConfig(): JSX.Element { const { updateCurrentTeam } = useActions(teamLogic) const { setTeamDefault } = useActions(filterTestAccountsDefaultsLogic) const { reportTestAccountFiltersUpdated } = useActions(eventUsageLogic) @@ -83,3 +83,42 @@ export function TestAccountFiltersConfig(): JSX.Element {
    ) } + +export function ProjectAccountFiltersSetting(): JSX.Element { + return ( + <> +

    + Increase the quality of your analytics results by filtering out events from internal sources, such as + team members, test accounts, or development environments.{' '} + The filters you apply here are added as extra filters when the toggle is switched on.{' '} + So, if you apply a cohort, it means you will only match users in that cohort. +

    + + Events and recordings will still be ingested and saved, but they will be excluded from any queries where + the "Filter out internal and test users" toggle is set. You can learn how to{' '} + + capture fewer events + {' '} + or how to{' '} + + capture fewer recordings + {' '} + in our docs. + +
    + Example filters +
      +
    • + "Email does not contain yourcompany.com" to exclude all events + from your company's team members. +
    • +
    • + "Host does not contain localhost" to exclude all events from + local development environments. +
    • +
    +
    + + + ) +} diff --git a/frontend/src/scenes/project/Settings/TimezoneConfig.tsx b/frontend/src/scenes/settings/project/TimezoneConfig.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/TimezoneConfig.tsx rename to frontend/src/scenes/settings/project/TimezoneConfig.tsx diff --git a/frontend/src/scenes/project/Settings/WebhookIntegration.tsx b/frontend/src/scenes/settings/project/WebhookIntegration.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/WebhookIntegration.tsx rename to frontend/src/scenes/settings/project/WebhookIntegration.tsx diff --git a/frontend/src/scenes/project/Settings/WeekStartConfig.tsx b/frontend/src/scenes/settings/project/WeekStartConfig.tsx similarity index 100% rename from frontend/src/scenes/project/Settings/WeekStartConfig.tsx rename to frontend/src/scenes/settings/project/WeekStartConfig.tsx diff --git a/frontend/src/scenes/project/Settings/autocaptureExceptionsLogic.ts b/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts similarity index 85% rename from frontend/src/scenes/project/Settings/autocaptureExceptionsLogic.ts rename to frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts index d2fa99e1729dfb..9aa7a465bdb0c3 100644 --- a/frontend/src/scenes/project/Settings/autocaptureExceptionsLogic.ts +++ b/frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts @@ -1,20 +1,20 @@ -import { kea, reducers, actions, listeners, events, selectors, connect, path } from 'kea' +import { kea, reducers, actions, listeners, selectors, connect, path, afterMount } from 'kea' import { teamLogic } from 'scenes/teamLogic' import type { autocaptureExceptionsLogicType } from './autocaptureExceptionsLogicType' export const autocaptureExceptionsLogic = kea([ path(['scenes', 'project', 'Settings', 'autocaptureExceptionsLogic']), - connect({ + connect(() => ({ values: [teamLogic, ['currentTeam']], actions: [teamLogic, ['updateCurrentTeam']], - }), + })), actions({ setErrorsToIgnoreRules: (newRules: string) => ({ newRules }), }), reducers({ errorsToIgnoreRules: [ - (teamLogic.values.currentTeam?.autocapture_exceptions_errors_to_ignore || []).join('\n'), + '', { setErrorsToIgnoreRules: (_, { newRules }) => newRules, }, @@ -45,5 +45,7 @@ export const autocaptureExceptionsLogic = kea([ }) }, })), - events(() => ({})), + afterMount(({ actions, values }) => { + actions.setErrorsToIgnoreRules(values.currentTeamErrorsToIgnoreRules) + }), ]) diff --git a/frontend/src/scenes/project/Settings/filterTestAccountDefaultsLogic.ts b/frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts similarity index 100% rename from frontend/src/scenes/project/Settings/filterTestAccountDefaultsLogic.ts rename to frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts diff --git a/frontend/src/scenes/project/Settings/groupAnalyticsConfigLogic.ts b/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts similarity index 100% rename from frontend/src/scenes/project/Settings/groupAnalyticsConfigLogic.ts rename to frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts diff --git a/frontend/src/scenes/project/Settings/integrationsLogic.ts b/frontend/src/scenes/settings/project/integrationsLogic.ts similarity index 98% rename from frontend/src/scenes/project/Settings/integrationsLogic.ts rename to frontend/src/scenes/settings/project/integrationsLogic.ts index 56cfa47d330f30..be287440db304b 100644 --- a/frontend/src/scenes/project/Settings/integrationsLogic.ts +++ b/frontend/src/scenes/settings/project/integrationsLogic.ts @@ -97,7 +97,7 @@ export const integrationsLogic = kea([ case 'slack': { const { state, code, error, next } = searchParams - const replaceUrl = next || urls.projectSettings() + const replaceUrl = next || urls.settings('project') if (error) { lemonToast.error(`Failed due to "${error}"`) diff --git a/frontend/src/scenes/project/Settings/teamMembersLogic.tsx b/frontend/src/scenes/settings/project/teamMembersLogic.tsx similarity index 99% rename from frontend/src/scenes/project/Settings/teamMembersLogic.tsx rename to frontend/src/scenes/settings/project/teamMembersLogic.tsx index 67455ac28c39fe..d6d137b48a2be2 100644 --- a/frontend/src/scenes/project/Settings/teamMembersLogic.tsx +++ b/frontend/src/scenes/settings/project/teamMembersLogic.tsx @@ -13,11 +13,11 @@ import { UserType, } from '~/types' import type { teamMembersLogicType } from './teamMembersLogicType' -import { membersLogic } from '../../organization/Settings/membersLogic' 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' export const MINIMUM_IMPLICIT_ACCESS_LEVEL = OrganizationMembershipLevel.Admin diff --git a/frontend/src/scenes/project/Settings/webhookIntegrationLogic.ts b/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts similarity index 100% rename from frontend/src/scenes/project/Settings/webhookIntegrationLogic.ts rename to frontend/src/scenes/settings/project/webhookIntegrationLogic.ts diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts new file mode 100644 index 00000000000000..665b7d27a4c7fc --- /dev/null +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -0,0 +1,103 @@ +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 { 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 +} + +export const settingsLogic = kea([ + props({} as SettingsLogicProps), + key((props) => props.logicKey ?? 'global'), + path((key) => ['scenes', 'settings', 'settingsLogic', key]), + connect({ + values: [featureFlagLogic, ['featureFlags']], + }), + + actions({ + selectSection: (section: SettingSectionId) => ({ section }), + selectLevel: (level: SettingLevelId) => ({ level }), + selectSetting: (setting: string) => ({ setting }), + openCompactNavigation: true, + closeCompactNavigation: true, + }), + + reducers(({ props }) => ({ + selectedLevel: [ + (props.settingLevelId ?? 'project') as SettingLevelId, + { + selectLevel: (_, { level }) => level, + selectSection: (_, { section }) => SettingsMap.find((x) => x.id === section)?.level || 'user', + }, + ], + selectedSectionId: [ + (props.sectionId ?? null) as SettingSectionId | null, + { + selectLevel: () => null, + selectSection: (_, { section }) => section, + }, + ], + + isCompactNavigationOpen: [ + false, + { + openCompactNavigation: () => true, + closeCompactNavigation: () => false, + selectLevel: () => false, + selectSection: () => false, + }, + ], + })), + + selectors({ + sections: [ + (s) => [s.featureFlags], + (featureFlags): SettingSection[] => { + return SettingsMap.filter((x) => (x.flag ? featureFlags[FEATURE_FLAGS[x.flag]] : true)) + }, + ], + selectedSection: [ + (s) => [s.sections, s.selectedSectionId], + (sections, selectedSectionId): SettingSection | null => { + return sections.find((x) => x.id === selectedSectionId) ?? null + }, + ], + settings: [ + (s) => [s.selectedLevel, s.selectedSectionId, s.sections, s.featureFlags], + (selectedLevel, selectedSectionId, sections, featureFlags): Setting[] => { + let settings: Setting[] = [] + + if (!selectedSectionId) { + settings = sections + .filter((section) => section.level === selectedLevel) + .reduce((acc, section) => [...acc, ...section.settings], [] as Setting[]) + } else { + settings = sections.find((x) => x.id === selectedSectionId)?.settings || [] + } + + return settings.filter((x) => (x.flag ? featureFlags[FEATURE_FLAGS[x.flag]] : true)) + }, + ], + }), + + listeners(({ values }) => ({ + selectSetting({ setting }) { + const url = urls.settings(values.selectedSectionId ?? values.selectedLevel, setting as SettingId) + + copyToClipboard(window.location.origin + url) + }, + })), +]) diff --git a/frontend/src/scenes/settings/settingsSceneLogic.ts b/frontend/src/scenes/settings/settingsSceneLogic.ts new file mode 100644 index 00000000000000..ecd2b85d06e3f0 --- /dev/null +++ b/frontend/src/scenes/settings/settingsSceneLogic.ts @@ -0,0 +1,71 @@ +import { connect, kea, path, selectors } from 'kea' +import { SettingsMap } from './SettingsMap' + +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { actionToUrl, urlToAction } from 'kea-router' +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' + +export const settingsSceneLogic = kea([ + path(['scenes', 'settings', 'settingsSceneLogic']), + connect(() => ({ + values: [ + featureFlagLogic, + ['featureFlags'], + settingsLogic({ logicKey: 'settingsScene' }), + ['selectedLevel', 'selectedSectionId', 'sections', 'settings'], + ], + actions: [settingsLogic({ logicKey: 'settingsScene' }), ['selectLevel', 'selectSection', 'selectSetting']], + })), + + selectors({ + breadcrumbs: [ + (s) => [s.selectedLevel, s.selectedSectionId, s.sections], + (selectedLevel, selectedSectionId): Breadcrumb[] => [ + { + name: `Settings`, + path: urls.settings('project'), + }, + { + name: selectedSectionId + ? SettingsMap.find((x) => x.id === selectedSectionId)?.title + : capitalizeFirstLetter(selectedLevel), + }, + ], + ], + }), + + urlToAction(({ actions, values }) => ({ + '/settings/:section': ({ section }) => { + if (!section) { + return + } + if (SettingLevelIds.includes(section as SettingLevelId)) { + if (section !== values.selectedLevel) { + actions.selectLevel(section as SettingLevelId) + } + } else if (section !== values.selectedSectionId) { + actions.selectSection(section as SettingSectionId) + } + }, + })), + + actionToUrl(({ values }) => ({ + selectLevel({ level }) { + return [urls.settings(level)] + }, + selectSection({ section }) { + return [urls.settings(section)] + }, + selectSetting({ setting }) { + const url = urls.settings(values.selectedSectionId ?? values.selectedLevel, setting) + + return [url] + }, + })), +]) diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts new file mode 100644 index 00000000000000..30ee8324d0ebe7 --- /dev/null +++ b/frontend/src/scenes/settings/types.ts @@ -0,0 +1,80 @@ +import { FEATURE_FLAGS } from 'lib/constants' +import { EitherMembershipLevel } from 'lib/utils/permissioning' + +export type SettingLevelId = 'user' | 'project' | 'organization' +export const SettingLevelIds: SettingLevelId[] = ['project', 'organization', 'user'] + +export type SettingSectionId = + | 'project-details' + | 'project-autocapture' + | 'project-product-analytics' + | 'project-replay' + | 'project-surveys' + | 'project-toolbar' + | 'project-integrations' + | 'project-rbac' + | 'project-danger-zone' + | 'organization-details' + | 'organization-members' + | 'organization-authentication' + | 'organization-rbac' + | 'organization-danger-zone' + | 'user-profile' + | 'user-api-keys' + | 'user-notifications' + +export type SettingId = + | 'display-name' + | 'snippet' + | 'bookmarklet' + | 'variables' + | 'autocapture' + | 'exception-autocapture' + | 'autocapture-data-attributes' + | 'date-and-time' + | 'internal-user-filtering' + | 'correlation-analysis' + | 'person-display-name' + | 'path-cleaning' + | 'datacapture' + | 'group-analytics' + | 'persons-on-events' + | 'replay' + | 'replay-authorized-domains' + | 'replay-ingestion' + | 'surveys-interface' + | 'authorized-toolbar-urls' + | 'integration-webhooks' + | 'integration-slack' + | 'project-rbac' + | 'project-delete' + | 'organization-display-name' + | 'invites' + | 'members' + | 'email-members' + | 'authentication-domains' + | 'organization-rbac' + | 'organization-delete' + | 'details' + | 'change-password' + | '2fa' + | 'personal-api-keys' + | 'notifications' + | 'optout' + +export type Setting = { + id: SettingId + title: string + description?: JSX.Element | string + component: JSX.Element + flag?: keyof typeof FEATURE_FLAGS +} + +export type SettingSection = { + id: SettingSectionId + title: string + level: SettingLevelId + settings: Setting[] + flag?: keyof typeof FEATURE_FLAGS + minimumAccessLevel?: EitherMembershipLevel +} diff --git a/frontend/src/scenes/me/Settings/ChangePassword.tsx b/frontend/src/scenes/settings/user/ChangePassword.tsx similarity index 100% rename from frontend/src/scenes/me/Settings/ChangePassword.tsx rename to frontend/src/scenes/settings/user/ChangePassword.tsx diff --git a/frontend/src/scenes/me/Settings/OptOutCapture.tsx b/frontend/src/scenes/settings/user/OptOutCapture.tsx similarity index 73% rename from frontend/src/scenes/me/Settings/OptOutCapture.tsx rename to frontend/src/scenes/settings/user/OptOutCapture.tsx index b4cd0ce8b4ccff..4290928ebb5d1b 100644 --- a/frontend/src/scenes/me/Settings/OptOutCapture.tsx +++ b/frontend/src/scenes/settings/user/OptOutCapture.tsx @@ -1,6 +1,6 @@ import { useActions, useValues } from 'kea' -import { Switch } from 'antd' import { userLogic } from 'scenes/userLogic' +import { LemonSwitch } from '@posthog/lemon-ui' export function OptOutCapture(): JSX.Element { const { user, userLoading } = useValues(userLogic) @@ -16,21 +16,14 @@ export function OptOutCapture(): JSX.Element { We also understand there are many reasons why people don't want to or aren't allowed to send this usage data. If you would like to anonymize your personal usage data, just tick the box below.

    - updateUser({ anonymize_data: checked })} - defaultChecked={user?.anonymize_data} - loading={userLoading} + checked={user?.anonymize_data ?? false} disabled={userLoading} + bordered /> -
    ) } diff --git a/frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx similarity index 56% rename from frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx rename to frontend/src/scenes/settings/user/PersonalAPIKeys.tsx index 1df2bf94370186..f9f79327e1ba4a 100644 --- a/frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx +++ b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx @@ -1,14 +1,11 @@ -import { useState, useCallback, Dispatch, SetStateAction } from 'react' -import { Table, Popconfirm } from 'antd' +import { useState, useCallback, Dispatch, SetStateAction, useEffect } from 'react' import { useActions, useValues } from 'kea' -import { ExclamationCircleOutlined } from '@ant-design/icons' import { personalAPIKeysLogic } from './personalAPIKeysLogic' import { PersonalAPIKeyType } from '~/types' import { humanFriendlyDetailedTime } from 'lib/utils' -import { CopyToClipboardInline } from '../CopyToClipboard' -import { ColumnsType } from 'antd/lib/table' +import { CopyToClipboardInline } from '../../../lib/components/CopyToClipboard' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonInput, LemonModal, Link } from '@posthog/lemon-ui' +import { LemonDialog, LemonInput, LemonModal, LemonTable, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconPlus } from 'lib/lemon-ui/icons' @@ -71,79 +68,75 @@ function CreateKeyModal({ ) } -function RowValue(value: string): JSX.Element { - return value ? ( - {value} - ) : ( - secret - ) -} - -function RowActionsCreator( - deleteKey: (key: PersonalAPIKeyType) => void -): (personalAPIKey: PersonalAPIKeyType) => JSX.Element { - return function RowActions(personalAPIKey: PersonalAPIKeyType) { - return ( - } - placement="left" - onConfirm={() => { - deleteKey(personalAPIKey) - }} - > - Danger - - ) - } -} - function PersonalAPIKeysTable(): JSX.Element { const { keys } = useValues(personalAPIKeysLogic) as { keys: PersonalAPIKeyType[] } - const { deleteKey } = useActions(personalAPIKeysLogic) + const { deleteKey, loadKeys } = useActions(personalAPIKeysLogic) - const columns: ColumnsType> = [ - { - title: 'Label', - dataIndex: 'label', - key: 'label', - }, - { - title: 'Value', - dataIndex: 'value', - key: 'value', - className: 'ph-no-capture', - render: RowValue, - }, - { - title: 'Last Used', - dataIndex: 'last_used_at', - key: 'lastUsedAt', - render: (lastUsedAt: string | null) => humanFriendlyDetailedTime(lastUsedAt, 'MMMM DD, YYYY', 'h A'), - }, - { - title: 'Created', - dataIndex: 'created_at', - key: 'createdAt', - render: (createdAt: string | null) => humanFriendlyDetailedTime(createdAt), - }, - { - title: '', - key: 'actions', - align: 'center', - render: RowActionsCreator(deleteKey), - }, - ] + useEffect(() => loadKeys(), []) return ( - {`${value}`} + ) : ( + secret + ) + }, + }, + { + title: 'Last Used', + dataIndex: 'last_used_at', + key: 'lastUsedAt', + render: (_, key) => humanFriendlyDetailedTime(key.last_used_at, 'MMMM DD, YYYY', 'h A'), + }, + { + title: 'Created', + dataIndex: 'created_at', + key: 'createdAt', + render: (_, key) => humanFriendlyDetailedTime(key.created_at), + }, + { + title: '', + key: 'actions', + align: 'right', + width: 0, + render: (_, key) => { + return ( + { + LemonDialog.open({ + title: `Permanently delete key "${key.label}"?`, + description: 'This action cannot be undone.', + primaryButton: { + status: 'danger', + children: 'Permanently delete', + onClick: () => deleteKey(key), + }, + }) + }} + > + Delete + + ) + }, + }, + ]} /> ) } @@ -174,8 +167,10 @@ export function PersonalAPIKeys(): JSX.Element { > Create personal API key - + + + ) } diff --git a/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx similarity index 96% rename from frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx rename to frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx index b31e3d92da0b15..a7d71f1bb3d74b 100644 --- a/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx +++ b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx @@ -4,7 +4,7 @@ import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { IconCheckmark, IconWarning } from 'lib/lemon-ui/icons' import { useState } from 'react' import { Setup2FA } from 'scenes/authentication/Setup2FA' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/organization/membersLogic' export function TwoFactorAuthentication(): JSX.Element { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/me/Settings/UpdateEmailPreferences.tsx b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx similarity index 96% rename from frontend/src/scenes/me/Settings/UpdateEmailPreferences.tsx rename to frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx index 4849e56ebb8dc0..5d1387d3763240 100644 --- a/frontend/src/scenes/me/Settings/UpdateEmailPreferences.tsx +++ b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx @@ -16,7 +16,6 @@ export function UpdateEmailPreferences(): JSX.Element { checked={user?.email_opt_in || false} disabled={userLoading} label="Receive security and feature updates via email. You can easily unsubscribe at any time." - fullWidth bordered />
    @@ -35,7 +34,6 @@ export function UpdateEmailPreferences(): JSX.Element { }} checked={user?.notification_settings.plugin_disabled || false} disabled={userLoading} - fullWidth bordered label="Get notified when plugins are disabled due to errors." /> diff --git a/frontend/src/scenes/me/Settings/UserDetails.tsx b/frontend/src/scenes/settings/user/UserDetails.tsx similarity index 100% rename from frontend/src/scenes/me/Settings/UserDetails.tsx rename to frontend/src/scenes/settings/user/UserDetails.tsx diff --git a/frontend/src/scenes/me/Settings/changePasswordLogic.ts b/frontend/src/scenes/settings/user/changePasswordLogic.ts similarity index 100% rename from frontend/src/scenes/me/Settings/changePasswordLogic.ts rename to frontend/src/scenes/settings/user/changePasswordLogic.ts diff --git a/frontend/src/lib/components/PersonalAPIKeys/personalAPIKeysLogic.ts b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts similarity index 92% rename from frontend/src/lib/components/PersonalAPIKeys/personalAPIKeysLogic.ts rename to frontend/src/scenes/settings/user/personalAPIKeysLogic.ts index ec44ba725b3773..f500ebe5e05535 100644 --- a/frontend/src/lib/components/PersonalAPIKeys/personalAPIKeysLogic.ts +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts @@ -1,5 +1,5 @@ import { loaders } from 'kea-loaders' -import { kea, path, listeners, events } from 'kea' +import { kea, path, listeners } from 'kea' import api from 'lib/api' import { PersonalAPIKeyType } from '~/types' import type { personalAPIKeysLogicType } from './personalAPIKeysLogicType' @@ -37,7 +37,4 @@ export const personalAPIKeysLogic = kea([ lemonToast.success(`Personal API key deleted`) }, })), - events(({ actions }) => ({ - afterMount: [actions.loadKeys], - })), ]) diff --git a/frontend/src/scenes/surveys/EditSurvey.scss b/frontend/src/scenes/surveys/EditSurvey.scss index a504bf234d4fcf..2995d066678872 100644 --- a/frontend/src/scenes/surveys/EditSurvey.scss +++ b/frontend/src/scenes/surveys/EditSurvey.scss @@ -7,7 +7,3 @@ background: var(--border-light); } } - -.SurveyFormAppearance { - max-width: 320px; -} diff --git a/frontend/src/scenes/surveys/SurveyAppearance.scss b/frontend/src/scenes/surveys/SurveyAppearance.scss index 5c2e1081e8fd58..640e99ec703e00 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.scss +++ b/frontend/src/scenes/surveys/SurveyAppearance.scss @@ -158,6 +158,7 @@ .multiple-choice-options { margin-top: 13px; font-size: 14px; + color: black; } .multiple-choice-options .choice-option { display: flex; diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index af90fcad98b518..3f1fa5c4074b7a 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -23,8 +23,8 @@ import { import { surveysLogic } from './surveysLogic' import { useValues } from 'kea' import React, { useEffect, useRef, useState } from 'react' -import { sanitize } from 'dompurify' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { sanitizeHTML } from './utils' interface SurveyAppearanceProps { type: SurveyQuestionType @@ -224,6 +224,7 @@ export function Customization({ appearance, surveyQuestionItem, onAppearanceChan } onChange={(checked) => onAppearanceChange({ ...appearance, whiteLabel: checked })} + checked={appearance?.whiteLabel} disabledReason={!whitelabelAvailable ? 'Upgrade to any paid plan to hide PostHog branding' : null} /> @@ -283,12 +284,12 @@ export function BaseAppearance({ )}
    -
    +
    {/* Using dangerouslySetInnerHTML is safe here, because it's taking the user's input and showing it to the same user. They can try passing in arbitrary scripts, but it would show up only for them, so it's like trying to XSS yourself, where you already have all the data. Furthermore, sanitization should catch all obvious attempts */} {description && ( -
    +
    )} {type === SurveyQuestionType.Open && (