diff --git a/.eslintrc.js b/.eslintrc.js
index c7de1979550a2..6b38aaed71fd7 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -126,7 +126,7 @@ module.exports = {
},
],
'react/forbid-dom-props': [
- 'warn',
+ 'error',
{
forbid: [
{
diff --git a/.github/workflows/storybook-chromatic.yml b/.github/workflows/storybook-chromatic.yml
index 48fb95f8b804a..4faed2ea8e084 100644
--- a/.github/workflows/storybook-chromatic.yml
+++ b/.github/workflows/storybook-chromatic.yml
@@ -124,7 +124,7 @@ jobs:
run: pnpm install http-server wait-on
- name: Build Storybook
- run: pnpm build-storybook --quiet # Silence since progress logging results in a massive wall of spam
+ run: pnpm build-storybook --test --quiet # Silence since progress logging results in a massive wall of spam
- name: Serve Storybook in the background
run: |
diff --git a/.storybook/main.ts b/.storybook/main.ts
index a800cd203cf27..e2956ae3da544 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -44,6 +44,8 @@ const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
+
+ typescript: { reactDocgen: 'react-docgen' }, // Shouldn't be needed in Storybook 8
}
export default config
diff --git a/README.md b/README.md
index c8200e3c30491..4629af2ac101e 100644
--- a/README.md
+++ b/README.md
@@ -22,15 +22,21 @@
- Gather insights by capturing session replays, console logs, and network monitoring
- Improve your product with A/B testing that automatically analyzes performance
- Safely roll out features to select users or cohorts with feature flags
+- Send out fully customizable surveys to specific cohorts of users
- Connect to external services and manage data flows with PostHog CDP
-PostHog is available with hosting in the EU or US and is fully SOC 2 compliant. It's free to get started and your first 1 million events (and 15k replays) are free every month. We're constantly adding new features, such as user surveys!
+PostHog is available with hosting in the EU or US and is fully SOC 2 compliant. It's free to get started and comes with a generous monthly free tier:
+- 1 million product analytics events
+- 15k session replays
+- 1 million feature flag requests
+- 250 survey responses
+
+We're constantly adding new features, with web analytics and data warehouse now in beta!
## Table of Contents
- [Get started for free](#get-started-for-free)
-- [Features](#features)
-- [Docs and support](#docs-and-support)
+- [Docs](#docs)
- [Contributing](#contributing)
- [Philosophy](#philosophy)
- [Open-source vs paid](#open-source-vs-paid)
@@ -49,11 +55,9 @@ You can deploy a hobby instance in one line on Linux with Docker (recommended 4G
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/posthog/posthog/HEAD/bin/deploy-hobby)"
```
-Open source deployments should scale to approximately 100k events per month, after which we recommend migrating to a PostHog Cloud instance. See our [docs for more info and limitations](https://posthog.com/docs/self-host/open-source/deployment).
-
-PostHog has [sunset support for self-hosted K8s deployments](https://posthog.com/blog/sunsetting-helm-support-posthog).
+Open source deployments should scale to approximately 100k events per month, after which we recommend migrating to a PostHog Cloud instance. See our [docs for more info and limitations](https://posthog.com/docs/self-host/open-source/deployment). Please note that we do not provide customer support for open source deployments.
-## Features
+## Docs
![ui-demo](https://user-images.githubusercontent.com/85295485/144591577-fe97e4a5-5631-4a60-a684-45caf421507f.gif)
Want to find out more? Request a demo!
@@ -64,12 +68,13 @@ PostHog brings all the tools and data you need to build better products.
- **Event-based analytics:** Capture your product's usage [automatically](https://posthog.com/docs/integrate/client/js#autocapture), or [customize](https://posthog.com/docs/integrate) it to your needs
- **User and group tracking:** Understand the [people](https://posthog.com/manual/persons) and [groups](https://posthog.com/manual/group-analytics) behind the events and track properties about them
- **Data visualizations:** Create and share [graphs](https://posthog.com/docs/features/trends), [funnels](https://posthog.com/docs/features/funnels), [paths](https://posthog.com/docs/features/paths), [retention](https://posthog.com/docs/features/retention), and [dashboards](https://posthog.com/docs/features/dashboards)
-- **SQL access:** Use SQL to get a deeper understanding of your users, breakdown information and create completely tailored visualizations
+- **SQL access:** Use [SQL](https://posthog.com/docs/product-analytics/sql) to get a deeper understanding of your users, breakdown information and create completely tailored visualizations
- **Session replays:** [Watch videos](https://posthog.com/docs/features/session-recording) of your users' behavior, with fine-grained filters and privacy controls, as well as network monitoring and captured console logs
- **Heatmaps:** See where users click and get a visual representation of their behaviour with the [PostHog Toolbar](https://posthog.com/docs/features/toolbar)
-- **Feature flags:** Test and manage the rollout of new features to specific users and groups, or deploy flags as kill-switches
+- **Feature flags:** Test and manage the rollout of [new features](https://posthog.com/docs/feature-flags/installation) to specific users and groups, or deploy flags as kill-switches
- **A/B and multivariate experimentation:** run simple or complex changes as [experiments](https://posthog.com/manual/experimentation) and get automatic significance calculations
- **Correlation analysis:** Discover what events and properties [correlate](https://posthog.com/manual/correlation) with success and failure
+- **Surveys:** Collect qualitative feedback from your users using fully customizable [surveys](https://posthog.com/docs/surveys/installation)
### Data and infrastructure tools
@@ -79,16 +84,6 @@ PostHog brings all the tools and data you need to build better products.
[Read a full list of PostHog features](https://posthog.com/product).
-## Docs and support
-
-Read how to [integrate](https://posthog.com/docs/integrate), and [extend](https://posthog.com/docs/apps) an open-source 'Hobby' deployment of PostHog in our [documentation](https://posthog.com/docs).
-
-Check out our [tutorials](https://posthog.com/docs/apps) for step-by-step guides, how-to's, and best practices.
-
-Learn more about getting the most out of PostHog's features in [our product manual](https://posthog.com/using-posthog).
-
-[Ask a question](https://posthog.com/questions) to get support.
-
## Contributing
We <3 contributions big and small. In priority order (although everything is appreciated) with the most helpful first:
diff --git a/bin/unit_metrics.py b/bin/unit_metrics.py
index 8ec2f5782f2e3..bab29481c4138 100644
--- a/bin/unit_metrics.py
+++ b/bin/unit_metrics.py
@@ -1,7 +1,12 @@
import http.client
import json
+import time
+import logging
from prometheus_client import CollectorRegistry, Gauge, multiprocess, generate_latest
+
+logger = logging.getLogger(__name__)
+
UNIT_CONNECTIONS_ACCEPTED_TOTAL = Gauge(
"unit_connections_accepted_total",
"",
@@ -42,12 +47,25 @@
def application(environ, start_response):
- connection = http.client.HTTPConnection("localhost:8181")
- connection.request("GET", "/status")
- response = connection.getresponse()
+ retries = 5
+ try:
+ connection = http.client.HTTPConnection("localhost:8181")
+ connection.request("GET", "/status")
+ response = connection.getresponse()
- statj = json.loads(response.read())
- connection.close()
+ statj = json.loads(response.read())
+ except Exception as e:
+ if retries > 0:
+ retries -= 1
+ time.sleep(1)
+ return application(environ, start_response)
+ else:
+ raise e
+ finally:
+ try:
+ connection.close()
+ except Exception as e:
+ logger.error("Failed to close connection to unit: ", e)
UNIT_CONNECTIONS_ACCEPTED_TOTAL.set(statj["connections"]["accepted"])
UNIT_CONNECTIONS_ACTIVE.set(statj["connections"]["active"])
@@ -55,19 +73,11 @@ def application(environ, start_response):
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"]
- )
+ for app in statj["applications"].keys():
+ UNIT_PROCESSES_RUNNING_GAUGE.labels(application=app).set(statj["applications"][app]["processes"]["running"])
+ UNIT_PROCESSES_STARTING_GAUGE.labels(application=app).set(statj["applications"][app]["processes"]["starting"])
+ UNIT_PROCESSES_IDLE_GAUGE.labels(application=app).set(statj["applications"][app]["processes"]["idle"])
+ UNIT_REQUESTS_ACTIVE_GAUGE.labels(application=app).set(statj["applications"][app]["requests"]["active"])
start_response("200 OK", [("Content-Type", "text/plain")])
# Create the prometheus multi-process metric registry here
diff --git a/cypress/e2e/notebooks.cy.ts b/cypress/e2e/notebooks.cy.ts
index 853f0aba6819c..b251c4ba44e65 100644
--- a/cypress/e2e/notebooks.cy.ts
+++ b/cypress/e2e/notebooks.cy.ts
@@ -54,9 +54,9 @@ describe('Notebooks', () => {
it('Can comment on a recording', () => {
cy.visit(urls.replay())
- cy.get('[data-attr="notebooks-add-button"]').click()
- cy.get('.LemonButton').contains('New notebook').click()
+ cy.get('[data-attr="notebooks-add-button"]').click()
+ cy.get('[data-attr="notebooks-select-button-create"]').click()
cy.get('.Notebook.Notebook--editable').should('be.visible')
cy.get('.ph-recording.NotebookNode').should('be.visible')
diff --git a/ee/api/test/test_organization.py b/ee/api/test/test_organization.py
index cc48690395b83..4d7cf41f21fab 100644
--- a/ee/api/test/test_organization.py
+++ b/ee/api/test/test_organization.py
@@ -274,3 +274,20 @@ def test_feature_available_self_hosted_license_expired(self, patch_post):
self.organization.refresh_from_db()
self.assertFalse(self.organization.is_feature_available("whatever"))
License.PLANS = current_plans
+
+ def test_get_organization_restricted_teams_hidden(self):
+ self.organization_membership.level = OrganizationMembership.Level.MEMBER
+ self.organization_membership.save()
+ Team.objects.create(
+ organization=self.organization,
+ name="FORBIDDEN",
+ access_control=True,
+ )
+
+ response = self.client.get(f"/api/organizations/{self.organization.id}")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertListEqual(
+ [team["name"] for team in response.json()["teams"]],
+ [self.team.name], # "FORBIDDEN" excluded
+ )
diff --git a/ee/clickhouse/models/test/__snapshots__/test_property.ambr b/ee/clickhouse/models/test/__snapshots__/test_property.ambr
index d27396834cf99..cc8e77f83a0dc 100644
--- a/ee/clickhouse/models/test/__snapshots__/test_property.ambr
+++ b/ee/clickhouse/models/test/__snapshots__/test_property.ambr
@@ -57,7 +57,7 @@
---
# name: test_parse_groups_persons_edge_case_with_single_filter
(
- 'AND ( has(%(vglobalperson_0)s, "pmat_email"))',
+ 'AND ( has(%(vglobalperson_0)s, replaceRegexpAll(JSONExtractRaw(person_props, %(kglobalperson_0)s), \'^"|"$\', \'\')))',
{
'kglobalperson_0': 'email',
'vglobalperson_0': [
diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr
index 8fd354505870a..2d1aa22e3bed4 100644
--- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr
+++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr
@@ -1,6 +1,6 @@
# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results
'
- /* user_id:131 celery:posthog.celery.sync_insight_caching_state */
+ /* user_id:132 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
diff --git a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap
index fc2cf8cd0d5e2..277da71c3dc6e 100644
--- a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap
+++ b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap
@@ -57,7 +57,7 @@ exports[`replay/transform transform can convert images 1`] = `
"type": 3,
},
],
- "id": 106,
+ "id": 104,
"tagName": "div",
"type": 2,
},
@@ -74,7 +74,7 @@ exports[`replay/transform transform can convert images 1`] = `
"type": 2,
},
],
- "id": 105,
+ "id": 103,
"tagName": "div",
"type": 2,
},
@@ -147,29 +147,11 @@ exports[`replay/transform transform can convert rect with text 1`] = `
"childNodes": [
{
"attributes": {
- "style": "width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;",
- "viewBox": "0 0 100 30",
+ "style": "color: #ee3ee4;border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;",
},
- "childNodes": [
- {
- "attributes": {
- "fill": "transparent",
- "height": 30,
- "rx": "10px",
- "stroke": "#ee3ee4",
- "stroke-width": "4px",
- "width": 100,
- "x": 0,
- "y": 0,
- },
- "childNodes": [],
- "id": 108,
- "tagName": "rect",
- "type": 2,
- },
- ],
+ "childNodes": [],
"id": 12345,
- "tagName": "svg",
+ "tagName": "div",
"type": 2,
},
{
@@ -183,12 +165,12 @@ exports[`replay/transform transform can convert rect with text 1`] = `
"type": 3,
},
],
- "id": 109,
+ "id": 106,
"tagName": "div",
"type": 2,
},
],
- "id": 107,
+ "id": 105,
"tagName": "div",
"type": 2,
},
@@ -259,7 +241,7 @@ exports[`replay/transform transform can ignore unknown wireframe types 1`] = `
{
"attributes": {},
"childNodes": [],
- "id": 104,
+ "id": 102,
"tagName": "div",
"type": 2,
},
@@ -344,46 +326,17 @@ exports[`replay/transform transform can process unknown types without error 1`]
"childNodes": [
{
"attributes": {
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 25px;top: 42px;",
- "width": 100,
+ "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 25px;top: 42px;align-items: center;justify-content: center;display: flex;",
},
"childNodes": [
{
- "attributes": {
- "fill": "grey",
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 25px;top: 42px;",
- "width": 100,
- },
- "childNodes": [],
"id": 101,
- "tagName": "rect",
- "type": 2,
- },
- {
- "attributes": {
- "dominant-baseline": "middle",
- "fill": "white",
- "font-size": "30",
- "text-anchor": "middle",
- "x": "50%",
- "y": "50%",
- },
- "childNodes": [
- {
- "id": 103,
- "textContent": "image",
- "type": 3,
- },
- ],
- "id": 102,
- "tagName": "text",
- "type": 2,
+ "textContent": "image",
+ "type": 3,
},
],
"id": 12345,
- "tagName": "svg",
+ "tagName": "div",
"type": 2,
},
],
@@ -502,7 +455,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = `
"type": 3,
},
],
- "id": 111,
+ "id": 108,
"tagName": "div",
"type": 2,
},
@@ -517,7 +470,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = `
"type": 3,
},
],
- "id": 112,
+ "id": 109,
"tagName": "div",
"type": 2,
},
@@ -537,7 +490,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = `
"type": 3,
},
],
- "id": 113,
+ "id": 110,
"tagName": "div",
"type": 2,
},
@@ -547,7 +500,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = `
"type": 2,
},
],
- "id": 110,
+ "id": 107,
"tagName": "div",
"type": 2,
},
@@ -625,7 +578,7 @@ exports[`replay/transform transform inputs buttons with nested elements 1`] = `
},
"childNodes": [
{
- "id": 119,
+ "id": 114,
"textContent": "click me",
"type": 3,
},
@@ -635,7 +588,7 @@ exports[`replay/transform transform inputs buttons with nested elements 1`] = `
"type": 2,
},
],
- "id": 118,
+ "id": 113,
"tagName": "div",
"type": 2,
},
@@ -695,7 +648,7 @@ exports[`replay/transform transform inputs input - $inputType - hello 1`] = `
{
"attributes": {},
"childNodes": [],
- "id": 163,
+ "id": 160,
"tagName": "div",
"type": 2,
},
@@ -762,7 +715,7 @@ exports[`replay/transform transform inputs input - button - click me 1`] = `
},
"childNodes": [
{
- "id": 155,
+ "id": 152,
"textContent": "click me",
"type": 3,
},
@@ -772,7 +725,7 @@ exports[`replay/transform transform inputs input - button - click me 1`] = `
"type": 2,
},
],
- "id": 154,
+ "id": 151,
"tagName": "div",
"type": 2,
},
@@ -849,17 +802,17 @@ exports[`replay/transform transform inputs input - checkbox - $value 1`] = `
"type": 2,
},
{
- "id": 136,
+ "id": 133,
"textContent": "first",
"type": 3,
},
],
- "id": 135,
+ "id": 132,
"tagName": "label",
"type": 2,
},
],
- "id": 134,
+ "id": 131,
"tagName": "div",
"type": 2,
},
@@ -935,17 +888,17 @@ exports[`replay/transform transform inputs input - checkbox - $value 2`] = `
"type": 2,
},
{
- "id": 139,
+ "id": 136,
"textContent": "second",
"type": 3,
},
],
- "id": 138,
+ "id": 135,
"tagName": "label",
"type": 2,
},
],
- "id": 137,
+ "id": 134,
"tagName": "div",
"type": 2,
},
@@ -1023,17 +976,17 @@ exports[`replay/transform transform inputs input - checkbox - $value 3`] = `
"type": 2,
},
{
- "id": 142,
+ "id": 139,
"textContent": "third",
"type": 3,
},
],
- "id": 141,
+ "id": 138,
"tagName": "label",
"type": 2,
},
],
- "id": 140,
+ "id": 137,
"tagName": "div",
"type": 2,
},
@@ -1105,7 +1058,7 @@ exports[`replay/transform transform inputs input - checkbox - $value 4`] = `
"type": 2,
},
],
- "id": 143,
+ "id": 140,
"tagName": "div",
"type": 2,
},
@@ -1177,7 +1130,7 @@ exports[`replay/transform transform inputs input - email - $value 1`] = `
"type": 2,
},
],
- "id": 127,
+ "id": 124,
"tagName": "div",
"type": 2,
},
@@ -1249,7 +1202,7 @@ exports[`replay/transform transform inputs input - number - $value 1`] = `
"type": 2,
},
],
- "id": 128,
+ "id": 125,
"tagName": "div",
"type": 2,
},
@@ -1321,7 +1274,7 @@ exports[`replay/transform transform inputs input - password - $value 1`] = `
"type": 2,
},
],
- "id": 126,
+ "id": 123,
"tagName": "div",
"type": 2,
},
@@ -1388,10 +1341,26 @@ exports[`replay/transform transform inputs input - progress - $value 1`] = `
"childNodes": [
{
"attributes": {
- "style": "width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;border: 4px solid transparent;border-radius: 50%;border-top: 4px solid #fff;animation: spin 2s linear infinite;",
+ "style": "background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;border: 4px solid #35373e;border-radius: 50%;border-top: 4px solid #fff;animation: spin 2s linear infinite;",
},
- "childNodes": [],
- "id": 165,
+ "childNodes": [
+ {
+ "attributes": {
+ "type": "text/css",
+ },
+ "childNodes": [
+ {
+ "id": 163,
+ "textContent": "@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }",
+ "type": 3,
+ },
+ ],
+ "id": 162,
+ "tagName": "style",
+ "type": 2,
+ },
+ ],
+ "id": 164,
"tagName": "div",
"type": 2,
},
@@ -1401,7 +1370,7 @@ exports[`replay/transform transform inputs input - progress - $value 1`] = `
"type": 2,
},
],
- "id": 164,
+ "id": 161,
"tagName": "div",
"type": 2,
},
@@ -1474,7 +1443,7 @@ exports[`replay/transform transform inputs input - progress - $value 2`] = `
"type": 2,
},
],
- "id": 166,
+ "id": 165,
"tagName": "div",
"type": 2,
},
@@ -1547,7 +1516,7 @@ exports[`replay/transform transform inputs input - progress - 0.75 1`] = `
"type": 2,
},
],
- "id": 167,
+ "id": 166,
"tagName": "div",
"type": 2,
},
@@ -1620,7 +1589,7 @@ exports[`replay/transform transform inputs input - progress - 0.75 2`] = `
"type": 2,
},
],
- "id": 168,
+ "id": 167,
"tagName": "div",
"type": 2,
},
@@ -1692,7 +1661,7 @@ exports[`replay/transform transform inputs input - search - $value 1`] = `
"type": 2,
},
],
- "id": 129,
+ "id": 126,
"tagName": "div",
"type": 2,
},
@@ -1765,12 +1734,12 @@ exports[`replay/transform transform inputs input - select - hello 1`] = `
},
"childNodes": [
{
- "id": 160,
+ "id": 157,
"textContent": "hello",
"type": 3,
},
],
- "id": 159,
+ "id": 156,
"tagName": "option",
"type": 2,
},
@@ -1778,12 +1747,12 @@ exports[`replay/transform transform inputs input - select - hello 1`] = `
"attributes": {},
"childNodes": [
{
- "id": 162,
+ "id": 159,
"textContent": "world",
"type": 3,
},
],
- "id": 161,
+ "id": 158,
"tagName": "option",
"type": 2,
},
@@ -1793,7 +1762,7 @@ exports[`replay/transform transform inputs input - select - hello 1`] = `
"type": 2,
},
],
- "id": 158,
+ "id": 155,
"tagName": "div",
"type": 2,
},
@@ -1866,7 +1835,7 @@ exports[`replay/transform transform inputs input - tel - $value 1`] = `
"type": 2,
},
],
- "id": 130,
+ "id": 127,
"tagName": "div",
"type": 2,
},
@@ -1938,7 +1907,7 @@ exports[`replay/transform transform inputs input - text - $value 1`] = `
"type": 2,
},
],
- "id": 125,
+ "id": 122,
"tagName": "div",
"type": 2,
},
@@ -2010,7 +1979,7 @@ exports[`replay/transform transform inputs input - text - hello 1`] = `
"type": 2,
},
],
- "id": 124,
+ "id": 121,
"tagName": "div",
"type": 2,
},
@@ -2082,7 +2051,7 @@ exports[`replay/transform transform inputs input - textArea - $value 1`] = `
"type": 2,
},
],
- "id": 157,
+ "id": 154,
"tagName": "div",
"type": 2,
},
@@ -2154,7 +2123,7 @@ exports[`replay/transform transform inputs input - textArea - hello 1`] = `
"type": 2,
},
],
- "id": 156,
+ "id": 153,
"tagName": "div",
"type": 2,
},
@@ -2231,17 +2200,17 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = `
"type": 2,
},
{
- "id": 146,
+ "id": 143,
"textContent": "first",
"type": 3,
},
],
- "id": 145,
+ "id": 142,
"tagName": "label",
"type": 2,
},
],
- "id": 144,
+ "id": 141,
"tagName": "div",
"type": 2,
},
@@ -2317,17 +2286,17 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = `
"type": 2,
},
{
- "id": 149,
+ "id": 146,
"textContent": "second",
"type": 3,
},
],
- "id": 148,
+ "id": 145,
"tagName": "label",
"type": 2,
},
],
- "id": 147,
+ "id": 144,
"tagName": "div",
"type": 2,
},
@@ -2405,17 +2374,17 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = `
"type": 2,
},
{
- "id": 152,
+ "id": 149,
"textContent": "third",
"type": 3,
},
],
- "id": 151,
+ "id": 148,
"tagName": "label",
"type": 2,
},
],
- "id": 150,
+ "id": 147,
"tagName": "div",
"type": 2,
},
@@ -2487,7 +2456,7 @@ exports[`replay/transform transform inputs input - toggle - $value 4`] = `
"type": 2,
},
],
- "id": 153,
+ "id": 150,
"tagName": "div",
"type": 2,
},
@@ -2559,7 +2528,7 @@ exports[`replay/transform transform inputs input - url - https://example.io 1`]
"type": 2,
},
],
- "id": 131,
+ "id": 128,
"tagName": "div",
"type": 2,
},
@@ -2621,50 +2590,21 @@ exports[`replay/transform transform inputs placeholder - $inputType - $value 1`]
"childNodes": [
{
"attributes": {
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;",
- "width": 100,
+ "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;",
},
"childNodes": [
{
- "attributes": {
- "fill": "grey",
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;",
- "width": 100,
- },
- "childNodes": [],
- "id": 170,
- "tagName": "rect",
- "type": 2,
- },
- {
- "attributes": {
- "dominant-baseline": "middle",
- "fill": "white",
- "font-size": "30",
- "text-anchor": "middle",
- "x": "50%",
- "y": "50%",
- },
- "childNodes": [
- {
- "id": 172,
- "textContent": "hello",
- "type": 3,
- },
- ],
- "id": 171,
- "tagName": "text",
- "type": 2,
+ "id": 169,
+ "textContent": "hello",
+ "type": 3,
},
],
"id": 12365,
- "tagName": "svg",
+ "tagName": "div",
"type": 2,
},
],
- "id": 169,
+ "id": 168,
"tagName": "div",
"type": 2,
},
@@ -2724,7 +2664,7 @@ exports[`replay/transform transform inputs radio group - $inputType - $value 1`]
{
"attributes": {},
"childNodes": [],
- "id": 133,
+ "id": 130,
"tagName": "div",
"type": 2,
},
@@ -2794,7 +2734,7 @@ exports[`replay/transform transform inputs radio_group - $inputType - $value 1`]
"type": 2,
},
],
- "id": 132,
+ "id": 129,
"tagName": "div",
"type": 2,
},
@@ -2864,7 +2804,7 @@ exports[`replay/transform transform inputs radio_group 1`] = `
"type": 2,
},
],
- "id": 123,
+ "id": 120,
"tagName": "div",
"type": 2,
},
@@ -2888,7 +2828,7 @@ exports[`replay/transform transform inputs radio_group 1`] = `
}
`;
-exports[`replay/transform transform inputs webview - $inputType - $value 1`] = `
+exports[`replay/transform transform inputs web_view - $inputType - $value 1`] = `
{
"data": {
"initialOffset": {
@@ -2923,8 +2863,100 @@ exports[`replay/transform transform inputs webview - $inputType - $value 1`] = `
"childNodes": [
{
"attributes": {},
- "childNodes": [],
- "id": 173,
+ "childNodes": [
+ {
+ "attributes": {
+ "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;",
+ },
+ "childNodes": [
+ {
+ "id": 171,
+ "textContent": "web_view",
+ "type": 3,
+ },
+ ],
+ "id": 12365,
+ "tagName": "div",
+ "type": 2,
+ },
+ ],
+ "id": 170,
+ "tagName": "div",
+ "type": 2,
+ },
+ ],
+ "id": 5,
+ "tagName": "body",
+ "type": 2,
+ },
+ ],
+ "id": 3,
+ "tagName": "html",
+ "type": 2,
+ },
+ ],
+ "id": 1,
+ "type": 0,
+ },
+ },
+ "timestamp": 1,
+ "type": 2,
+}
+`;
+
+exports[`replay/transform transform inputs web_view with URL 1`] = `
+{
+ "data": {
+ "initialOffset": {
+ "left": 0,
+ "top": 0,
+ },
+ "node": {
+ "childNodes": [
+ {
+ "id": 2,
+ "name": "html",
+ "publicId": "",
+ "systemId": "",
+ "type": 1,
+ },
+ {
+ "attributes": {
+ "style": "height: 100vh; width: 100vw;",
+ },
+ "childNodes": [
+ {
+ "attributes": {},
+ "childNodes": [],
+ "id": 4,
+ "tagName": "head",
+ "type": 2,
+ },
+ {
+ "attributes": {
+ "style": "height: 100vh; width: 100vw;",
+ },
+ "childNodes": [
+ {
+ "attributes": {},
+ "childNodes": [
+ {
+ "attributes": {
+ "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;",
+ },
+ "childNodes": [
+ {
+ "id": 119,
+ "textContent": "https://example.com",
+ "type": 3,
+ },
+ ],
+ "id": 12365,
+ "tagName": "div",
+ "type": 2,
+ },
+ ],
+ "id": 118,
"tagName": "div",
"type": 2,
},
@@ -3000,17 +3032,17 @@ exports[`replay/transform transform inputs wrapping with labels 1`] = `
"type": 2,
},
{
- "id": 122,
+ "id": 117,
"textContent": "i will wrap the checkbox",
"type": 3,
},
],
- "id": 121,
+ "id": 116,
"tagName": "label",
"type": 2,
},
],
- "id": 120,
+ "id": 115,
"tagName": "div",
"type": 2,
},
@@ -3073,50 +3105,21 @@ exports[`replay/transform transform omitting x and y is equivalent to setting th
"childNodes": [
{
"attributes": {
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;",
- "width": 100,
+ "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;",
},
"childNodes": [
{
- "attributes": {
- "fill": "grey",
- "height": 30,
- "style": "width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;",
- "width": 100,
- },
- "childNodes": [],
- "id": 115,
- "tagName": "rect",
- "type": 2,
- },
- {
- "attributes": {
- "dominant-baseline": "middle",
- "fill": "white",
- "font-size": "30",
- "text-anchor": "middle",
- "x": "50%",
- "y": "50%",
- },
- "childNodes": [
- {
- "id": 117,
- "textContent": "image",
- "type": 3,
- },
- ],
- "id": 116,
- "tagName": "text",
- "type": 2,
+ "id": 112,
+ "textContent": "image",
+ "type": 3,
},
],
"id": 12345,
- "tagName": "svg",
+ "tagName": "div",
"type": 2,
},
],
- "id": 114,
+ "id": 111,
"tagName": "div",
"type": 2,
},
diff --git a/ee/frontend/mobile-replay/mobile.types.ts b/ee/frontend/mobile-replay/mobile.types.ts
index e8948c4b5427e..90db59407e500 100644
--- a/ee/frontend/mobile-replay/mobile.types.ts
+++ b/ee/frontend/mobile-replay/mobile.types.ts
@@ -235,6 +235,7 @@ export type wireframeRectangle = wireframeBase & {
export type wireframeWebView = wireframeBase & {
type: 'web_view'
+ url?: string
}
export type wireframePlaceholder = wireframeBase & {
diff --git a/ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json b/ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json
index 2f3d967ce77ed..c815bdba85690 100644
--- a/ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json
+++ b/ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json
@@ -874,6 +874,9 @@
"type": {
"$ref": "#/definitions/MobileNodeType"
},
+ "url": {
+ "type": "string"
+ },
"width": {
"type": "number"
},
diff --git a/ee/frontend/mobile-replay/transform.test.ts b/ee/frontend/mobile-replay/transform.test.ts
index ce6da521ea85c..c4cd67e346558 100644
--- a/ee/frontend/mobile-replay/transform.test.ts
+++ b/ee/frontend/mobile-replay/transform.test.ts
@@ -432,6 +432,27 @@ describe('replay/transform', () => {
})
).toMatchSnapshot()
})
+
+ test('web_view with URL', () => {
+ expect(
+ posthogEEModule.mobileReplay?.transformEventToWeb({
+ type: 2,
+ data: {
+ wireframes: [
+ {
+ id: 12365,
+ width: 100,
+ height: 30,
+ type: 'web_view',
+ url: 'https://example.com',
+ },
+ ],
+ },
+ timestamp: 1,
+ })
+ ).toMatchSnapshot()
+ })
+
test('radio_group', () => {
expect(
posthogEEModule.mobileReplay?.transformEventToWeb({
@@ -741,7 +762,7 @@ describe('replay/transform', () => {
id: 12365,
width: 100,
height: 30,
- type: 'webview',
+ type: 'web_view',
},
])('$type - $inputType - $value', (testCase) => {
expect(
diff --git a/ee/frontend/mobile-replay/transformers.ts b/ee/frontend/mobile-replay/transformers.ts
index e32d66fa44301..df35abee10704 100644
--- a/ee/frontend/mobile-replay/transformers.ts
+++ b/ee/frontend/mobile-replay/transformers.ts
@@ -15,6 +15,7 @@ import {
wireframeDiv,
wireframeImage,
wireframeInputComponent,
+ wireframePlaceholder,
wireframeProgress,
wireframeRadioGroup,
wireframeRectangle,
@@ -28,11 +29,12 @@ import {
makeHTMLStyles,
makeIndeterminateProgressStyles,
makeMinimalStyles,
- makePositionStyles,
makeStylesString,
- makeSvgBorder,
} from './wireframeStyle'
+const BACKGROUND = '#f3f4ef'
+const FOREGROUND = '#35373e'
+
/**
* generates a sequence of ids
* from 100 to 9,999,999
@@ -121,58 +123,33 @@ function makeTextElement(wireframe: wireframeText, children: serializedNodeWithI
}
function makeWebViewElement(wireframe: wireframe, children: serializedNodeWithId[]): serializedNodeWithId | null {
- return makePlaceholderElement(wireframe, children)
+ const labelledWireframe: wireframePlaceholder = { ...wireframe } as wireframePlaceholder
+ if ('url' in wireframe) {
+ labelledWireframe.label = wireframe.url
+ }
+
+ return makePlaceholderElement(labelledWireframe, children)
}
function makePlaceholderElement(wireframe: wireframe, children: serializedNodeWithId[]): serializedNodeWithId | null {
- //
- //
- //
- // Keyboard
- //
- //
const txt = 'label' in wireframe && wireframe.label ? wireframe.label : wireframe.type || 'PLACEHOLDER'
return {
type: NodeType.Element,
- tagName: 'svg',
+ tagName: 'div',
attributes: {
- width: wireframe.width,
- height: wireframe.height,
- style: makeStylesString(wireframe),
+ style: makeStylesString(wireframe, {
+ verticalAlign: 'center',
+ horizontalAlign: 'center',
+ backgroundColor: wireframe.style?.backgroundColor || BACKGROUND,
+ color: wireframe.style?.color || FOREGROUND,
+ }),
},
id: wireframe.id,
childNodes: [
{
- type: NodeType.Element,
- tagName: 'rect',
- attributes: {
- width: wireframe.width,
- height: wireframe.height,
- style: makeStylesString(wireframe),
- fill: wireframe.style?.backgroundColor || 'grey',
- },
- id: idSequence.next().value,
- childNodes: [],
- },
- {
- type: NodeType.Element,
- tagName: 'text',
- attributes: {
- fill: wireframe.style?.color || 'white',
- 'font-size': '30',
- x: '50%',
- y: '50%',
- 'dominant-baseline': 'middle',
- 'text-anchor': 'middle',
- },
+ type: NodeType.Text,
id: idSequence.next().value,
- childNodes: [
- {
- type: NodeType.Text,
- textContent: txt,
- id: idSequence.next().value,
- },
- ],
+ textContent: txt,
},
...children,
],
@@ -351,6 +328,33 @@ function makeProgressElement(
value = null
}
+ const styleOverride = {
+ color: wireframe.style?.color || FOREGROUND,
+ backgroundColor: wireframe.style?.backgroundColor || BACKGROUND,
+ }
+
+ // if not _isPositiveInteger(value) then we render a spinner,
+ // so we need to add a style element with the spin keyframe
+ const stylingChildren: serializedNodeWithId[] = _isPositiveInteger(value)
+ ? []
+ : [
+ {
+ type: NodeType.Element,
+ tagName: 'style',
+ attributes: {
+ type: 'text/css',
+ },
+ id: idSequence.next().value,
+ childNodes: [
+ {
+ type: NodeType.Text,
+ textContent: `@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`,
+ id: idSequence.next().value,
+ },
+ ],
+ },
+ ]
+
return {
type: NodeType.Element,
tagName: 'div',
@@ -365,11 +369,11 @@ function makeProgressElement(
attributes: {
// with no provided value we render a spinner
style: _isPositiveInteger(value)
- ? makeDeterminateProgressStyles(wireframe)
- : makeIndeterminateProgressStyles(wireframe),
+ ? makeDeterminateProgressStyles(wireframe, styleOverride)
+ : makeIndeterminateProgressStyles(wireframe, styleOverride),
},
id: idSequence.next().value,
- childNodes: [],
+ childNodes: stylingChildren,
},
...children,
],
@@ -471,28 +475,12 @@ function makeRectangleElement(
): serializedNodeWithId | null {
return {
type: NodeType.Element,
- tagName: 'svg',
+ tagName: 'div',
attributes: {
- style: makePositionStyles(wireframe),
- viewBox: `0 0 ${wireframe.width} ${wireframe.height}`,
+ style: makeStylesString(wireframe),
},
id: wireframe.id,
- childNodes: [
- {
- type: NodeType.Element,
- tagName: 'rect',
- attributes: {
- x: 0,
- y: 0,
- width: wireframe.width,
- height: wireframe.height,
- fill: wireframe.style?.backgroundColor || 'transparent',
- ...makeSvgBorder(wireframe.style),
- },
- id: idSequence.next().value,
- childNodes: children,
- },
- ],
+ childNodes: children,
}
}
diff --git a/ee/frontend/mobile-replay/wireframeStyle.ts b/ee/frontend/mobile-replay/wireframeStyle.ts
index c171730083dcb..579d68fde85e2 100644
--- a/ee/frontend/mobile-replay/wireframeStyle.ts
+++ b/ee/frontend/mobile-replay/wireframeStyle.ts
@@ -16,23 +16,28 @@ function ensureUnit(value: string | number): string {
return isNumber(value) ? `${value}px` : value.replace(/px$/g, '') + 'px'
}
-function makeBorderStyles(wireframe: wireframe): string {
+function makeBorderStyles(wireframe: wireframe, styleOverride?: MobileStyles): string {
let styles = ''
- if (!wireframe.style) {
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
+ }
+
+ if (!combinedStyles) {
return styles
}
- if (isUnitLike(wireframe.style.borderWidth)) {
- const borderWidth = ensureUnit(wireframe.style.borderWidth)
+ if (isUnitLike(combinedStyles.borderWidth)) {
+ const borderWidth = ensureUnit(combinedStyles.borderWidth)
styles += `border-width: ${borderWidth};`
}
- if (isUnitLike(wireframe.style.borderRadius)) {
- const borderRadius = ensureUnit(wireframe.style.borderRadius)
+ if (isUnitLike(combinedStyles.borderRadius)) {
+ const borderRadius = ensureUnit(combinedStyles.borderRadius)
styles += `border-radius: ${borderRadius};`
}
- if (wireframe.style?.borderColor) {
- styles += `border-color: ${wireframe.style.borderColor};`
+ if (combinedStyles?.borderColor) {
+ styles += `border-color: ${combinedStyles.borderColor};`
}
if (styles.length > 0) {
@@ -42,27 +47,7 @@ function makeBorderStyles(wireframe: wireframe): string {
return styles
}
-export function makeSvgBorder(style: MobileStyles | undefined): Record {
- const svgBorderStyles: Record = {}
-
- if (!style) {
- return svgBorderStyles
- }
-
- if (isUnitLike(style.borderWidth)) {
- svgBorderStyles['stroke-width'] = ensureUnit(style.borderWidth)
- }
- if (style.borderColor) {
- svgBorderStyles.stroke = style.borderColor
- }
- if (isUnitLike(style.borderRadius)) {
- svgBorderStyles.rx = ensureUnit(style.borderRadius)
- }
-
- return svgBorderStyles
-}
-
-export function makePositionStyles(wireframe: wireframe): string {
+function makePositionStyles(wireframe: wireframe): string {
let styles = ''
if (isNumber(wireframe.width)) {
styles += `width: ${ensureUnit(wireframe.width)};`
@@ -85,16 +70,22 @@ export function makePositionStyles(wireframe: wireframe): string {
return styles
}
-function makeLayoutStyles(wireframe: wireframe): string {
+function makeLayoutStyles(wireframe: wireframe, styleOverride?: MobileStyles): string {
let styles = ''
- if (wireframe.style?.verticalAlign) {
+
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
+ }
+
+ if (combinedStyles.verticalAlign) {
styles += `align-items: ${
- { top: 'flex-start', center: 'center', bottom: 'flex-end' }[wireframe.style.verticalAlign]
+ { top: 'flex-start', center: 'center', bottom: 'flex-end' }[combinedStyles.verticalAlign]
};`
}
- if (wireframe.style?.horizontalAlign) {
+ if (combinedStyles.horizontalAlign) {
styles += `justify-content: ${
- { left: 'flex-start', center: 'center', right: 'flex-end' }[wireframe.style.horizontalAlign]
+ { left: 'flex-start', center: 'center', right: 'flex-end' }[combinedStyles.horizontalAlign]
};`
}
if (styles.length) {
@@ -103,45 +94,59 @@ function makeLayoutStyles(wireframe: wireframe): string {
return styles
}
-function makeFontStyles(wireframe: wireframe): string {
+function makeFontStyles(wireframe: wireframe, styleOverride?: MobileStyles): string {
let styles = ''
- if (!wireframe.style) {
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
+ }
+
+ if (!combinedStyles) {
return styles
}
- if (isUnitLike(wireframe.style.fontSize)) {
- styles += `font-size: ${ensureUnit(wireframe.style?.fontSize)};`
+ if (isUnitLike(combinedStyles.fontSize)) {
+ styles += `font-size: ${ensureUnit(combinedStyles?.fontSize)};`
}
- if (wireframe.style.fontFamily) {
- styles += `font-family: ${wireframe.style.fontFamily};`
+ if (combinedStyles.fontFamily) {
+ styles += `font-family: ${combinedStyles.fontFamily};`
}
return styles
}
-export function makeIndeterminateProgressStyles(wireframe: wireframeProgress): string {
+export function makeIndeterminateProgressStyles(wireframe: wireframeProgress, styleOverride?: MobileStyles): string {
let styles = ''
- if (wireframe.style?.backgroundColor) {
- styles += `background-color: ${wireframe.style.backgroundColor};`
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
+ }
+ if (combinedStyles.backgroundColor) {
+ styles += `background-color: ${combinedStyles.backgroundColor};`
}
styles += makePositionStyles(wireframe)
- styles += `border: 4px solid ${wireframe.style?.borderColor || 'transparent'};`
+ styles += `border: 4px solid ${combinedStyles.borderColor || combinedStyles.color || 'transparent'};`
styles += `border-radius: 50%;border-top: 4px solid #fff;`
styles += `animation: spin 2s linear infinite;`
return styles
}
-export function makeDeterminateProgressStyles(wireframe: wireframeProgress): string {
+export function makeDeterminateProgressStyles(wireframe: wireframeProgress, styleOverride?: MobileStyles): string {
let styles = ''
- if (wireframe.style?.backgroundColor) {
- styles += `background-color: ${wireframe.style.backgroundColor};`
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
+ }
+
+ if (combinedStyles.backgroundColor) {
+ styles += `background-color: ${combinedStyles.backgroundColor};`
}
styles += makePositionStyles(wireframe)
styles += 'border-radius: 50%;'
const radialGradient = `radial-gradient(closest-side, white 80%, transparent 0 99.9%, white 0)`
- const conicGradient = `conic-gradient(${wireframe.style?.color || 'black'} calc(${wireframe.value} * 1%), ${
- wireframe.style?.backgroundColor
+ const conicGradient = `conic-gradient(${combinedStyles.color || 'black'} calc(${wireframe.value} * 1%), ${
+ combinedStyles.backgroundColor
} 0)`
styles += `background: ${radialGradient}, ${conicGradient};`
@@ -151,27 +156,33 @@ export function makeDeterminateProgressStyles(wireframe: wireframeProgress): str
/**
* normally use makeStylesString instead, but sometimes you need styles without any colors applied
* */
-export function makeMinimalStyles(wireframe: wireframe): string {
+export function makeMinimalStyles(wireframe: wireframe, styleOverride?: MobileStyles): string {
let styles = ''
styles += makePositionStyles(wireframe)
- styles += makeLayoutStyles(wireframe)
- styles += makeFontStyles(wireframe)
+ styles += makeLayoutStyles(wireframe, styleOverride)
+ styles += makeFontStyles(wireframe, styleOverride)
return styles
}
-export function makeStylesString(wireframe: wireframe): string {
+export function makeStylesString(wireframe: wireframe, styleOverride?: MobileStyles): string {
let styles = ''
- if (wireframe.style?.color) {
- styles += `color: ${wireframe.style.color};`
+ const combinedStyles = {
+ ...wireframe.style,
+ ...styleOverride,
}
- if (wireframe.style?.backgroundColor) {
- styles += `background-color: ${wireframe.style.backgroundColor};`
+
+ if (combinedStyles.color) {
+ styles += `color: ${combinedStyles.color};`
}
- styles += makeBorderStyles(wireframe)
- styles += makeMinimalStyles(wireframe)
+ if (combinedStyles.backgroundColor) {
+ styles += `background-color: ${combinedStyles.backgroundColor};`
+ }
+
+ styles += makeBorderStyles(wireframe, styleOverride)
+ styles += makeMinimalStyles(wireframe, styleOverride)
return styles
}
diff --git a/frontend/__snapshots__/components-cards-text-card--template.png b/frontend/__snapshots__/components-cards-text-card--template.png
index 3c045b8354ba6..208c5a11fa5d4 100644
Binary files a/frontend/__snapshots__/components-cards-text-card--template.png and b/frontend/__snapshots__/components-cards-text-card--template.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--dashboard.png b/frontend/__snapshots__/exporter-exporter--dashboard.png
index f9f627a9ec7fd..a7164982fde68 100644
Binary files a/frontend/__snapshots__/exporter-exporter--dashboard.png and b/frontend/__snapshots__/exporter-exporter--dashboard.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png b/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png
index 49e132b82db76..0b7380b3d5b8e 100644
Binary files a/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png and b/frontend/__snapshots__/exporter-exporter--lifecycle-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--stickiness-insight.png b/frontend/__snapshots__/exporter-exporter--stickiness-insight.png
index 3fc3e03143c98..1623f5654fa3e 100644
Binary files a/frontend/__snapshots__/exporter-exporter--stickiness-insight.png and b/frontend/__snapshots__/exporter-exporter--stickiness-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png
index 7de2045284f44..16d5054623119 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-area-breakdown-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-area-insight.png b/frontend/__snapshots__/exporter-exporter--trends-area-insight.png
index 41d262d023141..06a4cd04eaebd 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-area-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-area-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png b/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png
index 41d262d023141..06a4cd04eaebd 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-bar-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-insight.png
index 41d262d023141..06a4cd04eaebd 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-line-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-line-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png
index 93c140d75f49a..55fc5f56d80b4 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-line-multi-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png
index 282daf6b30543..c9e00fdd402a4 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-insight.png b/frontend/__snapshots__/exporter-exporter--trends-table-insight.png
index 5223b75398630..14ef9b1154ca4 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-table-insight.png differ
diff --git a/frontend/__snapshots__/exporter-exporter--trends-value-insight.png b/frontend/__snapshots__/exporter-exporter--trends-value-insight.png
index 41d262d023141..06a4cd04eaebd 100644
Binary files a/frontend/__snapshots__/exporter-exporter--trends-value-insight.png and b/frontend/__snapshots__/exporter-exporter--trends-value-insight.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--button--dark.png b/frontend/__snapshots__/filters-property-filter-button--button--dark.png
new file mode 100644
index 0000000000000..d2d5914ad6fd7
Binary files /dev/null and b/frontend/__snapshots__/filters-property-filter-button--button--dark.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--button--light.png b/frontend/__snapshots__/filters-property-filter-button--button--light.png
new file mode 100644
index 0000000000000..01ecaac73a211
Binary files /dev/null and b/frontend/__snapshots__/filters-property-filter-button--button--light.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--button.png b/frontend/__snapshots__/filters-property-filter-button--button.png
new file mode 100644
index 0000000000000..3b8c14ecc0b7c
Binary files /dev/null and b/frontend/__snapshots__/filters-property-filter-button--button.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--filter-types--dark.png b/frontend/__snapshots__/filters-property-filter-button--filter-types--dark.png
index 7d327b393da5c..e5da922ada12b 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--filter-types--dark.png and b/frontend/__snapshots__/filters-property-filter-button--filter-types--dark.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--filter-types--light.png b/frontend/__snapshots__/filters-property-filter-button--filter-types--light.png
index 84df0a6bd221a..9769da9797415 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--filter-types--light.png and b/frontend/__snapshots__/filters-property-filter-button--filter-types--light.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--filter-types.png b/frontend/__snapshots__/filters-property-filter-button--filter-types.png
index 7a70395f75462..6ffd6cde8b8b8 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--filter-types.png and b/frontend/__snapshots__/filters-property-filter-button--filter-types.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--pseudo-states--dark.png b/frontend/__snapshots__/filters-property-filter-button--pseudo-states--dark.png
index f516817d47b28..409601d4b2735 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--pseudo-states--dark.png and b/frontend/__snapshots__/filters-property-filter-button--pseudo-states--dark.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--pseudo-states--light.png b/frontend/__snapshots__/filters-property-filter-button--pseudo-states--light.png
index c0ba463dbff70..0937f703a736a 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--pseudo-states--light.png and b/frontend/__snapshots__/filters-property-filter-button--pseudo-states--light.png differ
diff --git a/frontend/__snapshots__/filters-property-filter-button--pseudo-states.png b/frontend/__snapshots__/filters-property-filter-button--pseudo-states.png
index 979a5cb5fe074..e2d1b750a91c2 100644
Binary files a/frontend/__snapshots__/filters-property-filter-button--pseudo-states.png and b/frontend/__snapshots__/filters-property-filter-button--pseudo-states.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 511e5bcc051b4..d6139bbdf9627 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__/lemon-ui-colors--color-palette.png b/frontend/__snapshots__/lemon-ui-colors--color-palette.png
index bda274f89b37b..540cb196b3f81 100644
Binary files a/frontend/__snapshots__/lemon-ui-colors--color-palette.png and b/frontend/__snapshots__/lemon-ui-colors--color-palette.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--loading--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--loading--dark.png
index 436c618a7212e..d771bd1185591 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--loading--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--loading--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--loading--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--loading--light.png
index 7983e16c30ab5..4e4ab63b3dfc1 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--loading--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--loading--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--loading.png b/frontend/__snapshots__/lemon-ui-lemon-button--loading.png
index 0242c063f0781..f09505f02ebc5 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--loading.png and b/frontend/__snapshots__/lemon-ui-lemon-button--loading.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--dark.png
index 8f467d6638590..46440a99a9495 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--light.png
index a003b06034a14..898fabc457edb 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states.png b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states.png
index d33d03c99278f..ce16df7e1b459 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states.png and b/frontend/__snapshots__/lemon-ui-lemon-button--pseudo-states.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--sizes--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--sizes--dark.png
index b5b3d8bdb3bd7..c5587c6cbbac7 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--sizes--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--sizes--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--sizes--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--sizes--light.png
index 6eff03a894756..ffd8d8fda6cef 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--sizes--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--sizes--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--dark.png
index c8d52e384f548..46812f187d7b6 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--light.png
index c8ae34ef7107c..712933f866dcc 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only.png b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only.png
index 18fb90a365541..8bb5647b5ab6b 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only.png and b/frontend/__snapshots__/lemon-ui-lemon-button--sizes-icon-only.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--dark.png
index 88fc67bc2f79d..36ef77e47bb64 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--light.png
index ea790808089c2..d99244bb29516 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000.png
index bcf468b94ebfe..77ba7626ec560 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-3000.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-3000.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--dark.png
index 9c4cc82a1667d..196511a61d100 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--light.png
index caf9a688bf48d..e67d391598530 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses.png b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses.png
index d3cd52d1cce62..6c069b90a1d73 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses.png and b/frontend/__snapshots__/lemon-ui-lemon-button--types-and-statuses.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--dark.png b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--dark.png
index 597b5bce0e0bc..7cf8065b14abc 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--light.png b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--light.png
index f7cee3572ce3a..24515a6bd7a8c 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--light.png and b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action.png b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action.png
index 51a84ba81aa51..2cad7296bb3c3 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action.png and b/frontend/__snapshots__/lemon-ui-lemon-button--with-side-action.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png
index b5e2113ee71af..00807ba1c9f73 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-range--lemon-calendar-range.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png
index 8ea8124973d82..13cde27282b24 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--lemon-calendar-select.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png
index e81dc1cf31bbc..3c15edb09ac56 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png and b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-label--basic.png b/frontend/__snapshots__/lemon-ui-lemon-label--basic.png
index cf671f447d65b..528b9a55db843 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-label--basic.png and b/frontend/__snapshots__/lemon-ui-lemon-label--basic.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-menu--sectioned-items.png b/frontend/__snapshots__/lemon-ui-lemon-menu--sectioned-items.png
index 883d7f489d1d2..f24b402e339d0 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-menu--sectioned-items.png and b/frontend/__snapshots__/lemon-ui-lemon-menu--sectioned-items.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--clearable.png b/frontend/__snapshots__/lemon-ui-lemon-select--clearable.png
index 4208f609c7000..173e6083b8d7f 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--clearable.png and b/frontend/__snapshots__/lemon-ui-lemon-select--clearable.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--custom-element.png b/frontend/__snapshots__/lemon-ui-lemon-select--custom-element.png
index 04084d27caaf9..f5f49e01f6c14 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--custom-element.png and b/frontend/__snapshots__/lemon-ui-lemon-select--custom-element.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--flat.png b/frontend/__snapshots__/lemon-ui-lemon-select--flat.png
index af67619e3e97e..014fa2c439173 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--flat.png and b/frontend/__snapshots__/lemon-ui-lemon-select--flat.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png b/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png
index 01c803c849fb2..b5e107be8cf4f 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png and b/frontend/__snapshots__/lemon-ui-lemon-select--long-options.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--mixed-values-types.png b/frontend/__snapshots__/lemon-ui-lemon-select--mixed-values-types.png
index af67619e3e97e..014fa2c439173 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--mixed-values-types.png and b/frontend/__snapshots__/lemon-ui-lemon-select--mixed-values-types.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--nested-select.png b/frontend/__snapshots__/lemon-ui-lemon-select--nested-select.png
index af67619e3e97e..014fa2c439173 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--nested-select.png and b/frontend/__snapshots__/lemon-ui-lemon-select--nested-select.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-select--sectioned-options.png b/frontend/__snapshots__/lemon-ui-lemon-select--sectioned-options.png
index af67619e3e97e..014fa2c439173 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-select--sectioned-options.png and b/frontend/__snapshots__/lemon-ui-lemon-select--sectioned-options.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png
new file mode 100644
index 0000000000000..3b4b55227faac
Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png
new file mode 100644
index 0000000000000..44c5df9cd7035
Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag.png b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag.png
new file mode 100644
index 0000000000000..22ba415f7ec49
Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag.png differ
diff --git a/frontend/__snapshots__/lemon-ui-object-tags--default.png b/frontend/__snapshots__/lemon-ui-object-tags--default.png
index 718a544e296e9..ec7a9b62816b1 100644
Binary files a/frontend/__snapshots__/lemon-ui-object-tags--default.png and b/frontend/__snapshots__/lemon-ui-object-tags--default.png differ
diff --git a/frontend/__snapshots__/scenes-app-events--event-explorer.png b/frontend/__snapshots__/scenes-app-events--event-explorer.png
index a01b0acd54c8e..6a39938cc49d3 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 9829aceb1fb29..1100671504112 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--running-trend-experiment.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png
index e430a1e7e64b5..70a44479250c4 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png differ
diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png
index 2d6a0dd22fbb2..cb7249e0ab666 100644
Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png
index fad924d6d992c..06ba2306497bf 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 ccf8234088213..3001cd8a9bf15 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-top-to-bottom-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png
index 75e979c6000f2..82c54a20f63cc 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 1c676df02d51b..cdc972639fefc 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--trends-area-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png
index 4f9ecafb2cb91..0cb97e224e4e6 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png
index c1b930d99d3e0..9d9d059e7c30b 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png
index 23df97529d796..c89e656254be9 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png
index 3898222555311..2104c79414a0c 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png
index 1609928c7ceb8..7d05ac750c474 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png
index 06178fa43b12e..7eb244c5d0d4c 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png
index e3a58a360df55..98c5274496d9d 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png
index 909a8e0fb08f4..968fb23bb9228 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png
index 0987fe2e35a86..ef61297736835 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png
index 14f36451d7c4d..bd8cb78fc7f40 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png
index a263bb10b8e2c..0533de4693a29 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png
index a5b0fe38ddcfb..c82dd5a7302c8 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png
index 0267ec23cbe4d..378dc8501afb5 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png
index f0f8735e3daa3..30a1dc81d26dc 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png differ
diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png
index b40312a58dfcc..bef4b4f7619f2 100644
Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png differ
diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png
index a2cc724637cdf..d9db9b57a35ad 100644
Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png differ
diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png
index 9ffc850689b47..367671dfe77b3 100644
Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png differ
diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png
index 68b4ed6a5cdb7..4a044d9792d7d 100644
Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png differ
diff --git a/frontend/__snapshots__/scenes-app-persons-modal--persons-modal.png b/frontend/__snapshots__/scenes-app-persons-modal--persons-modal.png
index 849f0694e545c..e8fd319d1d87e 100644
Binary files a/frontend/__snapshots__/scenes-app-persons-modal--persons-modal.png and b/frontend/__snapshots__/scenes-app-persons-modal--persons-modal.png differ
diff --git a/frontend/__snapshots__/scenes-app-saved-insights--card-view.png b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png
index 5c8773e3ed204..77483b1607058 100644
Binary files a/frontend/__snapshots__/scenes-app-saved-insights--card-view.png and b/frontend/__snapshots__/scenes-app-saved-insights--card-view.png differ
diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-settings.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-settings.png
index 66da5de8f633e..6adacd6d152c8 100644
Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-settings.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-settings.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 99f004ff819fd..911eaae6e5196 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-other-billing-v2--billing-v-2--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png
new file mode 100644
index 0000000000000..1206e9b1193bf
Binary files /dev/null and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png
new file mode 100644
index 0000000000000..3023765d7b992
Binary files /dev/null and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png
new file mode 100644
index 0000000000000..2f7dfd3f4fff9
Binary files /dev/null and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png
new file mode 100644
index 0000000000000..83ea709a5f282
Binary files /dev/null and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2-with-discount--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-other-products.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-other-products.png
index 67ca8a8ca62da..a5a8a431f0cd4 100644
Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-other-products.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-other-products.png differ
diff --git a/frontend/public/hedgehog/phone-pair-hogs.png b/frontend/public/hedgehog/phone-pair-hogs.png
new file mode 100644
index 0000000000000..a7faa325a9979
Binary files /dev/null and b/frontend/public/hedgehog/phone-pair-hogs.png differ
diff --git a/frontend/src/layout/navigation-3000/Navigation.scss b/frontend/src/layout/navigation-3000/Navigation.scss
index 68787fb5643a0..5c7e9dd1e29b5 100644
--- a/frontend/src/layout/navigation-3000/Navigation.scss
+++ b/frontend/src/layout/navigation-3000/Navigation.scss
@@ -73,7 +73,6 @@
flex: 1;
flex-direction: column;
justify-content: space-between;
- padding: 0 0.375rem;
overflow-y: auto;
background: var(--accent-3000);
@@ -81,6 +80,10 @@
--lemon-button-padding-horizontal: 0.25rem !important;
}
+ > * {
+ padding: 0 0.375rem;
+ }
+
ul {
padding: 0.5rem 0;
}
@@ -91,6 +94,10 @@
}
}
+.Navbar3000__top {
+ overflow: auto;
+}
+
.Navbar3000__overlay {
position: fixed;
z-index: var(--z-mobile-nav-overlay);
@@ -99,12 +106,12 @@
background-color: var(--modal-backdrop-color);
backdrop-filter: blur(var(--modal-backdrop-blur));
opacity: 1;
- transition: background-color 100ms ease-out, backdrop-filter 100ms ease-out;
+ transition: opacity 100ms ease-out, backdrop-filter 100ms ease-out;
&.Navbar3000--hidden {
pointer-events: none;
- background-color: transparent;
backdrop-filter: blur(0);
+ opacity: 0;
}
}
diff --git a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx
index 25f2b0c10d073..f1dd5b6adbb17 100644
--- a/frontend/src/layout/navigation-3000/components/NavbarButton.tsx
+++ b/frontend/src/layout/navigation-3000/components/NavbarButton.tsx
@@ -4,6 +4,7 @@ import { useActions, useValues } from 'kea'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
+import { isMobile } from 'lib/utils'
import React, { FunctionComponent, ReactElement, useState } from 'react'
import { sceneLogic } from 'scenes/sceneLogic'
@@ -68,7 +69,9 @@ export const NavbarButton: FunctionComponent = React.forwardR
'data-attr': `menu-item-${sideAction.identifier.toLowerCase()}`,
}
buttonProps.sideIcon = null
- } else if (keyboardShortcut) {
+ } else if (keyboardShortcut && !isMobile()) {
+ // If the user agent says we're on mobile, then it's unlikely - but not impossible -
+ // that there's a physical keyboard. Hence in that case we don't show the keyboard shortcut
buttonProps.sideIcon = (
diff --git a/frontend/src/layout/navigation-3000/components/TopBar.scss b/frontend/src/layout/navigation-3000/components/TopBar.scss
index 240d91607f908..4268b95d7d28c 100644
--- a/frontend/src/layout/navigation-3000/components/TopBar.scss
+++ b/frontend/src/layout/navigation-3000/components/TopBar.scss
@@ -38,6 +38,7 @@
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
+ overflow: hidden;
}
.TopBar3000__trail {
diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelWelcome.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelWelcome.tsx
index 62920a7d1fac0..ce08967b7e528 100644
--- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelWelcome.tsx
+++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelWelcome.tsx
@@ -32,6 +32,7 @@ type RowProps = {
}
const Row = ({ className, columns, children }: RowProps): JSX.Element => (
+ // eslint-disable-next-line react/forbid-dom-props
{children}
@@ -66,6 +67,7 @@ const Image = ({
width?: number | string
height?: number | string
style?: React.CSSProperties
+ // eslint-disable-next-line react/forbid-dom-props
}): JSX.Element =>
export const SidePanelWelcome = (): JSX.Element => {
@@ -96,6 +98,7 @@ export const SidePanelWelcome = (): JSX.Element => {
We're past 0 to 1.
It's time to go from 1 to 3000. And this is just the beginning…
@@ -247,7 +250,7 @@ export const SidePanelWelcome = (): JSX.Element => {
Share feedback
-
+
Pro tip: Access this panel again from the{' '}
diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss
index caa7b400e575a..c42b01543f804 100644
--- a/frontend/src/lib/components/CommandBar/index.scss
+++ b/frontend/src/lib/components/CommandBar/index.scss
@@ -18,5 +18,5 @@
.SearchResults {
// offset container height by input
- height: 'calc(100% - 2.375rem)';
+ height: calc(100% - 2.375rem);
}
diff --git a/frontend/src/lib/components/CopyToClipboard.tsx b/frontend/src/lib/components/CopyToClipboard.tsx
index fd8e9c78dd3a7..74f6ee407c4ed 100644
--- a/frontend/src/lib/components/CopyToClipboard.tsx
+++ b/frontend/src/lib/components/CopyToClipboard.tsx
@@ -62,6 +62,7 @@ export function CopyToClipboardInline({
isValueSensitive && 'ph-no-capture',
className
)}
+ // eslint-disable-next-line react/forbid-dom-props
style={style}
onClick={!selectable ? copy : undefined}
{...props}
diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
index db1cdb0b48cdc..118327650e662 100644
--- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
+++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
@@ -1,5 +1,6 @@
import './DefinitionPopover.scss'
+import { ProfilePicture } from '@posthog/lemon-ui'
import { Divider, DividerProps } from 'antd'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
@@ -11,7 +12,6 @@ import { Link } from 'lib/lemon-ui/Link'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { getKeyMapping } from 'lib/taxonomy'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
-import { Owner } from 'scenes/events/Owner'
import { KeyMapping, UserBasicType } from '~/types'
@@ -159,10 +159,7 @@ function TimeMeta({
{updatedBy && (
by
-
+
)}
@@ -174,11 +171,7 @@ function TimeMeta({
Created {dayjs().to(dayjs.utc(createdAt))}
{updatedBy && (
- by {' '}
-
+ by
)}
@@ -187,6 +180,21 @@ function TimeMeta({
return <>>
}
+function Owner({ user }: { user?: UserBasicType | null }): JSX.Element {
+ return (
+ <>
+ {user?.uuid ? (
+
+ ) : (
+
No owner
+ )}
+ >
+ )
+}
+
function HorizontalLine({ children, ...props }: DividerProps): JSX.Element {
return (
diff --git a/frontend/src/lib/components/PageHeader.tsx b/frontend/src/lib/components/PageHeader.tsx
index 27b4773d25947..85563adf950ff 100644
--- a/frontend/src/lib/components/PageHeader.tsx
+++ b/frontend/src/lib/components/PageHeader.tsx
@@ -12,7 +12,6 @@ interface PageHeaderProps {
title: string | JSX.Element
caption?: string | JSX.Element | null | false
buttons?: JSX.Element | false
- style?: React.CSSProperties
tabbedPage?: boolean // Whether the page has tabs for secondary navigation
delimited?: boolean
notebookProps?: Pick
@@ -22,7 +21,6 @@ export function PageHeader({
title,
caption,
buttons,
- style,
tabbedPage,
delimited,
notebookProps,
@@ -33,8 +31,7 @@ export function PageHeader({
return (
<>
{!is3000 && (
- // eslint-disable-next-line react/forbid-dom-props
-
+
{!is3000 &&
(notebookProps ? (
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.scss b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.scss
index f4760971ff52d..b78e0aeedb8ac 100644
--- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.scss
+++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.scss
@@ -2,20 +2,36 @@
display: inline-flex;
gap: 0.5rem;
align-items: center;
+ height: 2rem;
+ padding: 0.25rem 1rem;
overflow: hidden;
- text-shadow: none;
+ color: var(--default);
+ white-space: nowrap;
+ user-select: none;
background: var(--border);
- border-color: transparent;
- box-shadow: none;
- transition-duration: 200ms; // Shorten from Ant's 300ms
+ border: 1px solid transparent;
+ border-radius: 2rem;
+ outline: 0;
+ transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover,
&:focus {
- color: var(--text-default);
- background: var(--border);
border-color: var(--border);
}
+ &:not(.PropertyFilterButton--clickable) {
+ border-color: transparent;
+ }
+
+ &.PropertyFilterButton--clickable {
+ touch-action: manipulation; // faster clicks
+ cursor: pointer;
+ }
+
+ &.PropertyFilterButton--closable {
+ padding-right: 0.5rem;
+ }
+
> :not(.PropertyFilterButton-content) {
flex-shrink: 0;
}
@@ -25,4 +41,28 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+ // Close button
+ .LemonButton {
+ margin-left: -0.25rem;
+ }
+
+ .LemonButton__chrome {
+ height: 22px !important;
+ min-height: auto !important;
+ padding: 0 4px !important;
+ }
+}
+
+.posthog-3000 {
+ .PropertyFilterButton {
+ &:hover,
+ &:focus {
+ border-color: var(--border-bold);
+ }
+
+ &:not(.PropertyFilterButton--clickable) {
+ border-color: transparent;
+ }
+ }
}
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.stories.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.stories.tsx
index d966a58546986..b92f13b21f989 100644
--- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.stories.tsx
+++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.stories.tsx
@@ -83,6 +83,10 @@ const meta: Meta
= {
}
export default meta
+export function Button(): JSX.Element {
+ return {}} onClose={() => {}} />
+}
+
export function FilterTypes(): JSX.Element {
return (
@@ -99,22 +103,30 @@ export function PseudoStates(): JSX.Element {
return (
-
STATE=DEFAULT
+
Default
+
{}} />
+
{}} />
+
+ {}} onClose={() => {}} />
-
STATE=HOVER
+
Hover
+
{}} />
+
{}} />
-
-
-
STATE=HOVER,ACTIVE
-
+
{}} onClose={() => {}} />
+
+
+
Hover over close
{}} />
+
+ {}} onClose={() => {}} />
)
@@ -122,7 +134,6 @@ export function PseudoStates(): JSX.Element {
PseudoStates.parameters = {
pseudo: {
- hover: ['#hover .PropertyFilterButton', '#active .PropertyFilterButton'],
- active: ['#active .PropertyFilterButton'],
+ hover: ['#hover .PropertyFilterButton', '#hover-close .PropertyFilterButton', '#hover-close .LemonButton'],
},
}
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx
index eea640e7c1b60..bed6c93160f93 100644
--- a/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx
+++ b/frontend/src/lib/components/PropertyFilters/components/PropertyFilterButton.tsx
@@ -1,9 +1,12 @@
import './PropertyFilterButton.scss'
-import { Button } from 'antd'
+import { LemonButton } from '@posthog/lemon-ui'
+import clsx from 'clsx'
import { useValues } from 'kea'
import { CloseButton } from 'lib/components/CloseButton'
import { PropertyFilterIcon } from 'lib/components/PropertyFilters/components/PropertyFilterIcon'
+import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
+import { IconClose } from 'lib/lemon-ui/icons'
import { KEY_MAPPING } from 'lib/taxonomy'
import { midEllipsis } from 'lib/utils'
import React from 'react'
@@ -19,14 +22,16 @@ export interface PropertyFilterButtonProps {
onClose?: () => void
children?: string
item: AnyPropertyFilter
- style?: React.CSSProperties
}
export const PropertyFilterButton = React.forwardRef
(
- function PropertyFilterButton({ onClick, onClose, children, item, style }, ref): JSX.Element {
+ function PropertyFilterButton({ onClick, onClose, children, item }, ref): JSX.Element {
const { cohortsById } = useValues(cohortsModel)
const { formatPropertyValueForDisplay } = useValues(propertyDefinitionsModel)
+ const is3000 = useFeatureFlag('POSTHOG_3000', 'test')
+ const closable = onClose !== undefined
+ const clickable = onClick !== undefined
const label =
children ||
formatPropertyLabel(
@@ -36,27 +41,47 @@ export const PropertyFilterButton = React.forwardRef formatPropertyValueForDisplay(item.key, s)?.toString() || '?'
)
+ const ButtonComponent = clickable ? 'button' : 'div'
+
return (
-
{midEllipsis(label, 32)}
- {onClose && (
- {
- e.stopPropagation()
- onClose()
- }}
- />
+ {closable && (
+ <>
+ {is3000 ? (
+ }
+ onClick={(e) => {
+ e.stopPropagation()
+ onClose()
+ }}
+ stealth
+ className="p-0.5"
+ status="stealth"
+ />
+ ) : (
+ {
+ e.stopPropagation()
+ onClose()
+ }}
+ />
+ )}
+ >
)}
-
+
)
}
)
diff --git a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx
index 47e0dcd57b105..6c49bbdfb845e 100644
--- a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx
+++ b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx
@@ -170,6 +170,7 @@ export function TaxonomicPropertyFilter({
status={!valuePresent ? 'primary' : 'stealth'}
icon={!valuePresent ? : undefined}
data-attr={'property-select-toggle-' + index}
+ sideIcon={null} // The null sideIcon is here on purpose - it prevents the dropdown caret
onClick={() => (dropdownOpen ? closeDropdown() : openDropdown())}
>
{filter?.type === 'cohort' ? (
diff --git a/frontend/src/lib/components/hedgehogs.tsx b/frontend/src/lib/components/hedgehogs.tsx
index ddc41d7bf8c4c..a657aee5d1898 100644
--- a/frontend/src/lib/components/hedgehogs.tsx
+++ b/frontend/src/lib/components/hedgehogs.tsx
@@ -17,6 +17,7 @@ import laptopHogEU from 'public/hedgehog/laptop-hog-eu.png'
import listHog from 'public/hedgehog/list-hog.png'
import mailHog from 'public/hedgehog/mail-hog.png'
import microphoneHog from 'public/hedgehog/microphone-hog.png'
+import phonePairHogs from 'public/hedgehog/phone-pair-hogs.png'
import policeHog from 'public/hedgehog/police-hog.png'
import professorHog from 'public/hedgehog/professor-hog.png'
import readingHog from 'public/hedgehog/reading-hog.png'
@@ -136,3 +137,6 @@ export const ReadingHog = (props: HedgehogProps): JSX.Element => {
export const MicrophoneHog = (props: HedgehogProps): JSX.Element => {
return
}
+export const PhonePairHogs = (props: HedgehogProps): JSX.Element => {
+ return
+}
diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx
index 74ff3b0ecf1d6..a6d1176087685 100644
--- a/frontend/src/lib/constants.tsx
+++ b/frontend/src/lib/constants.tsx
@@ -189,8 +189,10 @@ export const FEATURE_FLAGS = {
SCHEDULED_CHANGES_FEATURE_FLAGS: 'scheduled-changes-feature-flags', // owner: @jurajmajerik #team-feature-success
ALWAYS_SHOW_SEEKBAR_PREVIEW: 'always-show-seekbar-preview', // owner: #team-replay
SESSION_REPLAY_MOBILE: 'session-replay-mobile', // owner: #team-replay
+ INVITE_TEAM_MEMBER_ONBOARDING: 'invite-team-member-onboarding', // owner: @biancayang
SESSION_REPLAY_IOS: 'session-replay-ios', // owner: #team-replay
YEAR_IN_HOG: 'year-in-hog', // owner: #team-replay
+ SESSION_REPLAY_EXPORT_MOBILE_DATA: 'session-replay-export-mobile-data', // owner: #team-replay
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
diff --git a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss
index 0780aadb3a745..6613a79bde94f 100644
--- a/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss
+++ b/frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.scss
@@ -46,4 +46,10 @@
}
}
}
+
+ .posthog-3000 & {
+ .LemonCalendar__range--boundary {
+ background-color: var(--glass-border-3000);
+ }
+ }
}
diff --git a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx
index 3fb703a5b4081..aa30d604a6a59 100644
--- a/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx
+++ b/frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx
@@ -102,10 +102,14 @@ export function LemonCalendarRangeInline({
className:
date.isSame(rangeStart, 'd') && date.isSame(rangeEnd, 'd')
? props.className
- : clsx(props.className, {
- 'rounded-r-none': date.isSame(rangeStart, 'd') && dayIndex < 6,
- 'rounded-l-none': date.isSame(rangeEnd, 'd') && dayIndex > 0,
- }),
+ : clsx(
+ props.className,
+ {
+ 'rounded-r-none': date.isSame(rangeStart, 'd') && dayIndex < 6,
+ 'rounded-l-none': date.isSame(rangeEnd, 'd') && dayIndex > 0,
+ },
+ 'LemonCalendar__range--boundary'
+ ),
status: 'primary',
type: 'primary',
}
diff --git a/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx b/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx
index c1fe28245c5c6..7d7f1d0502739 100644
--- a/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx
+++ b/frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx
@@ -39,7 +39,7 @@ export const LemonDropdown: React.FunctionComponent {
const [, parentPopoverLevel] = useContext(PopoverOverlayContext)
- const [localVisible, setLocalVisible] = useState(false)
+ const [localVisible, setLocalVisible] = useState(visible ?? false)
const floatingRef = useRef(null)
const referenceRef = useRef(null)
@@ -47,8 +47,8 @@ export const LemonDropdown: React.FunctionComponent {
- onVisibilityChange?.(effectiveVisible)
- }, [effectiveVisible, onVisibilityChange])
+ onVisibilityChange?.(localVisible)
+ }, [localVisible, onVisibilityChange])
return (
table {
+ > thead {
+ text-transform: none;
+ letter-spacing: normal;
+ }
+ }
+ }
+
&.LemonTable--inset {
--row-horizontal-padding: 0.5rem;
}
diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx
index 398f3d8dd735b..a6f44f57c84af 100644
--- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx
+++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx
@@ -223,8 +223,10 @@ export function LemonTable>({
stealth && 'LemonTable--stealth',
isScrollableLeft && 'scrollable--left',
isScrollableRight && 'scrollable--right',
+ !uppercaseHeader && 'LemonTable--lowercase-header',
className
)}
+ // eslint-disable-next-line react/forbid-dom-props
style={style}
data-attr={dataAttr}
>
@@ -232,17 +234,14 @@ export function LemonTable>({
- {!!expandable && /* Expand/collapse column */}
+ {!!expandable && /* Expand/collapse column */}
{columns.map((column, index) => (
+ // eslint-disable-next-line react/forbid-dom-props
))}
{showHeader && (
-
+
{columnGroups.some((group) => group.title) && (
{!!expandable && /* Expand/collapse */}
diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss
index fd474dbc2e865..e3f81c245f92e 100644
--- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss
+++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss
@@ -15,7 +15,7 @@
font-weight: normal;
}
- .posthog-3000 &:not(.breakdown) {
+ .posthog-3000 &:not(.LemonTag--breakdown) {
padding: 0.075rem 0.25rem;
font-size: 0.688rem;
background: none;
@@ -115,7 +115,7 @@
background: none;
}
- &.breakdown {
+ &.LemonTag--breakdown {
padding: 8px 12px;
font-size: 14px;
font-weight: 400;
diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx
index bdb8fa03531a0..9c1213d323b14 100644
--- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx
+++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.stories.tsx
@@ -1,8 +1,8 @@
-import { Meta, StoryFn, StoryObj } from '@storybook/react'
+import { Meta, StoryObj } from '@storybook/react'
+import { BreakdownTag as BreakdownTagComponent } from 'scenes/insights/filters/BreakdownFilter/BreakdownTag'
import { LemonTag as LemonTagComponent, LemonTagType } from './LemonTag'
-type Story = StoryObj
const meta: Meta = {
title: 'Lemon UI/Lemon Tag',
component: LemonTagComponent,
@@ -13,7 +13,9 @@ const meta: Meta = {
},
},
}
+
export default meta
+type Story = StoryObj
const ALL_COLORS: LemonTagType[] = [
'primary',
@@ -29,17 +31,26 @@ const ALL_COLORS: LemonTagType[] = [
'none',
]
-const Template: StoryFn = (props) => {
- return (
+export const LemonTag: Story = {
+ render: () => (
{ALL_COLORS.map((type) => (
-
+
{type}
))}
- )
+ ),
}
-export const LemonTag: Story = Template.bind({})
-LemonTag.args = {}
+export const BreakdownTag: Story = {
+ render: () => (
+ <>
+
+
+
+
+
+ >
+ ),
+}
diff --git a/frontend/src/lib/lemon-ui/Popover/Popover.tsx b/frontend/src/lib/lemon-ui/Popover/Popover.tsx
index 5850612f61a4e..0541bbade9e36 100644
--- a/frontend/src/lib/lemon-ui/Popover/Popover.tsx
+++ b/frontend/src/lib/lemon-ui/Popover/Popover.tsx
@@ -17,7 +17,7 @@ import clsx from 'clsx'
import { useEventListener } from 'lib/hooks/useEventListener'
import { useFloatingContainerContext } from 'lib/hooks/useFloatingContainerContext'
import { CLICK_OUTSIDE_BLOCK_CLASS, useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler'
-import React, { MouseEventHandler, ReactElement, useContext, useEffect, useLayoutEffect, useRef } from 'react'
+import React, { MouseEventHandler, ReactElement, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { CSSTransition } from 'react-transition-group'
export interface PopoverProps {
@@ -136,8 +136,9 @@ export const Popover = React.forwardRef(function P
...(middleware ?? []),
],
})
+
+ const [floatingElement, setFloatingElement] = useState(null)
const mergedReferenceRef = useMergeRefs([referenceRef, extraReferenceRef || null]) as React.RefCallback
- const mergedFloatingRef = useMergeRefs([floatingRef, extraFloatingRef || null]) as React.RefCallback
const arrowStyle = middlewareData.arrow
? {
@@ -177,10 +178,10 @@ export const Popover = React.forwardRef(function P
)
useEffect(() => {
- if (visible && referenceRef?.current && floatingRef?.current) {
- return autoUpdate(referenceRef.current, floatingRef.current, update)
+ if (visible && referenceRef?.current && floatingElement) {
+ return autoUpdate(referenceRef.current, floatingElement, update)
}
- }, [visible, referenceRef?.current, floatingRef?.current, ...additionalRefs])
+ }, [visible, referenceRef?.current, floatingElement, ...additionalRefs])
const floatingContainer = useFloatingContainerContext()?.current
@@ -224,7 +225,13 @@ export const Popover = React.forwardRef(function P
className
)}
data-placement={effectivePlacement}
- ref={mergedFloatingRef}
+ ref={(el) => {
+ setFloatingElement(el)
+ floatingRef.current = el
+ if (extraFloatingRef) {
+ extraFloatingRef.current = el
+ }
+ }}
// eslint-disable-next-line react/forbid-dom-props
style={{
display: middlewareData.hide?.referenceHidden ? 'none' : undefined,
diff --git a/frontend/src/lib/lemon-ui/colors.stories.tsx b/frontend/src/lib/lemon-ui/colors.stories.tsx
index 304661610cb2e..69ab38231380c 100644
--- a/frontend/src/lib/lemon-ui/colors.stories.tsx
+++ b/frontend/src/lib/lemon-ui/colors.stories.tsx
@@ -94,7 +94,7 @@ export function ColorPalette(): JSX.Element {
return (
{Object.keys(colorGroups).map((group) => (
-
+
{group}
{colorGroups[group].map((color: string) => (
diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx
index 94958d5e17fc8..947a9ae3da444 100644
--- a/frontend/src/lib/utils.tsx
+++ b/frontend/src/lib/utils.tsx
@@ -1691,3 +1691,19 @@ export const shouldIgnoreInput = (e: KeyboardEvent): boolean => {
false
)
}
+
+export const base64Encode = (str: string): string => {
+ const data = new TextEncoder().encode(str)
+ const binString = Array.from(data, (byte) => String.fromCharCode(byte)).join('')
+ return btoa(binString)
+}
+
+export const base64Decode = (encodedString: string): string => {
+ const binString = atob(encodedString)
+ const data = new Uint8Array(binString.length)
+ for (let i = 0; i < binString.length; i++) {
+ data[i] = binString.charCodeAt(i)
+ }
+
+ return new TextDecoder().decode(data)
+}
diff --git a/frontend/src/lib/utils/file-utils.ts b/frontend/src/lib/utils/file-utils.ts
new file mode 100644
index 0000000000000..e5eb293ce0bb7
--- /dev/null
+++ b/frontend/src/lib/utils/file-utils.ts
@@ -0,0 +1,38 @@
+export function selectFiles(options: { contentType: string; multiple: boolean }): Promise
{
+ return new Promise((resolve, reject) => {
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.multiple = options.multiple
+ input.accept = options.contentType
+
+ input.onchange = () => {
+ if (!input.files) {
+ return resolve([])
+ }
+ const files = Array.from(input.files)
+ resolve(files)
+ }
+
+ input.oncancel = () => {
+ resolve([])
+ }
+ input.onerror = () => {
+ reject(new Error('Error selecting file'))
+ }
+
+ input.click()
+ })
+}
+
+export function getTextFromFile(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const filereader = new FileReader()
+ filereader.onload = (e) => {
+ resolve(e.target?.result as string)
+ }
+ filereader.onerror = (e) => {
+ reject(e)
+ }
+ filereader.readAsText(file)
+ })
+}
diff --git a/frontend/src/mocks/fixtures/_billing_v2.json b/frontend/src/mocks/fixtures/_billing_v2.json
index 64f31e5c16d16..cc8fbfd6a1522 100644
--- a/frontend/src/mocks/fixtures/_billing_v2.json
+++ b/frontend/src/mocks/fixtures/_billing_v2.json
@@ -737,10 +737,10 @@
"unit_amount_usd": null,
"current_amount_usd_before_addons": "0.00",
"current_amount_usd": "0.00",
- "current_usage": 1003478,
+ "current_usage": 1603478,
"usage_limit": 3624548,
"has_exceeded_limit": false,
- "percentage_usage": 0.2768560383253305,
+ "percentage_usage": 0.4423939206,
"projected_usage": 5469181,
"projected_amount_usd": "1230.57",
"unit": "event",
diff --git a/frontend/src/models/notebooksModel.ts b/frontend/src/models/notebooksModel.ts
index b957473000bef..1678704bcf545 100644
--- a/frontend/src/models/notebooksModel.ts
+++ b/frontend/src/models/notebooksModel.ts
@@ -5,7 +5,7 @@ import api from 'lib/api'
import { deleteWithUndo } from 'lib/utils/deleteWithUndo'
import posthog from 'posthog-js'
import { notebookLogic } from 'scenes/notebooks/Notebook/notebookLogic'
-import { notebookLogicType } from 'scenes/notebooks/Notebook/notebookLogicType'
+import type { notebookLogicType } from 'scenes/notebooks/Notebook/notebookLogicType'
import { defaultNotebookContent, EditorFocusPosition, JSONContent } from 'scenes/notebooks/Notebook/utils'
import { notebookPanelLogic } from 'scenes/notebooks/NotebookPanel/notebookPanelLogic'
import { LOCAL_NOTEBOOK_TEMPLATES } from 'scenes/notebooks/NotebookTemplates/notebookTemplates'
@@ -28,7 +28,7 @@ export const SCRATCHPAD_NOTEBOOK: NotebookListItemType = {
export const openNotebook = async (
notebookId: string,
target: NotebookTarget,
- focus: EditorFocusPosition | undefined = undefined,
+ autofocus: EditorFocusPosition | undefined = undefined,
// operations to run against the notebook once it has opened and the editor is ready
onOpen: (logic: BuiltLogic) => void = () => {}
): Promise => {
@@ -36,7 +36,7 @@ export const openNotebook = async (
const thePanelLogic = notebookPanelLogic.findMounted()
if (thePanelLogic && target === NotebookTarget.Popover) {
- notebookPanelLogic.actions.selectNotebook(notebookId, focus)
+ notebookPanelLogic.actions.selectNotebook(notebookId, { autofocus })
} else {
if (router.values.location.pathname === urls.notebook('new')) {
router.actions.replace(urls.notebook(notebookId))
@@ -54,10 +54,10 @@ export const openNotebook = async (
unmount()
}
}
+
export const notebooksModel = kea([
path(['scenes', 'notebooks', 'Notebook', 'notebooksModel']),
actions({
- setScratchpadNotebook: (notebook: NotebookListItemType) => ({ notebook }),
createNotebook: (
location: NotebookTarget,
title?: string,
@@ -79,12 +79,7 @@ export const notebooksModel = kea([
}),
reducers({
- scratchpadNotebook: [
- SCRATCHPAD_NOTEBOOK,
- {
- setScratchpadNotebook: (_, { notebook }) => notebook,
- },
- ],
+ scratchpadNotebook: [SCRATCHPAD_NOTEBOOK],
}),
loaders(({ values }) => ({
@@ -116,7 +111,11 @@ export const notebooksModel = kea([
object: { name: title || shortId, id: shortId },
})
- notebookPanelLogic.findMounted()?.actions.selectNotebook(SCRATCHPAD_NOTEBOOK.short_id)
+ const panelLogic = notebookPanelLogic.findMounted()
+
+ if (panelLogic && panelLogic.values.selectedNotebook === shortId) {
+ panelLogic.actions.selectNotebook(SCRATCHPAD_NOTEBOOK.short_id, { silent: true })
+ }
return values.notebooks.filter((n) => n.short_id !== shortId)
},
diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts
index ae185ba6a1e03..3e0349c7a25ce 100644
--- a/frontend/src/queries/examples.ts
+++ b/frontend/src/queries/examples.ts
@@ -321,7 +321,7 @@ const HogQLForDataVisualization: HogQLQuery = {
kind: NodeKind.HogQLQuery,
query: `select toDate(timestamp) as timestamp, count()
from events
-where {filters}
+where {filters} and timestamp <= now()
group by timestamp
order by timestamp asc
limit 100`,
diff --git a/frontend/src/queries/nodes/DataNode/LoadNext.tsx b/frontend/src/queries/nodes/DataNode/LoadNext.tsx
index 3c3f7ab1fc9fe..714d391ce4ba7 100644
--- a/frontend/src/queries/nodes/DataNode/LoadNext.tsx
+++ b/frontend/src/queries/nodes/DataNode/LoadNext.tsx
@@ -9,13 +9,22 @@ interface LoadNextProps {
query: DataNode
}
export function LoadNext({ query }: LoadNextProps): JSX.Element {
- const { nextDataLoading } = useValues(dataNodeLogic)
+ const { canLoadNextData, nextDataLoading, numberOfRows } = useValues(dataNodeLogic)
const { loadNextData } = useActions(dataNodeLogic)
return (
-
- Load more {isPersonsNode(query) || isPersonsQuery(query) ? 'people' : 'events'}
+
+ Showing {canLoadNextData || numberOfRows === 1 ? '' : 'all '}
+ {numberOfRows === 1 ? 'one' : numberOfRows}{' '}
+ {isPersonsNode(query) || isPersonsQuery(query)
+ ? numberOfRows === 1
+ ? 'person'
+ : 'people'
+ : numberOfRows === 1
+ ? 'event'
+ : 'events'}
+ {canLoadNextData ? '. Click to load more.' : '. Reached the end of results.'}
)
diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts
index fbe91ff1a4653..c76387b9ed08e 100644
--- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts
+++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.test.ts
@@ -240,6 +240,7 @@ describe('dataNodeLogic', () => {
kind: NodeKind.EventsQuery,
select: ['*', 'event', 'timestamp'],
before: '2022-12-24T17:00:41.165000Z',
+ limit: 100,
},
response: partial({ results }),
})
@@ -268,6 +269,7 @@ describe('dataNodeLogic', () => {
kind: NodeKind.EventsQuery,
select: ['*', 'event', 'timestamp'],
before: '2022-12-24T17:00:41.165000Z',
+ limit: 100,
},
response: partial({ results }),
})
@@ -280,6 +282,7 @@ describe('dataNodeLogic', () => {
kind: NodeKind.EventsQuery,
select: ['*', 'event', 'timestamp'],
before: '2022-12-23T17:00:41.165000Z',
+ limit: 100,
},
response: partial({ results: [...results, ...results2] }),
})
diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts
index 0914fdf73fd33..c733efa7e3e75 100644
--- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts
+++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts
@@ -33,6 +33,8 @@ import {
DataNode,
EventsQuery,
EventsQueryResponse,
+ InsightVizNode,
+ NodeKind,
PersonsNode,
PersonsQuery,
PersonsQueryResponse,
@@ -41,6 +43,7 @@ import {
} from '~/queries/schema'
import {
isEventsQuery,
+ isInsightPersonsQuery,
isInsightQueryNode,
isLifecycleQuery,
isPersonsNode,
@@ -61,6 +64,7 @@ export interface DataNodeLogicProps {
}
const AUTOLOAD_INTERVAL = 30000
+const LOAD_MORE_ROWS_LIMIT = 10000
const queryEqual = (a: DataNode, b: DataNode): boolean => {
if (isInsightQueryNode(a) && isInsightQueryNode(b)) {
@@ -403,7 +407,14 @@ export const dataNodeLogic = kea([
if (sortColumnIndex !== -1) {
const lastTimestamp = typedResults?.[typedResults.length - 1]?.[sortColumnIndex]
if (lastTimestamp) {
- const newQuery: EventsQuery = { ...query, before: lastTimestamp }
+ const newQuery: EventsQuery = {
+ ...query,
+ before: lastTimestamp,
+ limit: Math.max(
+ 100,
+ Math.min(2 * (typedResults?.length || 100), LOAD_MORE_ROWS_LIMIT)
+ ),
+ }
return newQuery
}
}
@@ -411,6 +422,7 @@ export const dataNodeLogic = kea([
return {
...query,
offset: typedResults?.length || 0,
+ limit: Math.max(100, Math.min(2 * (typedResults?.length || 100), LOAD_MORE_ROWS_LIMIT)),
} as EventsQuery | PersonsQuery
}
}
@@ -431,6 +443,21 @@ export const dataNodeLogic = kea([
(s) => [s.nextQuery, s.isShowingCachedResults],
(nextQuery, isShowingCachedResults) => (isShowingCachedResults ? false : !!nextQuery),
],
+ backToSourceQuery: [
+ (s) => [s.query],
+ (query): InsightVizNode | null => {
+ if (isPersonsQuery(query) && isInsightPersonsQuery(query.source) && !!query.source.source) {
+ const insightQuery = query.source.source
+ const insightVizNode: InsightVizNode = {
+ kind: NodeKind.InsightVizNode,
+ source: insightQuery,
+ full: true,
+ }
+ return insightVizNode
+ }
+ return null
+ },
+ ],
autoLoadRunning: [
(s) => [s.autoLoadToggled, s.autoLoadStarted, s.dataLoading],
(autoLoadToggled, autoLoadStarted, dataLoading) => autoLoadToggled && autoLoadStarted && !dataLoading,
@@ -478,6 +505,21 @@ export const dataNodeLogic = kea([
return response && 'timings' in response ? response.timings : null
},
],
+ numberOfRows: [
+ (s) => [s.response],
+ (response): number | null => {
+ if (!response) {
+ return null
+ }
+ const fields = ['result', 'results']
+ for (const field of fields) {
+ if (field in response && Array.isArray(response[field])) {
+ return response[field].length
+ }
+ }
+ return null
+ },
+ ],
}),
listeners(({ actions, values, cache }) => ({
abortAnyRunningQuery: () => {
diff --git a/frontend/src/queries/nodes/DataTable/BackToSource.tsx b/frontend/src/queries/nodes/DataTable/BackToSource.tsx
new file mode 100644
index 0000000000000..3ca01e12904c2
--- /dev/null
+++ b/frontend/src/queries/nodes/DataTable/BackToSource.tsx
@@ -0,0 +1,41 @@
+import { useValues } from 'kea'
+import { router } from 'kea-router'
+import { LemonButton } from 'lib/lemon-ui/LemonButton'
+import { summarizeInsightQuery } from 'scenes/insights/summarizeInsight'
+import { mathsLogic } from 'scenes/trends/mathsLogic'
+import { urls } from 'scenes/urls'
+
+import { cohortsModel } from '~/models/cohortsModel'
+import { groupsModel } from '~/models/groupsModel'
+import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic'
+
+export function BackToSource(): JSX.Element | null {
+ const { aggregationLabel } = useValues(groupsModel)
+ const { cohortsById } = useValues(cohortsModel)
+ const { mathDefinitions } = useValues(mathsLogic)
+
+ const { backToSourceQuery } = useValues(dataNodeLogic)
+
+ if (!backToSourceQuery) {
+ return null
+ }
+
+ const summary = summarizeInsightQuery(backToSourceQuery.source, {
+ aggregationLabel,
+ cohortsById,
+ mathDefinitions,
+ })
+
+ return (
+
+ router.actions.push(urls.insightNew(undefined, undefined, JSON.stringify(backToSourceQuery)))
+ }
+ >
+ « Back to {backToSourceQuery.source.kind?.replace('Query', '') ?? 'Insight'}
+
+ )
+}
diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx
index a4ead65cdfb73..114d8811494f1 100644
--- a/frontend/src/queries/nodes/DataTable/DataTable.tsx
+++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx
@@ -19,6 +19,7 @@ import { DateRange } from '~/queries/nodes/DataNode/DateRange'
import { ElapsedTime } from '~/queries/nodes/DataNode/ElapsedTime'
import { LoadNext } from '~/queries/nodes/DataNode/LoadNext'
import { Reload } from '~/queries/nodes/DataNode/Reload'
+import { BackToSource } from '~/queries/nodes/DataTable/BackToSource'
import { ColumnConfigurator } from '~/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator'
import { DataTableExport } from '~/queries/nodes/DataTable/DataTableExport'
import { dataTableLogic, DataTableLogicProps, DataTableRow } from '~/queries/nodes/DataTable/dataTableLogic'
@@ -97,11 +98,11 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }
responseLoading,
responseError,
queryCancelled,
- canLoadNextData,
canLoadNewData,
nextDataLoading,
newDataLoading,
highlightedRows,
+ backToSourceQuery,
} = useValues(builtDataNodeLogic)
const dataTableLogicProps: DataTableLogicProps = { query, vizKey: vizKey, dataKey: dataKey, context }
@@ -375,6 +376,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }
)
const firstRowLeft = [
+ backToSourceQuery ? : null,
showDateRange && sourceFeatures.has(QueryFeature.dateRangePicker) ? (
) : null,
@@ -553,12 +555,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }
result && result[0] && result[0]['event'] === '$exception',
})
}
- footer={
- canLoadNextData &&
- ((response as any).results.length > 0 ||
- (response as any).result.length > 0 ||
- !responseLoading) &&
- }
+ footer={(dataTableRows ?? []).length > 0 ? : null}
onRow={context?.rowProps}
/>
)}
diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx
index ab0a886cf8e0e..ac141f208630c 100644
--- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx
+++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx
@@ -206,7 +206,6 @@ export function renderColumn(
return
} else if (key === 'person') {
const personRecord = record as PersonType
-
const displayProps: PersonDisplayProps = {
withIcon: true,
person: record as PersonType,
@@ -222,8 +221,11 @@ export function renderColumn(
displayProps.href = urls.personByDistinctId(personRecord.distinct_ids[0])
}
- if (isPersonsQuery(query.source)) {
- displayProps.href = urls.personByUUID(personRecord.id ?? '-')
+ if (isPersonsQuery(query.source) && value) {
+ displayProps.person = value
+ displayProps.href = value.id
+ ? urls.personByUUID(value.id)
+ : urls.personByDistinctId(value.distinct_ids?.[0] ?? '-')
}
return
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Chart.tsx b/frontend/src/queries/nodes/DataVisualization/Components/Chart.tsx
index 7493f85945cc3..1865f8a9f4018 100644
--- a/frontend/src/queries/nodes/DataVisualization/Components/Chart.tsx
+++ b/frontend/src/queries/nodes/DataVisualization/Components/Chart.tsx
@@ -4,7 +4,7 @@ import { useValues } from 'kea'
import { dataVisualizationLogic } from '../dataVisualizationLogic'
import { LineGraph } from './Charts/LineGraph'
-import { ChartSelection } from './ChartSelection'
+import { SideBar } from './SideBar'
export const Chart = (): JSX.Element => {
const { showEditingUI } = useValues(dataVisualizationLogic)
@@ -13,7 +13,7 @@ export const Chart = (): JSX.Element => {
{showEditingUI && (
-
+
)}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.scss b/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.scss
deleted file mode 100644
index 0d331d249cf7a..0000000000000
--- a/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-@import '../../../../styles/mixins';
-
-.DataVisualization {
- .ChartSelectionWrapper {
- min-width: 20vw;
- height: var(--viz-min-height);
- border-radius: var(--radius);
- }
-}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.tsx b/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.tsx
deleted file mode 100644
index 62ae5cb629aff..0000000000000
--- a/frontend/src/queries/nodes/DataVisualization/Components/ChartSelection.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import './ChartSelection.scss'
-
-import { LemonButton, LemonLabel, LemonSelect } from '@posthog/lemon-ui'
-import { useActions, useValues } from 'kea'
-import { IconDelete, IconPlusMini } from 'lib/lemon-ui/icons'
-
-import { dataNodeLogic } from '../../DataNode/dataNodeLogic'
-import { dataVisualizationLogic } from '../dataVisualizationLogic'
-
-export const ChartSelection = (): JSX.Element => {
- const { columns, selectedXIndex, selectedYIndexes } = useValues(dataVisualizationLogic)
- const { responseLoading } = useValues(dataNodeLogic)
- const { updateXSeries, updateYSeries, addYSeries, deletedYSeries } = useActions(dataVisualizationLogic)
-
- const options = columns.map(({ name, type }) => ({
- value: name,
- label: `${name} - ${type}`,
- }))
-
- return (
-
-
-
X-axis
-
{
- const columnIndex = options.findIndex((n) => n.value === value)
- updateXSeries(columnIndex)
- }}
- />
- Y-axis
- {(selectedYIndexes ?? [null]).map((selectedYIndex, index) => (
-
- {
- const columnIndex = options.findIndex((n) => n.value === value)
- updateYSeries(index, columnIndex)
- }}
- />
- }
- status="primary-alt"
- title="Delete Y-series"
- noPadding
- onClick={() => deletedYSeries(index)}
- />
-
- ))}
- addYSeries()}
- icon={ }
- fullWidth
- >
- Add Y-series
-
-
-
- )
-}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx b/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx
index 58879042f15e8..0d2e76f3d8ff1 100644
--- a/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx
+++ b/frontend/src/queries/nodes/DataVisualization/Components/Charts/LineGraph.tsx
@@ -5,6 +5,7 @@ import '../../../../../scenes/insights/InsightTooltip/InsightTooltip.scss'
import { LemonTable } from '@posthog/lemon-ui'
import { ChartData, ChartType, Color, GridLineOptions, TickOptions, TooltipModel } from 'chart.js'
+import annotationPlugin, { AnnotationPluginOptions, LineAnnotationOptions } from 'chartjs-plugin-annotation'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import clsx from 'clsx'
import { useValues } from 'kea'
@@ -18,6 +19,7 @@ import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { ChartDisplayType, GraphType } from '~/types'
import { dataVisualizationLogic } from '../../dataVisualizationLogic'
+import { displayLogic } from '../../displayLogic'
export const LineGraph = (): JSX.Element => {
const canvasRef = useRef
(null)
@@ -29,13 +31,15 @@ export const LineGraph = (): JSX.Element => {
const { xData, yData, presetChartHeight, visualizationType } = useValues(dataVisualizationLogic)
const isBarChart = visualizationType === ChartDisplayType.ActionsBar
+ const { goalLines } = useValues(displayLogic)
+
useEffect(() => {
if (!xData || !yData) {
return
}
const data: ChartData = {
- labels: xData,
+ labels: xData.data,
datasets: yData.map(({ data }, index) => {
const color = getSeriesColor(index)
@@ -54,6 +58,28 @@ export const LineGraph = (): JSX.Element => {
}),
}
+ const annotations = goalLines.reduce(
+ (acc, cur, curIndex) => {
+ const line: LineAnnotationOptions = {
+ label: {
+ display: true,
+ content: cur.label,
+ position: 'end',
+ },
+ scaleID: 'y',
+ value: cur.value,
+ }
+
+ acc.annotations[`line${curIndex}`] = {
+ type: 'line',
+ ...line,
+ }
+
+ return acc
+ },
+ { annotations: {} } as AnnotationPluginOptions
+ )
+
const tickOptions: Partial = {
color: colors.axisLabel as Color,
font: {
@@ -99,6 +125,7 @@ export const LineGraph = (): JSX.Element => {
legend: {
display: false,
},
+ annotation: annotations,
...(isBarChart
? { crosshair: false }
: {
@@ -146,13 +173,13 @@ export const LineGraph = (): JSX.Element => {
tooltipRoot.render(
({
- series: seriesLabel,
+ dataSource={yData.map(({ data, column }) => ({
+ series: column.name,
data: data[referenceDataPoint.dataIndex],
}))}
columns={[
{
- title: xData[referenceDataPoint.dataIndex],
+ title: xData.data[referenceDataPoint.dataIndex],
dataIndex: 'series',
render: (value) => {
return (
@@ -233,6 +260,7 @@ export const LineGraph = (): JSX.Element => {
},
}
+ Chart.register(annotationPlugin)
const newChart = new Chart(canvasRef.current?.getContext('2d') as ChartItem, {
type: isBarChart ? GraphType.Bar : GraphType.Line,
data,
@@ -240,7 +268,7 @@ export const LineGraph = (): JSX.Element => {
plugins: [ChartDataLabels],
})
return () => newChart.destroy()
- }, [xData, yData, visualizationType])
+ }, [xData, yData, visualizationType, goalLines])
return (
{
+ const { goalLines } = useValues(displayLogic)
+ const { addGoalLine, updateGoalLine, removeGoalLine } = useActions(displayLogic)
+
+ return (
+
+
Goal Line
+
+ {goalLines.map((goalLine, goalLineIndex) => (
+
+
+ updateGoalLine(goalLineIndex, 'label', value)}
+ />
+ updateGoalLine(goalLineIndex, 'value', parseInt(value))}
+ />
+ }
+ status="primary-alt"
+ title="Delete Y-series"
+ noPadding
+ onClick={() => removeGoalLine(goalLineIndex)}
+ />
+
+ ))}
+
+
addGoalLine()}
+ icon={ }
+ fullWidth
+ >
+ Add goal line
+
+
+ )
+}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx b/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx
new file mode 100644
index 0000000000000..2f65ed6a5a05f
--- /dev/null
+++ b/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx
@@ -0,0 +1,69 @@
+import { LemonButton, LemonLabel, LemonSelect } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { IconDelete, IconPlusMini } from 'lib/lemon-ui/icons'
+
+import { dataNodeLogic } from '../../DataNode/dataNodeLogic'
+import { dataVisualizationLogic } from '../dataVisualizationLogic'
+
+export const SeriesTab = (): JSX.Element => {
+ const { columns, xData, yData } = useValues(dataVisualizationLogic)
+ const { responseLoading } = useValues(dataNodeLogic)
+ const { updateXSeries, updateYSeries, addYSeries, deleteYSeries } = useActions(dataVisualizationLogic)
+
+ const options = columns.map(({ name, label }) => ({
+ value: name,
+ label,
+ }))
+
+ return (
+
+
X-axis
+
{
+ const column = columns.find((n) => n.name === value)
+ if (column) {
+ updateXSeries(column.name)
+ }
+ }}
+ />
+ Y-axis
+ {(yData ?? [null]).map((series, index) => (
+
+ {
+ const column = columns.find((n) => n.name === value)
+ if (column) {
+ updateYSeries(index, column.name)
+ }
+ }}
+ />
+ }
+ status="primary-alt"
+ title="Delete Y-series"
+ noPadding
+ onClick={() => deleteYSeries(index)}
+ />
+
+ ))}
+ addYSeries()}
+ icon={ }
+ fullWidth
+ >
+ Add Y-series
+
+
+ )
+}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/SideBar.scss b/frontend/src/queries/nodes/DataVisualization/Components/SideBar.scss
new file mode 100644
index 0000000000000..a9962f8c5d265
--- /dev/null
+++ b/frontend/src/queries/nodes/DataVisualization/Components/SideBar.scss
@@ -0,0 +1,26 @@
+@import '../../../../styles/mixins';
+
+.DataVisualization {
+ .LemonTabs .LemonTabs__bar {
+ margin-bottom: 0;
+ }
+
+ .SideBar {
+ min-width: 18rem;
+ min-height: var(--viz-min-height);
+ border-radius: var(--radius);
+
+ .LemonInput.LemonInput--medium {
+ display: flex;
+
+ .LemonInput__input {
+ flex: 1;
+ width: 0;
+ }
+ }
+
+ .grow-2 {
+ flex-grow: 2;
+ }
+ }
+}
diff --git a/frontend/src/queries/nodes/DataVisualization/Components/SideBar.tsx b/frontend/src/queries/nodes/DataVisualization/Components/SideBar.tsx
new file mode 100644
index 0000000000000..170b70ccdb878
--- /dev/null
+++ b/frontend/src/queries/nodes/DataVisualization/Components/SideBar.tsx
@@ -0,0 +1,41 @@
+import './SideBar.scss'
+
+import { LemonTabs } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+
+import { dataVisualizationLogic, SideBarTab } from '../dataVisualizationLogic'
+import { DisplayTab } from './DisplayTab'
+import { SeriesTab } from './SeriesTab'
+
+const TABS_TO_CONTENT = {
+ [SideBarTab.Series]: {
+ label: 'Series',
+ content:
,
+ },
+ [SideBarTab.Display]: {
+ label: 'Display',
+ content:
,
+ },
+}
+
+const ContentWrapper = ({ children }: { children: JSX.Element }): JSX.Element => {
+ return
{children}
+}
+
+export const SideBar = (): JSX.Element => {
+ const { activeSideBarTab } = useValues(dataVisualizationLogic)
+ const { setSideBarTab } = useActions(dataVisualizationLogic)
+
+ return (
+
setSideBarTab(tab as SideBarTab)}
+ borderless
+ tabs={Object.values(TABS_TO_CONTENT).map((tab, index) => ({
+ label: tab.label,
+ key: Object.keys(TABS_TO_CONTENT)[index],
+ content: {tab.content} ,
+ }))}
+ />
+ )
+}
diff --git a/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx b/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx
index b8c7b43b27e42..72dd7c18f600d 100644
--- a/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx
+++ b/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx
@@ -17,6 +17,7 @@ import { HogQLQueryEditor } from '../HogQLQuery/HogQLQueryEditor'
import { Chart } from './Components/Chart'
import { TableDisplay } from './Components/TableDisplay'
import { dataVisualizationLogic, DataVisualizationLogicProps } from './dataVisualizationLogic'
+import { displayLogic } from './displayLogic'
interface DataTableVisualizationProps {
uniqueKey?: string | number
@@ -82,43 +83,45 @@ export function DataTableVisualization(props: DataTableVisualizationProps): JSX.
return (
-
-
- {showEditingUI && (
- <>
-
- {sourceFeatures.has(QueryFeature.dateRangePicker) && (
-
-
{
- if (query.kind === NodeKind.HogQLQuery) {
- setQuerySource(query)
- }
- }}
- />
+
+
+
+ {showEditingUI && (
+ <>
+
+ {sourceFeatures.has(QueryFeature.dateRangePicker) && (
+
+ {
+ if (query.kind === NodeKind.HogQLQuery) {
+ setQuerySource(query)
+ }
+ }}
+ />
+
+ )}
+ >
+ )}
+ {showResultControls && (
+ <>
+
+
- )}
- >
- )}
- {showResultControls && (
- <>
-
-
- >
- )}
- {component}
+ >
+ )}
+ {component}
+
-
+
)
diff --git a/frontend/src/queries/nodes/DataVisualization/dataVisualizationLogic.ts b/frontend/src/queries/nodes/DataVisualization/dataVisualizationLogic.ts
index 52f5677b38d41..ae8103da14f09 100644
--- a/frontend/src/queries/nodes/DataVisualization/dataVisualizationLogic.ts
+++ b/frontend/src/queries/nodes/DataVisualization/dataVisualizationLogic.ts
@@ -3,7 +3,7 @@ import { subscriptions } from 'kea-subscriptions'
import { insightSceneLogic } from 'scenes/insights/insightSceneLogic'
import { teamLogic } from 'scenes/teamLogic'
-import { AnyResponseType, DataVisualizationNode } from '~/queries/schema'
+import { AnyResponseType, ChartAxis, DataVisualizationNode } from '~/queries/schema'
import { QueryContext } from '~/queries/types'
import { ChartDisplayType, InsightLogicProps, ItemMode } from '~/types'
@@ -11,8 +11,20 @@ import { dataNodeLogic } from '../DataNode/dataNodeLogic'
import { getQueryFeatures, QueryFeature } from '../DataTable/queryFeatures'
import type { dataVisualizationLogicType } from './dataVisualizationLogicType'
-export interface AxisSeries
{
+export enum SideBarTab {
+ Series = 'series',
+ Display = 'display',
+}
+
+export interface Column {
name: string
+ type: string
+ label: string
+ dataIndex: number
+}
+
+export interface AxisSeries {
+ column: Column
data: T[]
}
@@ -35,21 +47,22 @@ export const dataVisualizationLogic = kea([
props({ query: {} } as DataVisualizationLogicProps),
actions({
setVisualizationType: (visualizationType: ChartDisplayType) => ({ visualizationType }),
- updateXSeries: (columnIndex: number) => ({
- selectedXSeriesColumnIndex: columnIndex,
+ updateXSeries: (columnName: string) => ({
+ columnName,
}),
- updateYSeries: (seriesIndex: number, columnIndex: number) => ({
+ updateYSeries: (seriesIndex: number, columnName: string) => ({
seriesIndex,
- selectedYSeriesColumnIndex: columnIndex,
+ columnName,
}),
- addYSeries: (columnIndex?: number) => ({ columnIndex }),
- deletedYSeries: (seriesIndex: number) => ({ seriesIndex }),
+ addYSeries: (columnName?: string) => ({ columnName }),
+ deleteYSeries: (seriesIndex: number) => ({ seriesIndex }),
clearAxis: true,
setQuery: (node: DataVisualizationNode) => ({ node }),
+ setSideBarTab: (tab: SideBarTab) => ({ tab }),
}),
reducers({
columns: [
- [] as { name: string; type: string }[],
+ [] as Column[],
{
loadDataSuccess: (_state, { response }) => {
if (!response) {
@@ -64,6 +77,8 @@ export const dataVisualizationLogic = kea([
return {
name: column,
type,
+ label: `${column} - ${type}`,
+ dataIndex: index,
}
})
},
@@ -75,52 +90,61 @@ export const dataVisualizationLogic = kea([
setVisualizationType: (_, { visualizationType }) => visualizationType,
},
],
- selectedXIndex: [
- null as number | null,
+ selectedXAxis: [
+ null as string | null,
{
clearAxis: () => null,
- updateXSeries: (_, { selectedXSeriesColumnIndex }) => selectedXSeriesColumnIndex,
+ updateXSeries: (_, { columnName }) => columnName,
},
],
- selectedYIndexes: [
- null as (number | null)[] | null,
+ selectedYAxis: [
+ null as (string | null)[] | null,
{
clearAxis: () => null,
- addYSeries: (prev, { columnIndex }) => {
- if (!prev && columnIndex !== undefined) {
- return [columnIndex]
+ addYSeries: (state, { columnName }) => {
+ if (!state && columnName !== undefined) {
+ return [columnName]
}
- if (!prev) {
+ if (!state) {
return [null]
}
- prev.push(columnIndex === undefined ? null : columnIndex)
- return [...prev]
+ return [...state, columnName === undefined ? null : columnName]
},
- updateYSeries: (prev, { seriesIndex, selectedYSeriesColumnIndex }) => {
- if (!prev) {
+ updateYSeries: (state, { seriesIndex, columnName }) => {
+ if (!state) {
return null
}
- prev[seriesIndex] = selectedYSeriesColumnIndex
- return [...prev]
+ const ySeries = [...state]
+
+ ySeries[seriesIndex] = columnName
+ return ySeries
},
- deletedYSeries: (prev, { seriesIndex }) => {
- if (!prev) {
+ deleteYSeries: (state, { seriesIndex }) => {
+ if (!state) {
return null
}
- if (prev.length <= 1) {
+ if (state.length <= 1) {
return [null]
}
- prev.splice(seriesIndex, 1)
+ const ySeries = [...state]
+
+ ySeries.splice(seriesIndex, 1)
- return [...prev]
+ return ySeries
},
},
],
+ activeSideBarTab: [
+ SideBarTab.Series as SideBarTab,
+ {
+ setSideBarTab: (_state, { tab }) => tab,
+ },
+ ],
}),
selectors({
query: [(_state, props) => [props.query], (query) => query],
@@ -156,40 +180,73 @@ export const dataVisualizationLogic = kea([
(cachedResults: AnyResponseType | null): boolean => !!cachedResults,
],
yData: [
- (state) => [state.selectedYIndexes, state.response],
- (yIndexes, response): null | AxisSeries[] => {
- if (!response || yIndexes === null || yIndexes.length === 0) {
+ (state) => [state.selectedYAxis, state.response, state.columns],
+ (ySeries, response, columns): null | AxisSeries[] => {
+ if (!response || ySeries === null || ySeries.length === 0) {
return null
}
const data: any[] = response?.['results'] ?? []
- const columns: string[] = response['columns']
- return yIndexes
- .filter((n): n is number => Boolean(n))
- .map((index): AxisSeries => {
+ return ySeries
+ .map((name): AxisSeries | null => {
+ if (!name) {
+ return {
+ column: {
+ name: 'None',
+ type: 'None',
+ label: 'None',
+ dataIndex: -1,
+ },
+ data: [],
+ }
+ }
+
+ const column = columns.find((n) => n.name === name)
+ if (!column) {
+ return null
+ }
+
return {
- name: columns[index],
+ column,
data: data.map((n) => {
try {
- return parseInt(n[index], 10)
+ return parseInt(n[column.dataIndex], 10)
} catch {
return 0
}
}),
}
})
+ .filter((series): series is AxisSeries => Boolean(series))
},
],
xData: [
- (state) => [state.selectedXIndex, state.response],
- (xIndex, response): null | string[] => {
- if (!response || xIndex === null) {
- return null
+ (state) => [state.selectedXAxis, state.response, state.columns],
+ (xSeries, response, columns): AxisSeries | null => {
+ if (!response || xSeries === null) {
+ return {
+ column: {
+ name: 'None',
+ type: 'None',
+ label: 'None',
+ dataIndex: -1,
+ },
+ data: [],
+ }
}
const data: any[] = response?.['results'] ?? []
- return data.map((n) => n[xIndex])
+
+ const column = columns.find((n) => n.name === xSeries)
+ if (!column) {
+ return null
+ }
+
+ return {
+ column,
+ data: data.map((n) => n[column.dataIndex]),
+ }
},
],
}),
@@ -214,15 +271,15 @@ export const dataVisualizationLogic = kea([
}
if (props.query.chartSettings) {
- const { xAxisIndex, yAxisIndex } = props.query.chartSettings
+ const { xAxis, yAxis } = props.query.chartSettings
- if (xAxisIndex && xAxisIndex.length) {
- actions.updateXSeries(xAxisIndex[0])
+ if (xAxis) {
+ actions.updateXSeries(xAxis.column)
}
- if (yAxisIndex && yAxisIndex.length) {
- yAxisIndex.forEach((index) => {
- actions.addYSeries(index)
+ if (yAxis && yAxis.length) {
+ yAxis.forEach((axis) => {
+ actions.addYSeries(axis.column)
})
}
}
@@ -236,40 +293,47 @@ export const dataVisualizationLogic = kea([
}
// Set default axis values
- if (values.response && values.selectedXIndex === null && values.selectedYIndexes === null) {
+ if (values.response && values.selectedXAxis === null && values.selectedYAxis === null) {
const types: string[][] = values.response['types']
- const yAxisIndex = types.findIndex((n) => n[1].indexOf('Int') !== -1 || n[1].indexOf('Float') !== -1)
- const xAxisIndex = types.findIndex((n) => n[1].indexOf('Date') !== -1)
+ const yAxisTypes = types.find((n) => n[1].indexOf('Int') !== -1 || n[1].indexOf('Float') !== -1)
+ const xAxisTypes = types.find((n) => n[1].indexOf('Date') !== -1)
- if (yAxisIndex >= 0) {
- actions.addYSeries(yAxisIndex)
+ if (yAxisTypes) {
+ actions.addYSeries(yAxisTypes[0])
}
- if (xAxisIndex >= 0) {
- actions.updateXSeries(xAxisIndex)
+ if (xAxisTypes) {
+ actions.updateXSeries(xAxisTypes[0])
}
}
},
- selectedXIndex: (value: number | null) => {
+ selectedXAxis: (value: string | null) => {
if (props.setQuery) {
+ const yColumns = values.selectedYAxis?.filter((n: string | null): n is string => Boolean(n)) ?? []
+ const xColumn: ChartAxis | undefined = value !== null ? { column: value } : undefined
+
props.setQuery({
...props.query,
chartSettings: {
...(props.query.chartSettings ?? {}),
- yAxisIndex: values.selectedYIndexes?.filter((n: number | null): n is number => Boolean(n)),
- xAxisIndex: value !== null ? [value] : undefined,
+ yAxis: yColumns.map((n) => ({ column: n })),
+ xAxis: xColumn,
},
})
}
},
- selectedYIndexes: (value: (number | null)[] | null) => {
+ selectedYAxis: (value: (string | null)[] | null) => {
if (props.setQuery) {
+ const yColumns = value?.filter((n: string | null): n is string => Boolean(n)) ?? []
+ const xColumn: ChartAxis | undefined =
+ values.selectedXAxis !== null ? { column: values.selectedXAxis } : undefined
+
props.setQuery({
...props.query,
chartSettings: {
...(props.query.chartSettings ?? {}),
- yAxisIndex: value?.filter((n: number | null): n is number => Boolean(n)),
- xAxisIndex: values.selectedXIndex !== null ? [values.selectedXIndex] : undefined,
+ yAxis: yColumns.map((n) => ({ column: n })),
+ xAxis: xColumn,
},
})
}
diff --git a/frontend/src/queries/nodes/DataVisualization/displayLogic.ts b/frontend/src/queries/nodes/DataVisualization/displayLogic.ts
new file mode 100644
index 0000000000000..0dd3fef0de781
--- /dev/null
+++ b/frontend/src/queries/nodes/DataVisualization/displayLogic.ts
@@ -0,0 +1,95 @@
+import * as d3 from 'd3'
+import { actions, afterMount, connect, kea, key, path, props, reducers } from 'kea'
+import { subscriptions } from 'kea-subscriptions'
+
+import { GoalLine } from '~/queries/schema'
+
+import { dataVisualizationLogic } from './dataVisualizationLogic'
+import type { displayLogicType } from './displayLogicType'
+
+export interface DisplayLogicProps {
+ key: string
+}
+
+export const displayLogic = kea([
+ key((props) => props.key),
+ path(['queries', 'nodes', 'DataVisualization', 'displayLogic']),
+ props({ key: '' } as DisplayLogicProps),
+ connect({
+ values: [dataVisualizationLogic, ['yData', 'query']],
+ actions: [dataVisualizationLogic, ['setQuery']],
+ }),
+ actions(({ values }) => ({
+ addGoalLine: () => ({ yData: values.yData }),
+ updateGoalLine: (goalLineIndex: number, key: string, value: string | number) => ({
+ goalLineIndex,
+ key,
+ value,
+ }),
+ removeGoalLine: (goalLineIndex: number) => ({ goalLineIndex }),
+ setGoalLines: (goalLines: GoalLine[]) => ({ goalLines }),
+ })),
+ reducers({
+ goalLines: [
+ [] as GoalLine[],
+ {
+ addGoalLine: (state, { yData }) => {
+ const yDataFlat = yData?.flatMap((n) => n.data) ?? []
+ const yDataAvg = Math.round(d3.mean(yDataFlat) ?? 0)
+
+ return [
+ ...state,
+ {
+ label: 'Q4 Goal',
+ value: yDataAvg ?? 0,
+ },
+ ]
+ },
+ removeGoalLine: (state, { goalLineIndex }) => {
+ const goalLines = [...state]
+
+ goalLines.splice(goalLineIndex, 1)
+ return goalLines
+ },
+ updateGoalLine: (state, { goalLineIndex, key, value }) => {
+ const goalLines = [...state]
+
+ if (key === 'value') {
+ if (Number.isNaN(value)) {
+ goalLines[goalLineIndex][key] = 0
+ } else {
+ goalLines[goalLineIndex][key] = parseInt(value.toString())
+ }
+ } else {
+ goalLines[goalLineIndex][key] = value
+ }
+
+ return goalLines
+ },
+ setGoalLines: (_state, { goalLines }) => {
+ return goalLines
+ },
+ },
+ ],
+ }),
+ afterMount(({ values, actions }) => {
+ const chartSettings = values.query.chartSettings
+
+ if (chartSettings?.goalLines) {
+ actions.setGoalLines(chartSettings.goalLines)
+ }
+ }),
+ subscriptions(({ values, actions }) => ({
+ goalLines: (value: GoalLine[]) => {
+ const goalLines = value.length > 0 ? value : undefined
+
+ actions.setQuery({
+ ...values.query,
+ chartSettings: {
+ ...values.query.chartSettings,
+ goalLines,
+ },
+ })
+ },
+ })),
+])
diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json
index 3d4a37a0cfdf7..91c43c38ac0ee 100644
--- a/frontend/src/queries/schema.json
+++ b/frontend/src/queries/schema.json
@@ -284,6 +284,16 @@
"enum": ["cohort", "person", "event", "group", "session", "hogql"],
"type": "string"
},
+ "ChartAxis": {
+ "additionalProperties": false,
+ "properties": {
+ "column": {
+ "type": "string"
+ }
+ },
+ "required": ["column"],
+ "type": "object"
+ },
"ChartDisplayType": {
"enum": [
"ActionsLineGraph",
@@ -514,15 +524,18 @@
"chartSettings": {
"additionalProperties": false,
"properties": {
- "xAxisIndex": {
+ "goalLines": {
"items": {
- "type": "number"
+ "$ref": "#/definitions/GoalLine"
},
"type": "array"
},
- "yAxisIndex": {
+ "xAxis": {
+ "$ref": "#/definitions/ChartAxis"
+ },
+ "yAxis": {
"items": {
- "type": "number"
+ "$ref": "#/definitions/ChartAxis"
},
"type": "array"
}
@@ -1353,6 +1366,19 @@
"required": ["kind", "series"],
"type": "object"
},
+ "GoalLine": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ }
+ },
+ "required": ["label", "value"],
+ "type": "object"
+ },
"GroupMathType": {
"const": "unique_group",
"type": "string"
diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts
index f005d289737f9..267b227a0625c 100644
--- a/frontend/src/queries/schema.ts
+++ b/frontend/src/queries/schema.ts
@@ -345,9 +345,19 @@ export interface DataTableNode extends Node, DataTableNodeViewProps {
hiddenColumns?: HogQLExpression[]
}
+export interface GoalLine {
+ label: string
+ value: number
+}
+
+export interface ChartAxis {
+ column: string
+}
+
interface ChartSettings {
- xAxisIndex?: number[]
- yAxisIndex?: number[]
+ xAxis?: ChartAxis
+ yAxis?: ChartAxis[]
+ goalLines?: GoalLine[]
}
export interface DataVisualizationNode extends Node {
diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts
index b7e230577785f..86e1697b4be95 100644
--- a/frontend/src/queries/utils.ts
+++ b/frontend/src/queries/utils.ts
@@ -16,6 +16,7 @@ import {
InsightFilter,
InsightFilterProperty,
InsightNodeKind,
+ InsightPersonsQuery,
InsightQueryNode,
InsightVizNode,
LifecycleQuery,
@@ -95,6 +96,10 @@ export function isPersonsQuery(node?: Node | null): node is PersonsQuery {
return node?.kind === NodeKind.PersonsQuery
}
+export function isInsightPersonsQuery(node?: Node | null): node is InsightPersonsQuery {
+ return node?.kind === NodeKind.InsightPersonsQuery
+}
+
export function isDataTableNode(node?: Node | null): node is DataTableNode {
return node?.kind === NodeKind.DataTableNode
}
diff --git a/frontend/src/scenes/ResourcePermissionModal.tsx b/frontend/src/scenes/ResourcePermissionModal.tsx
index 4e81f46ccc99b..8c7209a5bc77a 100644
--- a/frontend/src/scenes/ResourcePermissionModal.tsx
+++ b/frontend/src/scenes/ResourcePermissionModal.tsx
@@ -183,7 +183,7 @@ export function ResourcePermission({
<>
Roles
{roles.length > 0 ? (
-
+
{roles.map((role) => {
return (
{
+ if (item.type === BillingGaugeItemKind.FreeTier) {
+ return 'bg-success-light'
+ } else if (item.type === BillingGaugeItemKind.CurrentUsage) {
+ return isWithinUsageLimit ? 'bg-success' : 'bg-danger'
+ } else if (item.type === BillingGaugeItemKind.ProjectedUsage) {
+ return 'bg-border'
+ } else if (item.type === BillingGaugeItemKind.BillingLimit) {
+ return 'bg-primary-alt-light'
+ } else {
+ throw new Error(`Unknown type: ${item.type}`)
+ }
+}
+
+const BillingGaugeItem = ({ item, maxValue, isWithinUsageLimit }: BillingGaugeItemProps): JSX.Element => {
+ const width = `${(item.value / maxValue) * 100}%`
+ const bgColorClass = getBgColorClassForItem(item, isWithinUsageLimit)
+
+ return (
+
+
+
+
+
{item.text}
+
{compactNumber(item.value)}
+
+
+
+ )
}
-const BillingGaugeItem = ({ width, className, tooltip, top, value }: BillingGaugeItemProps): JSX.Element => {
+const BillingGaugeItem3000 = ({ item, maxValue, isWithinUsageLimit }: BillingGaugeItemProps): JSX.Element => {
+ const width = `${(item.value / maxValue) * 100}%`
+
return (
-
+
- {tooltip}
-
{compactNumber(value)}
+
{item.text}
+
{compactNumber(item.value)}
)
}
+/*
+ * Billing Gauge.
+ */
export type BillingGaugeProps = {
- items: {
- text: string | JSX.Element
- color: string
- value: number
- top: boolean
- }[]
+ items: BillingGaugeItemType[]
+ product: BillingProductV2Type
+}
+
+export function BillingGauge({ items, product }: BillingGaugeProps): JSX.Element {
+ const maxValue = useMemo(() => {
+ return Math.max(100, ...items.map((item) => item.value)) * 1.3
+ }, [items])
+ const isWithinUsageLimit = product.percentage_usage <= 1
+
+ return (
+
+ {items.map((item, i) => (
+
+ ))}
+
+ )
}
-export function BillingGauge({ items }: BillingGaugeProps): JSX.Element {
- const maxScale = useMemo(() => {
+export function BillingGauge3000({ items, product }: BillingGaugeProps): JSX.Element {
+ const maxValue = useMemo(() => {
return Math.max(100, ...items.map((item) => item.value)) * 1.3
}, [items])
+ const isWithinUsageLimit = product.percentage_usage <= 1
return (
{items.map((item, i) => (
- {item.text}}
- top={item.top}
- value={item.value}
- />
+
))}
)
diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx
index 35333fa65df2b..75bb4175e7d7c 100644
--- a/frontend/src/scenes/billing/BillingProduct.tsx
+++ b/frontend/src/scenes/billing/BillingProduct.tsx
@@ -1,6 +1,7 @@
import { LemonButton, LemonSelectOptions, LemonTable, LemonTag, Link } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
+import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
import {
IconArticle,
@@ -21,7 +22,7 @@ import { getProductIcon } from 'scenes/products/Products'
import { BillingProductV2AddonType, BillingProductV2Type, BillingV2TierType } from '~/types'
import { convertLargeNumberToWords, getUpgradeProductLink, summarizeUsage } from './billing-utils'
-import { BillingGauge } from './BillingGauge'
+import { BillingGauge, BillingGauge3000 } from './BillingGauge'
import { BillingLimitInput } from './BillingLimitInput'
import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'
@@ -150,6 +151,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
customLimitUsd,
showTierBreakdown,
billingGaugeItems,
+ billingGaugeItems3000,
isPricingModalOpen,
isPlanComparisonModalOpen,
currentAndUpgradePlans,
@@ -164,6 +166,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
setSurveyResponse,
} = useActions(billingProductLogic({ product }))
const { reportBillingUpgradeClicked } = useActions(eventUsageLogic)
+ const is3000 = useFeatureFlag('POSTHOG_3000', 'test')
const showUpgradeCTA = !product.subscribed && !product.contact_support && product.plans?.length
const upgradePlan = currentAndUpgradePlans?.upgradePlan
@@ -419,7 +422,11 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
/>
)}
-
+ {is3000 ? (
+
+ ) : (
+
+ )}
{product.current_amount_usd ? (
diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts
index 56e279ac2a66b..bc0d986bc7f83 100644
--- a/frontend/src/scenes/billing/billingProductLogic.ts
+++ b/frontend/src/scenes/billing/billingProductLogic.ts
@@ -7,6 +7,7 @@ import { BillingProductV2AddonType, BillingProductV2Type, BillingV2PlanType, Bil
import { convertAmountToUsage } from './billing-utils'
import { billingLogic } from './billingLogic'
import type { billingProductLogicType } from './billingProductLogicType'
+import { BillingGaugeItemKind, BillingGaugeItemType } from './types'
const DEFAULT_BILLING_LIMIT = 500
@@ -163,38 +164,34 @@ export const billingProductLogic = kea([
],
billingGaugeItems: [
(s, p) => [p.product, s.freeTier, s.billingLimitAsUsage],
- (product, freeTier, billingLimitAsUsage) => {
+ (product, freeTier, billingLimitAsUsage): BillingGaugeItemType[] => {
return [
freeTier
? {
+ type: BillingGaugeItemKind.FreeTier,
text: 'Free tier limit',
- color: 'success-light',
value: freeTier,
top: true,
}
: undefined,
{
+ type: BillingGaugeItemKind.CurrentUsage,
text: 'Current',
- color: product.percentage_usage
- ? product.percentage_usage <= 1
- ? 'success'
- : 'danger'
- : 'success',
value: product.current_usage || 0,
top: false,
},
product.projected_usage && product.projected_usage > (product.current_usage || 0)
? {
+ type: BillingGaugeItemKind.ProjectedUsage,
text: 'Projected',
- color: 'border',
value: product.projected_usage || 0,
top: false,
}
: undefined,
billingLimitAsUsage
? {
+ type: BillingGaugeItemKind.BillingLimit,
text: 'Billing limit',
- color: 'primary-alt-light',
top: true,
value: billingLimitAsUsage || 0,
}
@@ -202,6 +199,43 @@ export const billingProductLogic = kea([
].filter(Boolean)
},
],
+ billingGaugeItems3000: [
+ (s, p) => [p.product, s.freeTier, s.billingLimitAsUsage],
+ (product, freeTier, billingLimitAsUsage): BillingGaugeItemType[] => {
+ return [
+ billingLimitAsUsage
+ ? {
+ type: BillingGaugeItemKind.BillingLimit,
+ text: 'Billing limit',
+ top: true,
+ value: billingLimitAsUsage || 0,
+ }
+ : (undefined as any),
+ freeTier
+ ? {
+ type: BillingGaugeItemKind.FreeTier,
+ text: 'Free tier limit',
+ value: freeTier,
+ top: true,
+ }
+ : undefined,
+ product.projected_usage && product.projected_usage > (product.current_usage || 0)
+ ? {
+ type: BillingGaugeItemKind.ProjectedUsage,
+ text: 'Projected',
+ value: product.projected_usage || 0,
+ top: false,
+ }
+ : undefined,
+ {
+ type: BillingGaugeItemKind.CurrentUsage,
+ text: 'Current',
+ value: product.current_usage || 0,
+ top: false,
+ },
+ ].filter(Boolean)
+ },
+ ],
})),
listeners(({ actions, values, props }) => ({
loadBillingSuccess: actions.billingLoaded,
diff --git a/frontend/src/scenes/billing/types.ts b/frontend/src/scenes/billing/types.ts
new file mode 100644
index 0000000000000..e1845afdd36a3
--- /dev/null
+++ b/frontend/src/scenes/billing/types.ts
@@ -0,0 +1,13 @@
+export enum BillingGaugeItemKind {
+ FreeTier = 'free_tier',
+ CurrentUsage = 'current_usage',
+ ProjectedUsage = 'projected_usage',
+ BillingLimit = 'billing_limit',
+}
+
+export type BillingGaugeItemType = {
+ type: BillingGaugeItemKind
+ text: string | JSX.Element
+ value: number
+ top: boolean
+}
diff --git a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx
index 63e7c0547f504..3f43a36e43d42 100644
--- a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx
+++ b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx
@@ -90,7 +90,7 @@ export function DashboardCollaboration({ dashboardId }: { dashboardId: Dashboard
)}
Project members with access
-
+
{allCollaborators.map((collaborator) => (
-
+
prebuilt widget to opt-in to
features
-
+
),
},
diff --git a/frontend/src/scenes/events/Owner.tsx b/frontend/src/scenes/events/Owner.tsx
deleted file mode 100644
index 308912d41d455..0000000000000
--- a/frontend/src/scenes/events/Owner.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
-import { CSSProperties } from 'react'
-
-import { UserBasicType } from '~/types'
-
-export function Owner({ user, style = {} }: { user?: UserBasicType | null; style?: CSSProperties }): JSX.Element {
- return (
- <>
- {user?.uuid ? (
-
-
-
- {user.first_name}
-
-
- ) : (
-
- No owner
-
- )}
- >
- )
-}
diff --git a/frontend/src/scenes/experiments/Experiment.tsx b/frontend/src/scenes/experiments/Experiment.tsx
index 596aa0dcabf7e..d5e034407cd37 100644
--- a/frontend/src/scenes/experiments/Experiment.tsx
+++ b/frontend/src/scenes/experiments/Experiment.tsx
@@ -527,7 +527,6 @@ export function Experiment(): JSX.Element {
{experiment?.name}
diff --git a/frontend/src/scenes/experiments/Experiments.tsx b/frontend/src/scenes/experiments/Experiments.tsx
index 1a912cb6b240d..453e72ea4f2ea 100644
--- a/frontend/src/scenes/experiments/Experiments.tsx
+++ b/frontend/src/scenes/experiments/Experiments.tsx
@@ -204,7 +204,7 @@ export function Experiments(): JSX.Element {
))}
{!shouldShowEmptyState && (
<>
-
+
- {capitalizeFirstLetter(item.variant)}
+ {capitalizeFirstLetter(item.variant)}
)
},
diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
index fc133f316f7ad..3a212f4990141 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
@@ -323,9 +323,7 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
autoCorrect="off"
spellCheck={false}
/>
-
- Feature flag keys must be unique
-
+
Feature flag keys must be unique
>
)}
@@ -1017,7 +1015,7 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
/>
{filterGroups.filter((group) => group.variant === variant.key)
.length > 0 && (
-
+
Overridden by{' '}
{variantConcatWithPunctuation(
diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx
index 5eff9ffc7b491..5d9733aec817e 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx
@@ -258,7 +258,7 @@ export function OverViewTab({
{!shouldShowEmptyState && (
<>
-
+
{title}
- {value}
+ {value}
)
diff --git a/frontend/src/scenes/groups/GroupsIntroduction.tsx b/frontend/src/scenes/groups/GroupsIntroduction.tsx
index eb2bda5801ff9..21282119389a6 100644
--- a/frontend/src/scenes/groups/GroupsIntroduction.tsx
+++ b/frontend/src/scenes/groups/GroupsIntroduction.tsx
@@ -14,7 +14,7 @@ export function GroupsIntroduction({ access }: Props): JSX.Element {
if (access === GroupsAccessStatus.NoAccess) {
header = (
<>
- Introducing
Group Analytics !
+ Introducing
Group Analytics !
>
)
subtext = (
@@ -45,17 +45,17 @@ export function GroupsIntroduction({ access }: Props): JSX.Element {
export function GroupIntroductionFooter({ needsUpgrade }: { needsUpgrade: boolean }): JSX.Element {
return (
-
+
{needsUpgrade ? (
<>
- Track usage of groups of users with Group Analytics.{' '}
+ Track usage of groups of users with Group Analytics.{' '}
- Upgrade now
+ Upgrade now
{' '}
or{' '}
}
- const actuallyShowQueryEditor =
- insightMode === ItemMode.Edit &&
- ((isQueryBasedInsight && !containsHogQLQuery(query)) || (!isQueryBasedInsight && showQueryEditor))
+ const actuallyShowQueryEditor = insightMode === ItemMode.Edit && showQueryEditor
const setQuery = (query: Node): void => {
if (!isInsightVizNode(query)) {
diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx
index c100746612e11..ac44020406634 100644
--- a/frontend/src/scenes/insights/InsightPageHeader.tsx
+++ b/frontend/src/scenes/insights/InsightPageHeader.tsx
@@ -33,7 +33,6 @@ import { cohortsModel } from '~/models/cohortsModel'
import { groupsModel } from '~/models/groupsModel'
import { tagsModel } from '~/models/tagsModel'
import { DataTableNode, NodeKind } from '~/queries/schema'
-import { isInsightVizNode } from '~/queries/utils'
import {
AvailableFeature,
ExporterFormat,
@@ -203,28 +202,26 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In
) : null}
>
)}
- {isInsightVizNode(query) ? (
- {
- // for an existing insight in view mode
- if (hasDashboardItemId && insightMode !== ItemMode.Edit) {
- // enter edit mode
- setInsightMode(ItemMode.Edit, null)
+ {
+ // for an existing insight in view mode
+ if (hasDashboardItemId && insightMode !== ItemMode.Edit) {
+ // enter edit mode
+ setInsightMode(ItemMode.Edit, null)
- // exit early if query editor doesn't need to be toggled
- if (showQueryEditor !== false) {
- return
- }
+ // exit early if query editor doesn't need to be toggled
+ if (showQueryEditor !== false) {
+ return
}
- toggleQueryEditorPanel()
- }}
- fullWidth
- >
- {showQueryEditor ? 'Hide source' : 'View source'}
-
- ) : null}
+ }
+ toggleQueryEditorPanel()
+ }}
+ fullWidth
+ >
+ {showQueryEditor ? 'Hide source' : 'View source'}
+
{hogQL && (
-
+
>
)
diff --git a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx
index af69bc1bf967e..a3507b4ed93c8 100644
--- a/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx
+++ b/frontend/src/scenes/insights/InsightTooltip/InsightTooltip.stories.tsx
@@ -154,7 +154,7 @@ export function InWrapper(): JSX.Element {
useMountedLogic(cohortsModel)
return (
-
+
1 ? ' multiple' : ''}`} style={{ maxWidth: 300 }}>
+ 1 ? ' multiple' : ''}`)}>
{!hideHeader && (
{referenceDate && interval && !preferAltTitle ? (
diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts
index 4fcf69fb0924c..ef9e4834854c6 100644
--- a/frontend/src/scenes/insights/summarizeInsight.ts
+++ b/frontend/src/scenes/insights/summarizeInsight.ts
@@ -182,7 +182,7 @@ function summarizeInsightFilters(filters: AnyPartialFilterType, context: Summary
return ''
}
-function summarizeInsightQuery(query: InsightQueryNode, context: SummaryContext): string {
+export function summarizeInsightQuery(query: InsightQueryNode, context: SummaryContext): string {
if (isTrendsQuery(query)) {
let summary = query.series
.map((s, index) => {
diff --git a/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx b/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx
index 4609d5241d0ef..a9a6350e74ba2 100644
--- a/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx
+++ b/frontend/src/scenes/instance/SystemStatus/StaffUsersTab.tsx
@@ -74,7 +74,7 @@ export function StaffUsersTab(): JSX.Element {
{myself?.uuid === user.uuid ? (
<>
Please confirm you want to remove yourself as a staff user.
-
+
Only another staff user will be able to add you again.
>
diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
index d2c96c06ee054..aa76ffcc7d941 100644
--- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
@@ -17,7 +17,7 @@ import {
IconUnfoldLess,
IconUnfoldMore,
} from 'lib/lemon-ui/icons'
-import { LemonButton } from '@posthog/lemon-ui'
+import { LemonButton, LemonMenu, LemonMenuItems } from '@posthog/lemon-ui'
import './NodeWrapper.scss'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { BindLogic, BuiltLogic, useActions, useMountedLogic, useValues } from 'kea'
@@ -40,7 +40,7 @@ import { notebookNodeLogicType } from './notebookNodeLogicType'
import { SlashCommandsPopover } from '../Notebook/SlashCommands'
import posthog from 'posthog-js'
import { NotebookNodeContext } from './NotebookNodeContext'
-import { IconGear } from '@posthog/icons'
+import { IconCopy, IconEllipsis, IconGear } from '@posthog/icons'
function NodeWrapper
(props: NodeWrapperProps): JSX.Element {
const {
@@ -75,7 +75,16 @@ function NodeWrapper(props: NodeWrapperP
// nodeId can start null, but should then immediately be generated
const nodeLogic = useMountedLogic(notebookNodeLogic(logicProps))
const { resizeable, expanded, actions, nodeId } = useValues(nodeLogic)
- const { setRef, setExpanded, deleteNode, toggleEditing, insertOrSelectNextLine } = useActions(nodeLogic)
+ const {
+ setRef,
+ setExpanded,
+ deleteNode,
+ toggleEditing,
+ insertOrSelectNextLine,
+ toggleEditingTitle,
+ copyToClipboard,
+ convertToBacklink,
+ } = useActions(nodeLogic)
const { ref: inViewRef, inView } = useInView({ triggerOnce: true })
@@ -140,6 +149,36 @@ function NodeWrapper(props: NodeWrapperP
const isResizeable = resizeable && (!expandable || expanded)
const isDraggable = !!(isEditable && getPos)
+ const menuItems: LemonMenuItems = [
+ {
+ label: 'Copy',
+ onClick: () => copyToClipboard(),
+ sideIcon: ,
+ },
+ isEditable && isResizeable
+ ? {
+ label: 'Reset height to default',
+ onClick: () => {
+ updateAttributes({
+ height: null,
+ } as any)
+ },
+ }
+ : null,
+ isEditable && parsedHref
+ ? {
+ label: 'Convert to inline link',
+ onClick: () => convertToBacklink(parsedHref),
+ sideIcon: ,
+ }
+ : null,
+
+ isEditable ? { label: 'Edit title', onClick: () => toggleEditingTitle(true) } : null,
+ isEditable ? { label: 'Remove', onClick: () => deleteNode(), sideIcon: , status: 'danger' } : null,
+ ]
+
+ const hasMenu = menuItems.some((x) => !!x)
+
return (
@@ -157,10 +196,13 @@ function NodeWrapper(props: NodeWrapperP
{!inView ? (
<>
-
{/* Placeholder for the drag handle */}
+
+
+
+
{/* eslint-disable-next-line react/forbid-dom-props */}
-
-
+
+
>
) : (
@@ -206,14 +248,17 @@ function NodeWrapper
(props: NodeWrapperP
active={editingNodeId === nodeId}
/>
) : null}
+ >
+ ) : null}
+ {hasMenu ? (
+
deleteNode()}
+ icon={ }
+ status="stealth"
size="small"
- status="danger"
- icon={ }
/>
- >
+
) : null}
@@ -373,7 +418,20 @@ export function createPostHogWidgetNode(
},
renderHTML({ HTMLAttributes }) {
- return [wrapperProps.nodeType, mergeAttributes(HTMLAttributes)]
+ // We want to stringify all object attributes so that we can use them in the serializedText
+ const sanitizedAttributes = Object.fromEntries(
+ Object.entries(HTMLAttributes).map(([key, value]) => {
+ if (Array.isArray(value) || typeof value === 'object') {
+ return [key, JSON.stringify(value)]
+ }
+ return [key, value]
+ })
+ )
+
+ // This method is primarily used by copy and paste so we can remove the nodeID, assuming we don't want duplicates
+ delete sanitizedAttributes['nodeId']
+
+ return [wrapperProps.nodeType, mergeAttributes(sanitizedAttributes)]
},
addNodeView() {
diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx
index 2490bbea971c9..6a34c08235f49 100644
--- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx
@@ -2,8 +2,7 @@ import { mergeAttributes, Node, NodeViewProps } from '@tiptap/core'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import { InsightModel, NotebookNodeType, NotebookTarget } from '~/types'
import { Link } from '@posthog/lemon-ui'
-import { IconGauge, IconBarChart, IconFlag, IconExperiment, IconLive, IconPerson, IconCohort } from 'lib/lemon-ui/icons'
-import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
+import { IconBarChart, IconFlag, IconExperiment, IconLive, IconPerson, IconCohort } from 'lib/lemon-ui/icons'
import { urls } from 'scenes/urls'
import clsx from 'clsx'
import { router } from 'kea-router'
@@ -14,67 +13,158 @@ import { notebookLogic } from '../Notebook/notebookLogic'
import { openNotebook } from '~/models/notebooksModel'
import { IconNotebook } from '../IconNotebook'
+import { IconChat, IconDashboard, IconLogomark, IconRewindPlay } from '@posthog/icons'
+import { useEffect } from 'react'
-const ICON_MAP = {
- dashboards: ,
- insights: ,
- feature_flags: ,
- experiments: ,
- events: ,
- persons: ,
- cohorts: ,
- notebooks: ,
+type BackLinkMapper = {
+ regex: string
+ type: string
+ icon: JSX.Element
+ getTitle: (match: string) => Promise
}
+const BACKLINK_MAP: BackLinkMapper[] = [
+ {
+ type: 'dashboards',
+ regex: urls.dashboard('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const dashboard = await api.dashboards.get(Number(id))
+ return dashboard.name ?? ''
+ },
+ },
+ {
+ type: 'insights',
+ regex: urls.insightView('(.+)' as InsightModel['short_id']),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const insight = await api.insights.loadInsight(id as InsightModel['short_id'])
+ return insight.results[0]?.name ?? ''
+ },
+ },
+ {
+ type: 'feature_flags',
+ regex: urls.featureFlag('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const flag = await api.featureFlags.get(Number(id))
+ return flag.name ?? ''
+ },
+ },
+ {
+ type: 'experiments',
+ regex: urls.experiment('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const experiment = await api.experiments.get(Number(id))
+ return experiment.name ?? ''
+ },
+ },
+ {
+ type: 'surveys',
+ regex: urls.survey('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const survey = await api.surveys.get(id)
+ return survey.name ?? ''
+ },
+ },
+ {
+ type: 'events',
+ regex: urls.eventDefinition('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[3]
+ const event = await api.eventDefinitions.get({ eventDefinitionId: id })
+ return event.name ?? ''
+ },
+ },
+ {
+ type: 'persons',
+ regex: urls.personByDistinctId('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const response = await api.persons.list({ distinct_id: id })
+ return response.results[0]?.name ?? ''
+ },
+ },
+ {
+ type: 'cohorts',
+ regex: urls.cohort('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const cohort = await api.cohorts.get(Number(id))
+ return cohort.name ?? ''
+ },
+ },
+ {
+ type: 'replay',
+ regex: urls.replaySingle('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ return id
+ },
+ },
+ {
+ type: 'notebooks',
+ regex: urls.notebook('(.+)'),
+ icon: ,
+ getTitle: async (path: string) => {
+ const id = path.split('/')[2]
+ const notebook = await api.notebooks.get(id)
+ return notebook.title ?? ''
+ },
+ },
+]
+
const Component = (props: NodeViewProps): JSX.Element => {
const { shortId } = useValues(notebookLogic)
+ const { location } = useValues(router)
+
+ const href: string = props.node.attrs.href ?? ''
- const type: TaxonomicFilterGroupType = props.node.attrs.type
- const title: string = props.node.attrs.title
- const id: string = props.node.attrs.id
- const href = backlinkHref(id, type)
+ const backLinkConfig = BACKLINK_MAP.find((config) => href.match(config.regex))
+ const derivedText: string = props.node.attrs.title || props.node.attrs.href
+ const isViewing = location.pathname === href
- const isViewing = router.values.location.pathname === href
+ useEffect(() => {
+ if (props.node.attrs.title || !backLinkConfig) {
+ return
+ }
+
+ void backLinkConfig
+ .getTitle(href)
+ .then((title) => {
+ props.updateAttributes({
+ title,
+ })
+ })
+ .catch((e) => {
+ console.error(e)
+ })
+ }, [props.node.attrs.title])
return (
- void openNotebook(shortId, NotebookTarget.Popover)}
- target={undefined}
- className="space-x-1"
- >
- {ICON_MAP[type]}
- {title}
+ void openNotebook(shortId, NotebookTarget.Popover)} className="space-x-1">
+ {backLinkConfig?.icon || }
+ {derivedText}
)
}
-function backlinkHref(id: string, type: TaxonomicFilterGroupType): string {
- if (type === TaxonomicFilterGroupType.Events) {
- return urls.eventDefinition(id)
- } else if (type === TaxonomicFilterGroupType.Cohorts) {
- return urls.cohort(id)
- } else if (type === TaxonomicFilterGroupType.Persons) {
- return urls.personByDistinctId(id)
- } else if (type === TaxonomicFilterGroupType.Insights) {
- return urls.insightView(id as InsightModel['short_id'])
- } else if (type === TaxonomicFilterGroupType.FeatureFlags) {
- return urls.featureFlag(id)
- } else if (type === TaxonomicFilterGroupType.Experiments) {
- return urls.experiment(id)
- } else if (type === TaxonomicFilterGroupType.Dashboards) {
- return urls.dashboard(id)
- } else if (type === TaxonomicFilterGroupType.Notebooks) {
- return urls.notebook(id)
- }
- return ''
-}
-
export const NotebookNodeBacklink = Node.create({
name: NotebookNodeType.Backlink,
inline: true,
@@ -83,9 +173,9 @@ export const NotebookNodeBacklink = Node.create({
addAttributes() {
return {
- id: { default: '' },
- type: { default: '' },
- title: { default: '' },
+ href: { default: '' },
+ type: {},
+ title: {},
}
},
@@ -104,53 +194,11 @@ export const NotebookNodeBacklink = Node.create({
addPasteRules() {
return [
posthogNodePasteRule({
- find: urls.eventDefinition('(.+)'),
- editor: this.editor,
- type: this.type,
- getAttributes: async (match) => {
- const id = match[1]
- const event = await api.eventDefinitions.get({ eventDefinitionId: id })
- return { id: id, type: TaxonomicFilterGroupType.Events, title: event.name }
- },
- }),
- posthogNodePasteRule({
- find: urls.cohort('(.+)'),
- editor: this.editor,
- type: this.type,
- getAttributes: async (match) => {
- const id = match[1]
- const event = await api.cohorts.get(Number(id))
- return { id: id, type: TaxonomicFilterGroupType.Cohorts, title: event.name }
- },
- }),
- posthogNodePasteRule({
- find: urls.experiment('(.+)'),
- editor: this.editor,
- type: this.type,
- getAttributes: async (match) => {
- const id = match[1]
- const experiment = await api.experiments.get(Number(id))
- return { id: id, type: TaxonomicFilterGroupType.Experiments, title: experiment.name }
- },
- }),
- posthogNodePasteRule({
- find: urls.dashboard('(.+)'),
- editor: this.editor,
- type: this.type,
- getAttributes: async (match) => {
- const id = match[1]
- const dashboard = await api.dashboards.get(Number(id))
- return { id: id, type: TaxonomicFilterGroupType.Dashboards, title: dashboard.name }
- },
- }),
- posthogNodePasteRule({
- find: urls.notebook('(.+)'),
+ find: '(.+)',
editor: this.editor,
type: this.type,
getAttributes: async (match) => {
- const id = match[1]
- const notebook = await api.notebooks.get(id)
- return { id: id, type: TaxonomicFilterGroupType.Notebooks, title: notebook.title }
+ return { href: match[1] }
},
}),
]
diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx
index 7af4f1e7e956f..84c3329b48118 100644
--- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx
@@ -59,7 +59,11 @@ const Component = ({ attributes, updateAttributes }: NotebookNodeProps{error}
+ return (
+
+ {error}
+
+ )
}
return (
diff --git a/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx b/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx
index 33a473a06edea..e05835a7d7603 100644
--- a/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/components/NotebookNodeTitle.tsx
@@ -9,14 +9,13 @@ import { notebookNodeLogic } from '../notebookNodeLogic'
export function NotebookNodeTitle(): JSX.Element {
const { isEditable } = useValues(notebookLogic)
- const { nodeAttributes, title, titlePlaceholder } = useValues(notebookNodeLogic)
- const { updateAttributes } = useActions(notebookNodeLogic)
- const [editing, setEditing] = useState(false)
+ const { nodeAttributes, title, titlePlaceholder, isEditingTitle } = useValues(notebookNodeLogic)
+ const { updateAttributes, toggleEditingTitle } = useActions(notebookNodeLogic)
const [newValue, setNewValue] = useState('')
useEffect(() => {
setNewValue(nodeAttributes.title ?? '')
- }, [editing])
+ }, [isEditingTitle])
const commitEdit = (): void => {
updateAttributes({
@@ -27,13 +26,13 @@ export function NotebookNodeTitle(): JSX.Element {
posthog.capture('notebook node title updated')
}
- setEditing(false)
+ toggleEditingTitle(false)
}
const onKeyUp = (e: KeyboardEvent): void => {
// Esc cancels, enter commits
if (e.key === 'Escape') {
- setEditing(false)
+ toggleEditingTitle(false)
} else if (e.key === 'Enter') {
commitEdit()
}
@@ -43,13 +42,13 @@ export function NotebookNodeTitle(): JSX.Element {
{title}
- ) : !editing ? (
+ ) : !isEditingTitle ? (
{
- setEditing(true)
+ toggleEditingTitle(true)
posthog.capture('notebook editing node title')
}}
>
diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts
index e49cd38e1042d..4f2de7954b1f4 100644
--- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts
+++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts
@@ -64,6 +64,9 @@ export const notebookNodeLogic = kea([
setMessageListeners: (listeners: NotebookNodeMessagesListeners) => ({ listeners }),
setTitlePlaceholder: (titlePlaceholder: string) => ({ titlePlaceholder }),
setRef: (ref: HTMLElement | null) => ({ ref }),
+ toggleEditingTitle: (editing?: boolean) => ({ editing }),
+ copyToClipboard: true,
+ convertToBacklink: (href: string) => ({ href }),
}),
connect((props: NotebookNodeLogicProps) => ({
@@ -121,6 +124,12 @@ export const notebookNodeLogic = kea([
setTitlePlaceholder: (_, { titlePlaceholder }) => titlePlaceholder,
},
],
+ isEditingTitle: [
+ false,
+ {
+ toggleEditingTitle: (state, { editing }) => (typeof editing === 'boolean' ? editing : !state),
+ },
+ ],
})),
selectors({
@@ -272,11 +281,56 @@ export const notebookNodeLogic = kea([
props.updateAttributes({ __init: null })
}
},
+
+ copyToClipboard: async () => {
+ const { nodeAttributes } = values
+
+ const htmlAttributesString = Object.entries(nodeAttributes)
+ .map(([key, value]) => {
+ if (key === 'nodeId' || key.startsWith('__')) {
+ return ''
+ }
+
+ if (value === null || value === undefined) {
+ return ''
+ }
+
+ return `${key}='${JSON.stringify(value)}'`
+ })
+ .filter((x) => !!x)
+ .join(' ')
+
+ const html = `<${props.nodeType} ${htmlAttributesString} data-pm-slice="0 0 []">${props.nodeType}>`
+
+ const type = 'text/html'
+ const blob = new Blob([html], { type })
+ const data = [new ClipboardItem({ [type]: blob })]
+
+ await window.navigator.clipboard.write(data)
+ },
+ convertToBacklink: ({ href }) => {
+ const editor = values.notebookLogic.values.editor
+ if (!props.getPos || !editor) {
+ return
+ }
+
+ editor.insertContentAfterNode(props.getPos(), {
+ type: NotebookNodeType.Backlink,
+ attrs: {
+ href,
+ },
+ })
+ actions.deleteNode()
+ },
})),
afterMount((logic) => {
const { props, actions, values } = logic
- props.notebookLogic.actions.registerNodeLogic(values.nodeId, logic as any)
+
+ // The node logic is mounted after the editor is mounted, so we need to wait a tick before we can register it
+ queueMicrotask(() => {
+ props.notebookLogic.actions.registerNodeLogic(values.nodeId, logic as any)
+ })
const isResizeable =
typeof props.resizeable === 'function' ? props.resizeable(props.attributes) : props.resizeable ?? true
diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx
index d60eb6ac8e827..114c52ec0fb69 100644
--- a/frontend/src/scenes/notebooks/Nodes/utils.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx
@@ -68,32 +68,6 @@ export function linkPasteRule(): PasteRule {
})
}
-export function selectFile(options: { contentType: string; multiple: boolean }): Promise {
- return new Promise((resolve, reject) => {
- const input = document.createElement('input')
- input.type = 'file'
- input.multiple = options.multiple
- input.accept = options.contentType
-
- input.onchange = () => {
- if (!input.files) {
- return resolve([])
- }
- const files = Array.from(input.files)
- resolve(files)
- }
-
- input.oncancel = () => {
- resolve([])
- }
- input.onerror = () => {
- reject(new Error('Error selecting file'))
- }
-
- input.click()
- })
-}
-
export function useSyncedAttributes(
props: NodeViewProps
): [NotebookNodeAttributes, (attrs: Partial>) => void] {
diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx
index c7121327108f6..af543133528a8 100644
--- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx
+++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx
@@ -159,17 +159,7 @@ export function Editor(): JSX.Element {
return true
}
- if (!moved && event.dataTransfer.files && event.dataTransfer.files[0]) {
- // if dropping external files
- const file = event.dataTransfer.files[0] // the dropped file
-
- posthog.capture('notebook file dropped', { file_type: file.type })
-
- if (!file.type.startsWith('image/')) {
- lemonToast.warning('Only images can be added to Notebooks at this time.')
- return true
- }
-
+ if (!moved && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
@@ -180,15 +170,24 @@ export function Editor(): JSX.Element {
return true
}
- editor
- .chain()
- .focus()
- .setTextSelection(coordinates.pos)
- .insertContent({
- type: NotebookNodeType.Image,
- attrs: { file },
- })
- .run()
+ // if dropping external files
+ const fileList = Array.from(event.dataTransfer.files)
+ const contentToAdd: any[] = []
+ for (const file of fileList) {
+ if (file.type.startsWith('image/')) {
+ contentToAdd.push({
+ type: NotebookNodeType.Image,
+ attrs: { file },
+ })
+ } else {
+ lemonToast.warning('Only images can be added to Notebooks at this time.')
+ }
+ }
+
+ editor.chain().focus().setTextSelection(coordinates.pos).insertContent(contentToAdd).run()
+ posthog.capture('notebook files dropped', {
+ file_types: fileList.map((x) => x.type),
+ })
return true
}
@@ -196,6 +195,36 @@ export function Editor(): JSX.Element {
return false
},
+ handlePaste: (_view, event) => {
+ const editor = editorRef.current
+ if (!editor) {
+ return false
+ }
+
+ // Special handling for pasting files such as images
+ if (event.clipboardData && event.clipboardData.files?.length > 0) {
+ // iterate over the clipboard files and add any supported file types
+ const fileList = Array.from(event.clipboardData.files)
+ const contentToAdd: any[] = []
+ for (const file of fileList) {
+ if (file.type.startsWith('image/')) {
+ contentToAdd.push({
+ type: NotebookNodeType.Image,
+ attrs: { file },
+ })
+ } else {
+ lemonToast.warning('Only images can be added to Notebooks at this time.')
+ }
+ }
+
+ editor.chain().focus().insertContent(contentToAdd).run()
+ posthog.capture('notebook files pasted', {
+ file_types: fileList.map((x) => x.type),
+ })
+
+ return true
+ }
+ },
},
onCreate: ({ editor }) => {
editorRef.current = editor
diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss
index 6b0881bd281a7..d2a228a9829ea 100644
--- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss
+++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss
@@ -4,6 +4,12 @@
flex-direction: column;
width: 100%;
+ // overriding ::selection is necessary here because
+ // antd makes it invisible otherwise
+ span::selection {
+ color: var(--primary-3000);
+ }
+
.NotebookEditor {
position: relative;
flex: 1;
@@ -117,7 +123,7 @@
.Backlink {
padding: 0.125rem 0.25rem;
- background-color: var(--white);
+ background-color: var(--bg-light);
border: 1px solid var(--border);
border-radius: var(--radius);
@@ -126,12 +132,11 @@
}
& svg {
- color: var(--muted-dark);
+ color: var(--text-3000);
}
&.Backlink--selected {
- outline-style: solid;
- outline-color: var(--primary-bg-active);
+ border-color: var(--primary-3000);
}
&.Backlink--active {
@@ -143,6 +148,10 @@
& svg {
color: var(--white);
}
+
+ span::selection {
+ color: var(--white);
+ }
}
}
@@ -277,12 +286,6 @@
max-height: 22px;
}
- // overriding ::selection is necessary here because
- // antd makes it invisible otherwise
- span::selection {
- color: var(--primary-3000);
- }
-
// Overrides for insight controls
.InsightVizDisplay {
diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx
index 5658ddbf7e5cb..d9bc8e6e5b9d8 100644
--- a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx
+++ b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx
@@ -1,18 +1,26 @@
import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui'
+import { useValues } from 'kea'
import { combineUrl } from 'kea-router'
import { IconCopy } from 'lib/lemon-ui/icons'
import { LemonDialog } from 'lib/lemon-ui/LemonDialog'
+import { base64Encode } from 'lib/utils'
import { copyToClipboard } from 'lib/utils/copyToClipboard'
import posthog from 'posthog-js'
import { useState } from 'react'
import { urls } from 'scenes/urls'
+import { notebookLogic } from './notebookLogic'
+
export type NotebookShareProps = {
shortId: string
}
export function NotebookShare({ shortId }: NotebookShareProps): JSX.Element {
+ const { content, isLocalOnly } = useValues(notebookLogic({ shortId }))
const url = combineUrl(`${window.location.origin}${urls.notebook(shortId)}`).url
+ const canvasUrl =
+ combineUrl(`${window.location.origin}${urls.canvas()}`).url + `#🦔=${base64Encode(JSON.stringify(content))}`
+
const [interestTracked, setInterestTracked] = useState(false)
const trackInterest = (): void => {
@@ -22,19 +30,45 @@ export function NotebookShare({ shortId }: NotebookShareProps): JSX.Element {
return (
Internal Link
+ {!isLocalOnly ? (
+ <>
+
+ Click the button below to copy a direct link to this Notebook. Make sure the person you
+ share it with has access to this PostHog project.
+
+
}
+ onClick={() => void copyToClipboard(url, 'notebook link')}
+ title={url}
+ >
+
{url}
+
+
+
+ >
+ ) : (
+
+ This Notebook cannot be shared directly with others as it is only visible to you.
+
+ )}
+
+
Template Link
- Click the button below to copy a direct link to this Notebook. Make sure the person you share it
- with has access to this PostHog project.
+ The link below will open a Canvas with the contents of this Notebook, allowing the receiver to view it,
+ edit it or create their own Notebook without affecting this one.
}
- onClick={() => void copyToClipboard(url, 'notebook link')}
+ onClick={() => void copyToClipboard(canvasUrl, 'canvas link')}
title={url}
>
-
{url}
+
{canvasUrl}
diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
index 0a7bf012d8e19..16244003b558e 100644
--- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
+++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
@@ -20,6 +20,7 @@ import Fuse from 'fuse.js'
import { useValues } from 'kea'
import { IconBold, IconItalic } from 'lib/lemon-ui/icons'
import { Popover } from 'lib/lemon-ui/Popover'
+import { selectFiles } from 'lib/utils/file-utils'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
@@ -29,7 +30,6 @@ import { BaseMathType, ChartDisplayType, FunnelVizType, NotebookNodeType, PathTy
import { buildNodeEmbed } from '../Nodes/NotebookNodeEmbed'
import { buildInsightVizQueryContent, buildNodeQueryContent } from '../Nodes/NotebookNodeQuery'
-import { selectFile } from '../Nodes/utils'
import NotebookIconHeading from './NotebookIconHeading'
import { notebookLogic } from './notebookLogic'
import { EditorCommands, EditorRange } from './utils'
@@ -313,7 +313,7 @@ order by count() desc
command: async (chain, pos) => {
// Trigger upload followed by insert
try {
- const files = await selectFile({ contentType: 'image/*', multiple: false })
+ const files = await selectFiles({ contentType: 'image/*', multiple: false })
if (files.length) {
return chain.insertContentAt(pos, { type: NotebookNodeType.Image, attrs: { file: files[0] } })
diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts
index 0ace283e8b22c..8bff3a9590f0a 100644
--- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts
+++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts
@@ -4,7 +4,7 @@ import { loaders } from 'kea-loaders'
import { router, urlToAction } from 'kea-router'
import { subscriptions } from 'kea-subscriptions'
import api from 'lib/api'
-import { downloadFile, slugify } from 'lib/utils'
+import { base64Decode, base64Encode, downloadFile, slugify } from 'lib/utils'
import posthog from 'posthog-js'
import {
buildTimestampCommentContent,
@@ -500,7 +500,7 @@ export const notebookLogic = kea
([
if (values.mode === 'canvas') {
// TODO: We probably want this to be configurable
- cache.lastState = btoa(JSON.stringify(jsonContent))
+ cache.lastState = base64Encode(JSON.stringify(jsonContent))
router.actions.replace(
router.values.currentLocation.pathname,
router.values.currentLocation.searchParams,
@@ -582,6 +582,9 @@ export const notebookLogic = kea([
},
scheduleNotebookRefresh: () => {
+ if (values.mode !== 'notebook') {
+ return
+ }
clearTimeout(cache.refreshTimeout)
cache.refreshTimeout = setTimeout(() => {
actions.loadNotebook()
@@ -607,7 +610,7 @@ export const notebookLogic = kea([
return
}
- actions.setLocalContent(JSON.parse(atob(hashParams['🦔'])))
+ actions.setLocalContent(JSON.parse(base64Decode(hashParams['🦔'])))
}
},
})),
diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts
index 74d0f0282ea96..7a8e74902a99b 100644
--- a/frontend/src/scenes/notebooks/Notebook/utils.ts
+++ b/frontend/src/scenes/notebooks/Notebook/utils.ts
@@ -15,7 +15,7 @@ import { Node as PMNode } from '@tiptap/pm/model'
import { NotebookNodeResource, NotebookNodeType } from '~/types'
-import { NotebookNodeLogicProps } from '../Nodes/notebookNodeLogic'
+import type { NotebookNodeLogicProps } from '../Nodes/notebookNodeLogic'
// TODO: fix the typing of string to NotebookNodeType
export const KNOWN_NODES: Record> = {}
diff --git a/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx b/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx
index b0a9b56f8fa4d..4b5f0936dee63 100644
--- a/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx
+++ b/frontend/src/scenes/notebooks/NotebookCanvasScene.tsx
@@ -1,10 +1,13 @@
import './NotebookScene.scss'
-import { LemonBanner } from '@posthog/lemon-ui'
+import { IconEllipsis } from '@posthog/icons'
+import { LemonBanner, LemonButton, LemonMenu, lemonToast } from '@posthog/lemon-ui'
import { useActions } from 'kea'
import { NotFound } from 'lib/components/NotFound'
+import { PageHeader } from 'lib/components/PageHeader'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { uuid } from 'lib/utils'
+import { getTextFromFile, selectFiles } from 'lib/utils/file-utils'
import { useMemo } from 'react'
import { SceneExport } from 'scenes/sceneTypes'
@@ -23,7 +26,7 @@ export function NotebookCanvas(): JSX.Element {
mode: 'canvas',
}
- const { duplicateNotebook } = useActions(notebookLogic(logicProps))
+ const { duplicateNotebook, exportJSON, setLocalContent } = useActions(notebookLogic(logicProps))
const is3000 = useFeatureFlag('POSTHOG_3000', 'test')
@@ -34,23 +37,63 @@ export function NotebookCanvas(): JSX.Element {
// TODO: The absolute positioning doesn't work so well in non-3000 mode
return (
-
-
- This is a canvas. You can change anything you like and it is persisted to the URL for easy
- sharing.
-
-
-
-
+ <>
+
+ setLocalContent({ type: 'doc', content: [] }, true),
+ },
+ {
+ label: 'Export as JSON',
+ onClick: () => exportJSON(),
+ },
+ {
+ label: 'Load from JSON',
+ onClick: () => {
+ void selectFiles({
+ contentType: 'application/json',
+ multiple: false,
+ })
+ .then((files) => getTextFromFile(files[0]))
+ .then((text) => {
+ const data = JSON.parse(text)
+ if (data.type !== 'doc') {
+ throw new Error('Not a notebook')
+ }
+ // Looks like a notebook
+ setLocalContent(data, true)
+ })
+ .catch((e) => {
+ lemonToast.error(e.message)
+ })
+ },
+ },
+ ]}
+ >
+ } status="stealth" size="small" />
+
+
+ Save as Notebook
+
+ >
+ }
+ />
+
+
+
+
+ This is a canvas. You can change anything you like and it is persisted to the URL for
+ easy sharing.
+
+
+
-
+ >
)
}
diff --git a/frontend/src/scenes/notebooks/NotebookMenu.tsx b/frontend/src/scenes/notebooks/NotebookMenu.tsx
index b604271643f1a..1b830564f8277 100644
--- a/frontend/src/scenes/notebooks/NotebookMenu.tsx
+++ b/frontend/src/scenes/notebooks/NotebookMenu.tsx
@@ -31,7 +31,7 @@ export function NotebookMenu({ shortId }: NotebookLogicProps): JSX.Element {
icon:
,
onClick: () => setShowHistory(!showHistory),
},
- !isLocalOnly && {
+ {
label: 'Share',
icon:
,
onClick: () => openNotebookShareDialog({ shortId }),
diff --git a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx
index 5571c5cb6f083..4bee840f00ea0 100644
--- a/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx
+++ b/frontend/src/scenes/notebooks/NotebookPanel/NotebookPanel.tsx
@@ -42,6 +42,7 @@ export function NotebookPanel(): JSX.Element | null {
selectNotebook(notebook.short_id)
}}
/>
+
{selectedNotebook &&
}
([
],
}),
actions({
- selectNotebook: (id: string, autofocus: EditorFocusPosition | undefined = undefined) => ({ id, autofocus }),
+ selectNotebook: (id: string, options: { autofocus?: EditorFocusPosition; silent?: boolean } = {}) => ({
+ id,
+ ...options,
+ }),
startDropMode: true,
endDropMode: true,
setDroppedResource: (resource: NotebookNodeResource | string | null) => ({ resource }),
@@ -108,6 +111,9 @@ export const notebookPanelLogic = kea([
listeners(({ cache, actions, values }) => ({
selectNotebook: (options) => {
+ if (options.silent) {
+ return
+ }
if (!values.is3000) {
actions.setPopoverVisibility('visible')
notebookPopoverLogic.actions.selectNotebook(options.id, options.autofocus)
diff --git a/frontend/src/scenes/notebooks/NotebookScene.tsx b/frontend/src/scenes/notebooks/NotebookScene.tsx
index e97761af1b97b..312ea603d001a 100644
--- a/frontend/src/scenes/notebooks/NotebookScene.tsx
+++ b/frontend/src/scenes/notebooks/NotebookScene.tsx
@@ -7,6 +7,7 @@ import { NotFound } from 'lib/components/NotFound'
import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
+import { useEffect } from 'react'
import { SceneExport } from 'scenes/sceneTypes'
import { NotebookTarget } from '~/types'
@@ -34,6 +35,7 @@ export const scene: SceneExport = {
export function NotebookScene(): JSX.Element {
const { notebookId, loading } = useValues(notebookSceneLogic)
+ const { createNotebook } = useActions(notebookSceneLogic)
const { notebook, conflictWarningVisible } = useValues(
notebookLogic({ shortId: notebookId, target: NotebookTarget.Scene })
)
@@ -43,6 +45,13 @@ export function NotebookScene(): JSX.Element {
const { featureFlags } = useValues(featureFlagLogic)
const buttonSize = featureFlags[FEATURE_FLAGS.POSTHOG_3000] === 'test' ? 'small' : 'medium'
+ useEffect(() => {
+ if (notebookId === 'new') {
+ // NOTE: We don't do this in the logic afterMount as the logic can get cached by the router
+ createNotebook(NotebookTarget.Scene)
+ }
+ }, [notebookId])
+
if (!notebook && !loading && !conflictWarningVisible) {
return
}
diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx
index d7bf1039a7f9a..4ac6210642710 100644
--- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx
+++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.stories.tsx
@@ -90,22 +90,6 @@ WithSlowNetworkResponse.args = {
resource: { type: NotebookNodeType.Recording, attrs: { id: 'very_slow' } },
visible: true,
}
-WithSlowNetworkResponse.parameters = {
- testOptions: {
- waitForLoadersToDisappear: false,
- },
-}
-
-export const WithSlowNetworkResponseClosedPopover = Template.bind({})
-WithSlowNetworkResponseClosedPopover.args = {
- resource: { type: NotebookNodeType.Recording, attrs: { id: 'very_slow' } },
- visible: false,
-}
-WithSlowNetworkResponseClosedPopover.parameters = {
- testOptions: {
- waitForLoadersToDisappear: false,
- },
-}
export const WithNoExistingContainingNotebooks = Template.bind({})
WithNoExistingContainingNotebooks.args = {
diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx
index c4091682c3938..1673babd2851e 100644
--- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx
+++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx
@@ -1,4 +1,4 @@
-import { LemonDivider, ProfilePicture } from '@posthog/lemon-ui'
+import { LemonDivider, LemonDropdown, ProfilePicture } from '@posthog/lemon-ui'
import { BuiltLogic, useActions, useValues } from 'kea'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'
@@ -6,8 +6,8 @@ import { dayjs } from 'lib/dayjs'
import { IconPlus, IconWithCount } from 'lib/lemon-ui/icons'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput'
-import { Popover, PopoverProps } from 'lib/lemon-ui/Popover'
-import { ReactChild, useEffect } from 'react'
+import { PopoverProps } from 'lib/lemon-ui/Popover'
+import { ReactChild, ReactElement, useEffect } from 'react'
import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext'
import {
notebookSelectButtonLogic,
@@ -30,8 +30,8 @@ export type NotebookSelectProps = NotebookSelectButtonLogicProps & {
}
export type NotebookSelectPopoverProps = NotebookSelectProps &
- Partial & {
- children?: ReactChild
+ Partial> & {
+ children: ReactElement
}
export type NotebookSelectButtonProps = NotebookSelectProps &
@@ -204,29 +204,27 @@ export function NotebookSelectPopover({
const { setShowPopover } = useActions(logic)
return (
- setShowPopover(false)}
- actionable
+
}
- {...props}
+ sameWidth={false}
+ actionable
+ visible={!!showPopover}
+ onVisibilityChange={(visible) => setShowPopover(visible)}
>
-
setShowPopover(!showPopover)}>
- {children}
-
-
+ {children}
+
)
}
-export function NotebookSelectButton({ children, ...props }: NotebookSelectButtonProps): JSX.Element {
+export function NotebookSelectButton({ children, onNotebookOpened, ...props }: NotebookSelectButtonProps): JSX.Element {
// if nodeLogic is available then the button is on a resource that _is already and currently in a notebook_
const nodeLogic = useNotebookNode()
- const logic = notebookSelectButtonLogic({ ...props })
- const { showPopover, notebooksLoading, notebooksContainingResource } = useValues(logic)
+ const logic = notebookSelectButtonLogic({ ...props, onNotebookOpened })
+ const { showPopover, notebooksContainingResource } = useValues(logic)
const { loadNotebooksContainingResource } = useActions(logic)
useEffect(() => {
@@ -246,12 +244,11 @@ export function NotebookSelectButton({ children, ...props }: NotebookSelectButto
sideIcon={null}
{...props}
active={showPopover}
- loading={notebooksLoading}
onClick={() => {
props.onClick?.()
if (nodeLogic) {
// If we are in a Notebook then we just call the callback directly
- props.onNotebookOpened?.(nodeLogic.props.notebookLogic, nodeLogic)
+ onNotebookOpened?.(nodeLogic.props.notebookLogic, nodeLogic)
}
}}
>
diff --git a/frontend/src/scenes/notebooks/NotebooksScene.tsx b/frontend/src/scenes/notebooks/NotebooksScene.tsx
index df4360e389353..a95fa87a93263 100644
--- a/frontend/src/scenes/notebooks/NotebooksScene.tsx
+++ b/frontend/src/scenes/notebooks/NotebooksScene.tsx
@@ -1,7 +1,12 @@
import './NotebookScene.scss'
-import { LemonButton, LemonTag } from '@posthog/lemon-ui'
+import { IconEllipsis } from '@posthog/icons'
+import { LemonButton, LemonMenu, LemonTag, lemonToast } from '@posthog/lemon-ui'
+import { router } from 'kea-router'
+import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { PageHeader } from 'lib/components/PageHeader'
+import { base64Encode } from 'lib/utils'
+import { getTextFromFile, selectFiles } from 'lib/utils/file-utils'
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
@@ -24,9 +29,47 @@ export function NotebooksScene(): JSX.Element {
}
buttons={
-
- New notebook
-
+ <>
+
+ {
+ void selectFiles({
+ contentType: 'application/json',
+ multiple: false,
+ })
+ .then((files) => getTextFromFile(files[0]))
+ .then((text) => {
+ const data = JSON.parse(text)
+ if (data.type !== 'doc') {
+ throw new Error('Not a notebook')
+ }
+
+ // Looks like a notebook
+ router.actions.push(
+ urls.canvas(),
+ {},
+ {
+ '🦔': base64Encode(text),
+ }
+ )
+ })
+ .catch((e) => {
+ lemonToast.error(e.message)
+ })
+ },
+ },
+ ]}
+ >
+ } status="stealth" size="small" />
+
+
+
+ New notebook
+
+ >
}
/>
diff --git a/frontend/src/scenes/notebooks/notebookSceneLogic.ts b/frontend/src/scenes/notebooks/notebookSceneLogic.ts
index 8d71c0f9f93db..19061b7208bea 100644
--- a/frontend/src/scenes/notebooks/notebookSceneLogic.ts
+++ b/frontend/src/scenes/notebooks/notebookSceneLogic.ts
@@ -3,7 +3,7 @@ import { Scene } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { notebooksModel } from '~/models/notebooksModel'
-import { Breadcrumb, NotebookTarget } from '~/types'
+import { Breadcrumb } from '~/types'
import { notebookLogic } from './Notebook/notebookLogic'
import type { notebookSceneLogicType } from './notebookSceneLogicType'
@@ -44,9 +44,7 @@ export const notebookSceneLogic = kea([
})),
afterMount(({ actions, props }) => {
- if (props.shortId === 'new') {
- actions.createNotebook(NotebookTarget.Scene)
- } else {
+ if (props.shortId !== 'new') {
actions.loadNotebook()
}
}),
diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx
index e2d3bcb0829c1..656435991ab62 100644
--- a/frontend/src/scenes/onboarding/Onboarding.tsx
+++ b/frontend/src/scenes/onboarding/Onboarding.tsx
@@ -8,6 +8,7 @@ import { teamLogic } from 'scenes/teamLogic'
import { ProductKey } from '~/types'
import { OnboardingBillingStep } from './OnboardingBillingStep'
+import { OnboardingInviteTeammates } from './OnboardingInviteTeammates'
import { onboardingLogic, OnboardingStepKey } from './onboardingLogic'
import { OnboardingOtherProductsStep } from './OnboardingOtherProductsStep'
import { OnboardingProductConfiguration } from './OnboardingProductConfiguration'
@@ -25,12 +26,13 @@ export const scene: SceneExport = {
}
/**
- * Wrapper for custom onboarding content. This automatically includes the product intro and billing step.
+ * Wrapper for custom onboarding content. This automatically includes billing, other products, and invite steps.
*/
const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Element => {
const { currentOnboardingStep, shouldShowBillingStep, shouldShowOtherProductsStep } = useValues(onboardingLogic)
const { setAllOnboardingSteps } = useActions(onboardingLogic)
const { product } = useValues(onboardingLogic)
+ const { featureFlags } = useValues(featureFlagLogic)
const [allSteps, setAllSteps] = useState([])
useEffect(() => {
@@ -63,6 +65,10 @@ const OnboardingWrapper = ({ children }: { children: React.ReactNode }): JSX.Ele
const OtherProductsStep =
steps = [...steps, OtherProductsStep]
}
+ if (featureFlags[FEATURE_FLAGS.INVITE_TEAM_MEMBER_ONBOARDING] == 'test') {
+ const inviteTeammatesStep =
+ steps = [...steps, inviteTeammatesStep]
+ }
setAllSteps(steps)
}
diff --git a/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx b/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx
new file mode 100644
index 0000000000000..e7a6c45bdd892
--- /dev/null
+++ b/frontend/src/scenes/onboarding/OnboardingInviteTeammates.tsx
@@ -0,0 +1,76 @@
+import { useActions, useValues } from 'kea'
+import { PhonePairHogs } from 'lib/components/hedgehogs'
+import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
+import { inviteLogic } from 'scenes/settings/organization/inviteLogic'
+import { InviteTeamMatesComponent } from 'scenes/settings/organization/InviteModal'
+
+import { ProductKey } from '~/types'
+
+import { onboardingLogic, OnboardingStepKey } from './onboardingLogic'
+import { OnboardingStep } from './OnboardingStep'
+
+export const OnboardingInviteTeammates = ({ stepKey }: { stepKey: OnboardingStepKey }): JSX.Element => {
+ const { preflight } = useValues(preflightLogic)
+ const { product } = useValues(onboardingLogic)
+ const { inviteTeamMembers } = useActions(inviteLogic)
+
+ const titlePrefix = (): string => {
+ switch (product?.type) {
+ case ProductKey.PRODUCT_ANALYTICS:
+ return 'Analytics are'
+ case ProductKey.SESSION_REPLAY:
+ return 'Replays are'
+ case ProductKey.FEATURE_FLAGS:
+ return 'Feature flags are'
+ case ProductKey.SURVEYS:
+ return 'Surveys are'
+ default:
+ return 'PostHog is'
+ }
+ }
+
+ const likeTo = (): string => {
+ switch (product?.type) {
+ case ProductKey.PRODUCT_ANALYTICS:
+ return 'dig into the data'
+ case ProductKey.SESSION_REPLAY:
+ return 'see how people use your product'
+ case ProductKey.FEATURE_FLAGS:
+ return 'customize user experiences'
+ case ProductKey.SURVEYS:
+ return 'ask all the questions'
+ default:
+ return 'dig into the data'
+ }
+ }
+
+ return (
+ }
+ continueAction={() => preflight?.email_service_available && inviteTeamMembers()}
+ >
+
+
+ ...or maybe even just coworkers. Ya know the ones who like to {likeTo()}?{' '}
+ {preflight?.email_service_available && (
+
+ Enter their email below and we'll send them a custom invite link. Invites expire after 3
+ days.
+
+ )}
+
+ {!preflight?.email_service_available && (
+
+ This PostHog instance isn't configured to send emails. In the meantime, enter your teammates'
+ emails below to generate their custom invite links.{' '}
+ You'll need to share the links with your project members manually . You can
+ invite more people later.
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx
index 2617df2f59132..c4972fee9cb09 100644
--- a/frontend/src/scenes/onboarding/OnboardingStep.tsx
+++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx
@@ -2,7 +2,9 @@ import { LemonButton } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { BridgePage } from 'lib/components/BridgePage/BridgePage'
+import { PhonePairHogs } from 'lib/components/hedgehogs'
import { IconArrowLeft, IconArrowRight } from 'lib/lemon-ui/icons'
+import React from 'react'
import { urls } from 'scenes/urls'
import { onboardingLogic, OnboardingStepKey } from './onboardingLogic'
@@ -17,6 +19,7 @@ export const OnboardingStep = ({
continueAction,
continueOverride,
backActionOverride,
+ hedgehog,
}: {
stepKey: OnboardingStepKey
title: string
@@ -27,9 +30,15 @@ export const OnboardingStep = ({
continueAction?: () => void
continueOverride?: JSX.Element
backActionOverride?: () => void
+ hedgehog?: JSX.Element
}): JSX.Element => {
const { hasNextStep, hasPreviousStep } = useValues(onboardingLogic)
const { completeOnboarding, goToNextStep, goToPreviousStep } = useActions(onboardingLogic)
+
+ const hedgehogToRender = React.cloneElement(hedgehog || , {
+ className: 'h-full w-full',
+ })
+
if (!stepKey) {
throw new Error('stepKey is required in any OnboardingStep')
}
@@ -58,6 +67,8 @@ export const OnboardingStep = ({
}
>
+ {hedgehog &&
{hedgehogToRender}
}
+
{title}
{subtitle}
{children}
diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx
index 4735a06277e40..2b37a54db2329 100644
--- a/frontend/src/scenes/onboarding/onboardingLogic.tsx
+++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx
@@ -20,6 +20,7 @@ export enum OnboardingStepKey {
OTHER_PRODUCTS = 'other_products',
VERIFY = 'verify',
PRODUCT_CONFIGURATION = 'configure',
+ INVITE_TEAMMATES = 'invite_teammates',
}
// These types have to be set like this, so that kea typegen is happy
diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx
index 0627a11bac8ea..95a16ffe7c853 100644
--- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx
+++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx
@@ -1,6 +1,7 @@
import { IconArrowLeft } from '@posthog/icons'
import { LemonButton, LemonCard, LemonDivider, LemonSelect } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
+import { LaptopHog1 } from 'lib/components/hedgehogs'
import { useWindowSize } from 'lib/hooks/useWindowSize'
import { useEffect } from 'react'
import React from 'react'
@@ -47,6 +48,7 @@ export function SDKs({
stepKey={stepKey}
continueOverride={!showSideBySide && panel === 'options' ? <>> : undefined}
backActionOverride={!showSideBySide && panel === 'instructions' ? () => setPanel('options') : undefined}
+ hedgehog={
}
>
diff --git a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx
index c641a38c478a2..b0d73e786b74f 100644
--- a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx
+++ b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx
@@ -82,7 +82,7 @@ export function RelatedFeatureFlags({ distinctId, groups }: Props): JSX.Element
width: 150,
render: function Render(_, featureFlag: RelatedFeatureFlag) {
return (
-
+
{featureFlag.active && featureFlag.value
? capitalizeFirstLetter(featureFlag.value.toString())
: 'False'}
@@ -101,7 +101,7 @@ export function RelatedFeatureFlags({ distinctId, groups }: Props): JSX.Element
{featureFlag.active ? <>{featureFlagMatchMapping[featureFlag.evaluation.reason]}> : '--'}
{matchesSet && (
-
+
Set {(featureFlag.evaluation.condition_index ?? 0) + 1}
)}
@@ -131,7 +131,7 @@ export function RelatedFeatureFlags({ distinctId, groups }: Props): JSX.Element
return (
<>
-
+
diff --git a/frontend/src/scenes/pipeline/Transformations.tsx b/frontend/src/scenes/pipeline/Transformations.tsx
index f5649c27ea781..ccf1c0b7ae7d6 100644
--- a/frontend/src/scenes/pipeline/Transformations.tsx
+++ b/frontend/src/scenes/pipeline/Transformations.tsx
@@ -365,6 +365,7 @@ const MinimalAppView = ({ pluginConfig, order }: { pluginConfig: PluginConfigTyp
-
+
{count
? `${startCount}${endCount - startCount > 1 ? '-' + endCount : ''} of ${count} insight${
diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts
index 8126bf6a97c35..652053e746dda 100644
--- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts
+++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts
@@ -3,7 +3,9 @@ import { eventWithTime } from '@rrweb/types'
import { BuiltLogic, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { beforeUnload } from 'kea-router'
+import { FEATURE_FLAGS } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
+import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { uuid } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { Scene } from 'scenes/sceneTypes'
@@ -11,7 +13,11 @@ import { urls } from 'scenes/urls'
import { Breadcrumb, PersonType, RecordingSnapshot, ReplayTabs, SessionRecordingType } from '~/types'
-import { prepareRecordingSnapshots, sessionRecordingDataLogic } from '../player/sessionRecordingDataLogic'
+import {
+ parseEncodedSnapshots,
+ prepareRecordingSnapshots,
+ sessionRecordingDataLogic,
+} from '../player/sessionRecordingDataLogic'
import type { sessionRecordingDataLogicType } from '../player/sessionRecordingDataLogicType'
import type { sessionRecordingFilePlaybackLogicType } from './sessionRecordingFilePlaybackLogicType'
@@ -33,7 +39,9 @@ export type ExportedSessionRecordingFileV2 = {
}
export const createExportedSessionRecording = (
- logic: BuiltLogic
+ logic: BuiltLogic,
+ // DEBUG signal only, to be removed before release
+ exportUntransformedMobileSnapshotData: boolean
): ExportedSessionRecordingFileV2 => {
const { sessionPlayerMetaData, sessionPlayerSnapshotData } = logic.values
@@ -42,7 +50,9 @@ export const createExportedSessionRecording = (
data: {
id: sessionPlayerMetaData?.id ?? '',
person: sessionPlayerMetaData?.person,
- snapshots: sessionPlayerSnapshotData?.snapshots || [],
+ snapshots: exportUntransformedMobileSnapshotData
+ ? sessionPlayerSnapshotData?.untransformed_snapshots || []
+ : sessionPlayerSnapshotData?.snapshots || [],
},
}
}
@@ -115,6 +125,7 @@ export const sessionRecordingFilePlaybackLogic = kea ({
@@ -166,7 +177,13 @@ export const sessionRecordingFilePlaybackLogic = kea
+
+ exportRecordingToFile(true)}
+ fullWidth
+ sideIcon={ }
+ tooltip="DEBUG ONLY - Export untransformed recording to a file. This can be loaded later into PostHog for playback."
+ >
+ DEBUG Export mobile replay to file DEBUG
+
+
+
openExplorer()}
diff --git a/frontend/src/scenes/session-recordings/player/icons.tsx b/frontend/src/scenes/session-recordings/player/icons.tsx
index fa8a79d631150..14d5f6c6164d4 100644
--- a/frontend/src/scenes/session-recordings/player/icons.tsx
+++ b/frontend/src/scenes/session-recordings/player/icons.tsx
@@ -10,6 +10,7 @@ export function IconWindowOld({ value, className = '', size = 'medium' }: IconWi
const shortValue = typeof value === 'number' ? value : String(value).charAt(0)
return (
+ {/* eslint-disable-next-line react/forbid-dom-props */}
{shortValue}
diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts
index 0f6b87d473200..b66cb79125742 100644
--- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts
+++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts
@@ -6,7 +6,7 @@ import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { FEATURE_FLAGS } from 'lib/constants'
import { Dayjs, dayjs } from 'lib/dayjs'
-import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
+import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic'
import { toParams } from 'lib/utils'
import { chainToElements } from 'lib/utils/elements-chain'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
@@ -41,8 +41,12 @@ const BUFFER_MS = 60000 // +- before and after start and end of a recording to q
let postHogEEModule: PostHogEE
-const parseEncodedSnapshots = async (
- items: (EncodedRecordingSnapshot | string)[],
+function isRecordingSnapshot(x: unknown): x is RecordingSnapshot {
+ return typeof x === 'object' && x !== null && 'type' in x && 'timestamp' in x
+}
+
+export const parseEncodedSnapshots = async (
+ items: (RecordingSnapshot | EncodedRecordingSnapshot | string)[],
sessionId: string,
withMobileTransformer: boolean
): Promise
=> {
@@ -50,15 +54,24 @@ const parseEncodedSnapshots = async (
postHogEEModule = await posthogEE()
}
return items.flatMap((l) => {
+ if (!l) {
+ // blob files have an empty line at the end
+ return []
+ }
try {
const snapshotLine = typeof l === 'string' ? (JSON.parse(l) as EncodedRecordingSnapshot) : l
- const snapshotData = snapshotLine['data']
+ const snapshotData = isRecordingSnapshot(snapshotLine) ? [snapshotLine] : snapshotLine['data']
- // TODO can we type this better and still have mobileEventWithTime in ee folder?
return snapshotData.map((d: unknown) => {
- const snap = postHogEEModule?.mobileReplay?.transformEventToWeb(d) || (d as eventWithTime)
+ const snap = withMobileTransformer
+ ? postHogEEModule?.mobileReplay?.transformEventToWeb(d) || (d as eventWithTime)
+ : (d as eventWithTime)
return {
- windowId: snapshotLine['window_id'],
+ // this handles parsing data that was loaded from blob storage "window_id"
+ // and data that was exported from the front-end "windowId"
+ // we have more than one format of data that we store/pass around
+ // but only one that we play back
+ windowId: snapshotLine['window_id'] || snapshotLine['windowId'],
...(snap || (d as eventWithTime)),
}
})
@@ -164,6 +177,37 @@ function makeEventsQuery(
})
}
+async function processEncodedResponse(
+ encodedResponse: (EncodedRecordingSnapshot | string)[],
+ props: SessionRecordingDataLogicProps,
+ existingData: SessionPlayerSnapshotData | null,
+ featureFlags: FeatureFlagsSet
+): Promise<{ transformed: RecordingSnapshot[]; untransformed: RecordingSnapshot[] | null }> {
+ let untransformed: RecordingSnapshot[] | null = null
+
+ const transformed = prepareRecordingSnapshots(
+ await parseEncodedSnapshots(
+ encodedResponse,
+ props.sessionRecordingId,
+ !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE]
+ ),
+ existingData?.snapshots ?? []
+ )
+
+ if (featureFlags[FEATURE_FLAGS.SESSION_REPLAY_EXPORT_MOBILE_DATA]) {
+ untransformed = prepareRecordingSnapshots(
+ await parseEncodedSnapshots(
+ encodedResponse,
+ props.sessionRecordingId,
+ false // don't transform mobile data
+ ),
+ existingData?.untransformed_snapshots ?? []
+ )
+ }
+
+ return { transformed, untransformed }
+}
+
export const sessionRecordingDataLogic = kea([
path((key) => ['scenes', 'session-recordings', 'sessionRecordingDataLogic', key]),
props({} as SessionRecordingDataLogicProps),
@@ -345,14 +389,14 @@ export const sessionRecordingDataLogic = kea([
source.blob_key
)
- data.snapshots = prepareRecordingSnapshots(
- await parseEncodedSnapshots(
- encodedResponse,
- props.sessionRecordingId,
- !!values.featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE]
- ),
- values.sessionPlayerSnapshotData?.snapshots ?? []
+ const { transformed, untransformed } = await processEncodedResponse(
+ encodedResponse,
+ props,
+ values.sessionPlayerSnapshotData,
+ values.featureFlags
)
+ data.snapshots = transformed
+ data.untransformed_snapshots = untransformed ?? undefined
} else {
const params = toParams({
source: source?.source,
@@ -361,14 +405,14 @@ export const sessionRecordingDataLogic = kea([
})
const response = await api.recordings.listSnapshots(props.sessionRecordingId, params)
if (response.snapshots) {
- data.snapshots = prepareRecordingSnapshots(
- await parseEncodedSnapshots(
- response.snapshots,
- props.sessionRecordingId,
- !!values.featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE]
- ),
- values.sessionPlayerSnapshotData?.snapshots ?? []
+ const { transformed, untransformed } = await processEncodedResponse(
+ response.snapshots,
+ props,
+ values.sessionPlayerSnapshotData,
+ values.featureFlags
)
+ data.snapshots = transformed
+ data.untransformed_snapshots = untransformed ?? undefined
}
if (response.sources) {
diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts
index 5d865c9dda4c8..fcbadb8f175ac 100644
--- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts
+++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts
@@ -170,7 +170,7 @@ export const sessionRecordingPlayerLogic = kea(
incrementErrorCount: true,
incrementWarningCount: (count: number = 1) => ({ count }),
updateFromMetadata: true,
- exportRecordingToFile: true,
+ exportRecordingToFile: (exportUntransformedMobileData?: boolean) => ({ exportUntransformedMobileData }),
deleteRecording: true,
openExplorer: true,
closeExplorer: true,
@@ -881,7 +881,7 @@ export const sessionRecordingPlayerLogic = kea(
cache.pausedMediaElements = []
},
- exportRecordingToFile: async () => {
+ exportRecordingToFile: async ({ exportUntransformedMobileData }) => {
if (!values.sessionPlayerData) {
return
}
@@ -906,11 +906,16 @@ export const sessionRecordingPlayerLogic = kea(
await delay(delayTime)
}
- const payload = createExportedSessionRecording(sessionRecordingDataLogic(props))
+ const payload = createExportedSessionRecording(
+ sessionRecordingDataLogic(props),
+ !!exportUntransformedMobileData
+ )
const recordingFile = new File(
[JSON.stringify(payload, null, 2)],
- `export-${props.sessionRecordingId}.ph-recording.json`,
+ `export-${props.sessionRecordingId}.${
+ exportUntransformedMobileData ? 'mobile.' : ''
+ }ph-recording.json`,
{ type: 'application/json' }
)
diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx
index 7c74657f483ba..b98688c75dc2c 100644
--- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx
+++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx
@@ -6,6 +6,7 @@ import { IconAutocapture, IconKeyboard, IconPinFilled, IconSchedule } from 'lib/
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { colonDelimitedDuration } from 'lib/utils'
+import { Fragment } from 'react'
import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook'
import { asDisplay } from 'scenes/persons/person-utils'
import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic'
@@ -119,9 +120,8 @@ export function PropertyIcons({
) : (
recordingProperties.map(({ property, value, tooltipValue, label }) => {
return (
- <>
+
{
if (e.altKey) {
e.stopPropagation()
@@ -141,7 +141,7 @@ export function PropertyIcons({
)}
/>
{showLabel?.(property) && {label || value} }
- >
+
)
})
)}
diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx
index e64b368496d5a..f34a3ccab77e5 100644
--- a/frontend/src/scenes/settings/SettingsMap.tsx
+++ b/frontend/src/scenes/settings/SettingsMap.tsx
@@ -101,7 +101,7 @@ export const SettingsMap: SettingSection[] = [
},
{
id: 'internal-user-filtering',
- title: 'Filter our internal and test users',
+ title: 'Filter out internal and test users',
component: ,
},
{
diff --git a/frontend/src/scenes/settings/organization/InviteModal.tsx b/frontend/src/scenes/settings/organization/InviteModal.tsx
index d0c719a294d5d..7565bd941e03b 100644
--- a/frontend/src/scenes/settings/organization/InviteModal.tsx
+++ b/frontend/src/scenes/settings/organization/InviteModal.tsx
@@ -20,7 +20,7 @@ import { inviteLogic } from './inviteLogic'
const PLACEHOLDER_NAMES: string[] = [...Array(10).fill('Jane'), ...Array(10).fill('John'), 'Sonic'].sort(
() => Math.random() - 0.5
)
-const MAX_INVITES_AT_ONCE = 20
+export const MAX_INVITES_AT_ONCE = 20
export function EmailUnavailableMessage(): JSX.Element {
return (
@@ -37,7 +37,7 @@ export function EmailUnavailableMessage(): JSX.Element {
)
}
-function InviteRow({ index, isDeletable }: { index: number; isDeletable: boolean }): JSX.Element {
+export function InviteRow({ index, isDeletable }: { index: number; isDeletable: boolean }): JSX.Element {
const name = PLACEHOLDER_NAMES[index % PLACEHOLDER_NAMES.length]
const { invitesToSend } = useValues(inviteLogic)
@@ -105,15 +105,122 @@ function InviteRow({ index, isDeletable }: { index: number; isDeletable: boolean
)
}
-export function InviteModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }): JSX.Element {
- const { user } = useValues(userLogic)
+export function InviteTeamMatesComponent(): JSX.Element {
const { preflight } = useValues(preflightLogic)
- const { invitesToSend, canSubmit, invites } = useValues(inviteLogic)
- const { appendInviteRow, resetInviteRows, inviteTeamMembers, deleteInvite, updateMessage } = useActions(inviteLogic)
+ const { invitesToSend, invites } = useValues(inviteLogic)
+ const { appendInviteRow, deleteInvite, updateMessage } = useActions(inviteLogic)
const invitesReversed = invites.slice().reverse()
const areInvitesCreatable = invitesToSend.length + 1 < MAX_INVITES_AT_ONCE
const areInvitesDeletable = invitesToSend.length > 1
+
+ return (
+ <>
+ {preflight?.licensed_users_available === 0 && (
+
+ You've hit the limit of team members you can invite to your PostHog instance given your license.
+ Please contact sales@posthog.com to upgrade your license.
+
+ )}
+
+
+ Email address
+ {preflight?.email_service_available ? 'Name (optional)' : 'Invite link'}
+
+
+ {invitesReversed.map((invite: OrganizationInviteType) => {
+ return (
+
+
+
{invite.target_email}
+
+
+
+ {invite.is_expired ? (
+
Expired – please recreate
+ ) : (
+ <>
+ {preflight?.email_service_available ? (
+
{invite.first_name}
+ ) : (
+
+
+ {new URL(`/signup/${invite.id}`, document.baseURI).href}
+
+
+ )}
+ >
+ )}
+
}
+ status="danger"
+ onClick={() => {
+ invite.is_expired
+ ? deleteInvite(invite)
+ : LemonDialog.open({
+ title: `Do you want to cancel the invite for ${invite.target_email}?`,
+ primaryButton: {
+ children: 'Yes, cancel invite',
+ status: 'danger',
+ onClick: () => deleteInvite(invite),
+ },
+ secondaryButton: {
+ children: 'No, keep invite',
+ },
+ })
+ }}
+ />
+
+
+ )
+ })}
+
+ {invitesToSend.map((_, index) => (
+
+ ))}
+
+
+ {areInvitesCreatable && (
+ } onClick={appendInviteRow} fullWidth center>
+ Add email address
+
+ )}
+
+
+ {preflight?.email_service_available && (
+
+
+ Message (optional)
+
+
updateMessage(e)}
+ />
+
+ )}
+ >
+ )
+}
+
+export function InviteModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }): JSX.Element {
+ const { user } = useValues(userLogic)
+ const { preflight } = useValues(preflightLogic)
+ const { invitesToSend, canSubmit } = useValues(inviteLogic)
+ const { resetInviteRows, inviteTeamMembers } = useActions(inviteLogic)
+
const validInvitesCount = invitesToSend.filter((invite) => invite.isValid && invite.target_email).length
return (
@@ -173,112 +280,7 @@ export function InviteModal({ isOpen, onClose }: { isOpen: boolean; onClose: ()
>
}
>
- {preflight?.licensed_users_available === 0 && (
-
- You've hit the limit of team members you can invite to your PostHog instance given your license.
- Please contact sales@posthog.com to upgrade your
- license.
-
- )}
-
-
- Email address
-
- {preflight?.email_service_available ? 'Name (optional)' : 'Invite link'}
-
-
-
- {invitesReversed.map((invite: OrganizationInviteType) => {
- return (
-
-
-
{invite.target_email}
-
-
-
- {invite.is_expired ? (
-
Expired – please recreate
- ) : (
- <>
- {preflight?.email_service_available ? (
-
{invite.first_name}
- ) : (
-
-
- {new URL(`/signup/${invite.id}`, document.baseURI).href}
-
-
- )}
- >
- )}
-
}
- status="danger"
- onClick={() => {
- invite.is_expired
- ? deleteInvite(invite)
- : LemonDialog.open({
- title: `Do you want to cancel the invite for ${invite.target_email}?`,
- primaryButton: {
- children: 'Yes, cancel invite',
- status: 'danger',
- onClick: () => deleteInvite(invite),
- },
- secondaryButton: {
- children: 'No, keep invite',
- },
- })
- }}
- />
-
-
- )
- })}
-
- {invitesToSend.map((_, index) => (
-
- ))}
-
-
- {areInvitesCreatable && (
- }
- onClick={appendInviteRow}
- fullWidth
- center
- >
- Add email address
-
- )}
-
-
- {preflight?.email_service_available && (
-
-
- Message (optional)
-
-
updateMessage(e)}
- />
-
- )}
+
)
diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx
index a0b97bb94302b..3311cc1833117 100644
--- a/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx
+++ b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx
@@ -120,12 +120,7 @@ export function CreateRoleModal(): JSX.Element {
<>
Role Members
{roleMembersInFocus.length > 0 ? (
-
+
{roleMembersInFocus.map((member) => {
return (
= (args) => {
},
})
return (
-
+
setValue(val)} />
)
diff --git a/frontend/src/scenes/settings/project/ProjectSettings.tsx b/frontend/src/scenes/settings/project/ProjectSettings.tsx
index 9bb788431fa31..77ace6cdb4675 100644
--- a/frontend/src/scenes/settings/project/ProjectSettings.tsx
+++ b/frontend/src/scenes/settings/project/ProjectSettings.tsx
@@ -29,7 +29,7 @@ export function ProjectDisplayName(): JSX.Element {
}
return (
-
+
@@ -298,6 +299,7 @@ export function BaseAppearance({