Date: Fri, 15 Dec 2023 11:17:58 +0000
Subject: [PATCH 08/33] fix name and description to be writable
---
.../src/scenes/pipeline/AppsManagement.tsx | 7 ------
.../ExportsUnsubscribeModal.tsx | 2 +-
posthog/api/plugin.py | 12 ++++-----
posthog/api/test/test_plugin.py | 12 ++++-----
posthog/batch_exports/http.py | 25 -------------------
5 files changed, 12 insertions(+), 46 deletions(-)
diff --git a/frontend/src/scenes/pipeline/AppsManagement.tsx b/frontend/src/scenes/pipeline/AppsManagement.tsx
index 53c776f04bc45..53b3ab5ffca46 100644
--- a/frontend/src/scenes/pipeline/AppsManagement.tsx
+++ b/frontend/src/scenes/pipeline/AppsManagement.tsx
@@ -11,7 +11,6 @@ import { SceneExport } from 'scenes/sceneTypes'
import { PluginInstallationType, PluginType } from '~/types'
import { appsManagementLogic } from './appsManagementLogic'
-import { ExportsUnsubscribeModal, exportsUnsubscribeModalLogic } from './ExportsUnsubscribeModal'
import { RenderApp } from './utils'
export const scene: SceneExport = {
@@ -30,8 +29,6 @@ export function AppsManagement(): JSX.Element {
localPlugins,
} = useValues(appsManagementLogic)
const { isDev, isCloudOrDev } = useValues(preflightLogic)
- const { startUnsubscribe } = useActions(exportsUnsubscribeModalLogic)
- const { loading } = useValues(exportsUnsubscribeModalLogic)
if (!canInstallPlugins || !canGloballyManagePlugins) {
return <>You don't have permission to manage apps.>
@@ -39,10 +36,6 @@ export function AppsManagement(): JSX.Element {
return (
-
-
- Unsubscribe from batch exports
-
{isCloudOrDev &&
(missingGlobalPlugins.length > 0 ||
shouldBeGlobalPlugins.length > 0 ||
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
index 8a174868a07e0..b48c7a9ea88ec 100644
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
@@ -22,7 +22,7 @@ export function ExportsUnsubscribeModal(): JSX.Element {
onClose={closeModal}
isOpen={modalOpen}
width={600}
- title="Disable remaining export apps"
+ title="Disable remaining data pipelines apps"
description="To make sure there's no unexpected impact on your data, you need to explicitly disable the following apps before unsubscribing:"
footer={
<>
diff --git a/posthog/api/plugin.py b/posthog/api/plugin.py
index fd8e82af53452..b5a8bfc130218 100644
--- a/posthog/api/plugin.py
+++ b/posthog/api/plugin.py
@@ -549,8 +549,6 @@ class PluginConfigSerializer(serializers.ModelSerializer):
plugin_info = serializers.SerializerMethodField()
delivery_rate_24h = serializers.SerializerMethodField()
error = serializers.SerializerMethodField()
- name = serializers.SerializerMethodField()
- description = serializers.SerializerMethodField()
class Meta:
model = PluginConfig
@@ -613,11 +611,11 @@ def get_config(self, plugin_config: PluginConfig):
return new_plugin_config
- def get_name(self, plugin_config: PluginConfig):
- return plugin_config.name or plugin_config.plugin.name
-
- def get_description(self, plugin_config: PluginConfig):
- return plugin_config.description or plugin_config.plugin.description
+ def to_representation(self, instance: Any) -> Any:
+ representation = super().to_representation(instance)
+ representation["name"] = representation["name"] or instance.plugin.name
+ representation["description"] = representation["description"] or instance.plugin.description
+ return representation
def get_plugin_info(self, plugin_config: PluginConfig):
if "view" in self.context and self.context["view"].action == "retrieve":
diff --git a/posthog/api/test/test_plugin.py b/posthog/api/test/test_plugin.py
index 12ce1bd16e079..a2eb76174fb48 100644
--- a/posthog/api/test/test_plugin.py
+++ b/posthog/api/test/test_plugin.py
@@ -1356,8 +1356,8 @@ def test_create_plugin_config_with_secrets(self, mock_get, mock_reload):
"delivery_rate_24h": None,
"created_at": mock.ANY,
"updated_at": mock.ANY,
- "name": None,
- "description": None,
+ "name": "Hello World",
+ "description": "Greet the World and Foo a Bar, JS edition!",
"deleted": False,
},
)
@@ -1385,8 +1385,8 @@ def test_create_plugin_config_with_secrets(self, mock_get, mock_reload):
"delivery_rate_24h": None,
"created_at": mock.ANY,
"updated_at": mock.ANY,
- "name": None,
- "description": None,
+ "name": "Hello World",
+ "description": "Greet the World and Foo a Bar, JS edition!",
"deleted": False,
},
)
@@ -1416,8 +1416,8 @@ def test_create_plugin_config_with_secrets(self, mock_get, mock_reload):
"delivery_rate_24h": None,
"created_at": mock.ANY,
"updated_at": mock.ANY,
- "name": None,
- "description": None,
+ "name": "Hello World",
+ "description": "Greet the World and Foo a Bar, JS edition!",
"deleted": False,
},
)
diff --git a/posthog/batch_exports/http.py b/posthog/batch_exports/http.py
index 4d29777e92b3d..38c9354df09e8 100644
--- a/posthog/batch_exports/http.py
+++ b/posthog/batch_exports/http.py
@@ -394,28 +394,3 @@ def get_queryset(self):
limit=limit,
level_filter=level_filter,
)
-
-
-# queryset = BatchExport.objects.all()
-# serializer_class = BatchExportSerializer
-# permission_classes = [
-# IsAuthenticated,
-# ProjectMembershipNecessaryPermissions,
-# OrganizationMemberPermissions,
-# ]
-# permission_classes = [
-# IsAuthenticated,
-# ProjectMembershipNecessaryPermissions,
-# TeamMemberAccessPermission,
-# ]
-
-# def get_queryset(self):
-# if not isinstance(self.request.user, User) or self.request.user.current_team is None:
-# raise NotAuthenticated()
-
-# return (
-# self.queryset.filter(team_id=self.team_id)
-# .exclude(deleted=True)
-# .order_by("-created_at")
-# .prefetch_related("destination")
-# )
From 7acd3046044a042af7c16086dc16578483416912 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Thu, 21 Dec 2023 11:38:13 -0800
Subject: [PATCH 09/33] rename and move table to main billing unsub modal
---
.../src/scenes/billing/BillingProduct.tsx | 16 ++-
.../scenes/billing/UnsubscribeSurveyModal.tsx | 10 +-
.../ExportsUnsubscribeModal.stories.tsx | 21 ----
.../ExportsUnsubscribeModal.tsx | 98 -------------------
.../pipeline/ExportsUnsubscribeModal/index.ts | 2 -
.../ExportsUnsubscribeTable.stories.tsx | 21 ++++
.../ExportsUnsubscribeTable.tsx | 73 ++++++++++++++
.../exportsUnsubscribeTableLogic.tsx} | 11 +--
.../pipeline/ExportsUnsubscribeTable/index.ts | 2 +
9 files changed, 121 insertions(+), 133 deletions(-)
delete mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.stories.tsx
delete mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
delete mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeModal/index.ts
create mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
create mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
rename frontend/src/scenes/pipeline/{ExportsUnsubscribeModal/exportsUnsubscribeModalLogic.tsx => ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx} (93%)
create mode 100644 frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts
diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx
index 4174817b26fcd..0961fe223e8f5 100644
--- a/frontend/src/scenes/billing/BillingProduct.tsx
+++ b/frontend/src/scenes/billing/BillingProduct.tsx
@@ -46,9 +46,10 @@ export const getTierDescription = (
export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonType }): JSX.Element => {
const { billing, redirectPath } = useValues(billingLogic)
- const { deactivateProduct } = useActions(billingLogic)
- const { isPricingModalOpen, currentAndUpgradePlans } = useValues(billingProductLogic({ product: addon }))
- const { toggleIsPricingModalOpen } = useActions(billingProductLogic({ product: addon }))
+ const { isPricingModalOpen, currentAndUpgradePlans, surveyID } = useValues(billingProductLogic({ product: addon }))
+ const { toggleIsPricingModalOpen, reportSurveyShown, setSurveyResponse } = useActions(
+ billingProductLogic({ product: addon })
+ )
const productType = { plural: `${addon.unit}s`, singular: addon.unit }
const tierDisplayOptions: LemonSelectOptions = [
@@ -89,7 +90,13 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
- deactivateProduct(addon.type)}>
+ {
+ setSurveyResponse(addon.type, '$survey_response_1')
+ reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, addon.type)
+ }}
+ >
Remove addon
>
@@ -136,6 +143,7 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
: currentAndUpgradePlans?.upgradePlan?.plan_key
}
/>
+ {surveyID && }
)
}
diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
index e13f5143a830e..f45d294f89ea2 100644
--- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
+++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
@@ -1,12 +1,17 @@
import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
+import { ExportsUnsubscribeTable } from 'scenes/pipeline/ExportsUnsubscribeTable'
-import { BillingProductV2Type } from '~/types'
+import { BillingProductV2AddonType, BillingProductV2Type } from '~/types'
import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'
-export const UnsubscribeSurveyModal = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => {
+export const UnsubscribeSurveyModal = ({
+ product,
+}: {
+ product: BillingProductV2Type | BillingProductV2AddonType
+}): JSX.Element | null => {
const { surveyID, surveyResponse } = useValues(billingProductLogic({ product }))
const { setSurveyResponse, reportSurveySent, reportSurveyDismissed } = useActions(billingProductLogic({ product }))
const { deactivateProduct } = useActions(billingLogic)
@@ -20,6 +25,7 @@ export const UnsubscribeSurveyModal = ({ product }: { product: BillingProductV2T
width={'max(40vw)'}
>
+ {product.type === 'group_analytics' ?
: <>>}
{`Why are you unsubscribing from ${product.name}?`}
= {
- title: 'Components/Exports Unsubscribe Modal',
-}
-export default meta
-
-export const _ExportsUnsubscribeModal: StoryFn = () => {
- const { openModal } = useActions(exportsUnsubscribeModalLogic)
-
- useEffect(() => {
- openModal()
- })
-
- return
-}
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
deleted file mode 100644
index b48c7a9ea88ec..0000000000000
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/ExportsUnsubscribeModal.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useActions, useValues } from 'kea'
-import { LemonButton } from 'lib/lemon-ui/LemonButton'
-import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown'
-import { LemonModal } from 'lib/lemon-ui/LemonModal'
-import { LemonTable } from 'lib/lemon-ui/LemonTable'
-import { organizationLogic } from 'scenes/organizationLogic'
-
-import { exportsUnsubscribeModalLogic } from './exportsUnsubscribeModalLogic'
-
-export function ExportsUnsubscribeModal(): JSX.Element {
- const { modalOpen, unsubscribeDisabledReason, loading, itemsToDisable } = useValues(exportsUnsubscribeModalLogic)
- const { closeModal, disablePlugin, pauseBatchExport, completeUnsubscribe } =
- useActions(exportsUnsubscribeModalLogic)
- const { currentOrganization } = useValues(organizationLogic)
-
- if (!currentOrganization) {
- return <>>
- }
-
- return (
-
-
- Cancel
-
-
- Unsubscribe
-
- >
- }
- >
- team.id === item.team_id)?.name
- },
- },
- {
- render: function RenderAppInfo(_, item) {
- return item.icon
- },
- },
- {
- title: 'Name',
- render: function RenderPluginName(_, item) {
- return (
- <>
- {item.name}
- {item.description && (
-
- {item.description}
-
- )}
- >
- )
- },
- },
- {
- title: '',
- render: function RenderPluginDisable(_, item) {
- return (
- {
- if (item.plugin_config_id !== undefined) {
- disablePlugin(item.plugin_config_id)
- } else if (item.batch_export_id !== undefined) {
- pauseBatchExport(item.batch_export_id)
- }
- }}
- disabledReason={item.disabled ? 'Already disabled' : null}
- >
- Disable
-
- )
- },
- },
- ]}
- />
-
- )
-}
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/index.ts b/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/index.ts
deleted file mode 100644
index fb9b78e00240f..0000000000000
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ExportsUnsubscribeModal } from './ExportsUnsubscribeModal'
-export { exportsUnsubscribeModalLogic } from './exportsUnsubscribeModalLogic'
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
new file mode 100644
index 0000000000000..6e72bd0488d48
--- /dev/null
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
@@ -0,0 +1,21 @@
+import { Meta, StoryFn } from '@storybook/react'
+import { useActions } from 'kea'
+import { useEffect } from 'react'
+
+import { ExportsUnsubscribeTable } from './ExportsUnsubscribeTable'
+import { exportsUnsubscribeTableLogic } from './exportsUnsubscribeTableLogic'
+
+const meta: Meta = {
+ title: 'Components/Exports Unsubscribe Table',
+}
+export default meta
+
+export const _ExportsUnsubscribeTable: StoryFn = () => {
+ const { openModal } = useActions(exportsUnsubscribeTableLogic)
+
+ useEffect(() => {
+ openModal()
+ })
+
+ return
+}
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
new file mode 100644
index 0000000000000..bf512fe0ef520
--- /dev/null
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
@@ -0,0 +1,73 @@
+import { useActions, useValues } from 'kea'
+import { LemonButton } from 'lib/lemon-ui/LemonButton'
+import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown'
+import { LemonTable } from 'lib/lemon-ui/LemonTable'
+import { organizationLogic } from 'scenes/organizationLogic'
+
+import { exportsUnsubscribeTableLogic } from './exportsUnsubscribeTableLogic'
+
+export function ExportsUnsubscribeTable(): JSX.Element {
+ const { loading, itemsToDisable } = useValues(exportsUnsubscribeTableLogic)
+ const { disablePlugin, pauseBatchExport } = useActions(exportsUnsubscribeTableLogic)
+ const { currentOrganization } = useValues(organizationLogic)
+
+ if (!currentOrganization) {
+ return <>>
+ }
+
+ return (
+ team.id === item.team_id)?.name
+ },
+ },
+ {
+ render: function RenderAppInfo(_, item) {
+ return item.icon
+ },
+ },
+ {
+ title: 'Name',
+ render: function RenderPluginName(_, item) {
+ return (
+ <>
+ {item.name}
+ {item.description && (
+
+ {item.description}
+
+ )}
+ >
+ )
+ },
+ },
+ {
+ title: '',
+ render: function RenderPluginDisable(_, item) {
+ return (
+ {
+ if (item.plugin_config_id !== undefined) {
+ disablePlugin(item.plugin_config_id)
+ } else if (item.batch_export_id !== undefined) {
+ pauseBatchExport(item.batch_export_id)
+ }
+ }}
+ disabledReason={item.disabled ? 'Already disabled' : null}
+ >
+ Disable
+
+ )
+ },
+ },
+ ]}
+ />
+ )
+}
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/exportsUnsubscribeModalLogic.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
similarity index 93%
rename from frontend/src/scenes/pipeline/ExportsUnsubscribeModal/exportsUnsubscribeModalLogic.tsx
rename to frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
index 05c312a6dc267..775a296b6253a 100644
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeModal/exportsUnsubscribeModalLogic.tsx
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
@@ -10,7 +10,6 @@ import { BatchExportConfiguration, PluginConfigTypeNew } from '~/types'
import { pipelineTransformationsLogic } from '../transformationsLogic'
import { RenderApp } from '../utils'
-import type { exportsUnsubscribeModalLogicType } from './exportsUnsubscribeModalLogicType'
export interface ItemToDisable {
plugin_config_id: number | undefined // exactly one of plugin_config_id or batch_export_id is set
@@ -22,8 +21,8 @@ export interface ItemToDisable {
disabled: boolean
}
-export const exportsUnsubscribeModalLogic = kea([
- path(['scenes', 'pipeline', 'exportsUnsubscribeModalLogic']),
+export const exportsUnsubscribeTableLogic = kea([
+ path(['scenes', 'pipeline', 'ExportsUnsubscribeTableLogic']),
connect({
values: [pluginsLogic, ['plugins'], pipelineTransformationsLogic, ['canConfigurePlugins'], userLogic, ['user']],
}),
@@ -135,10 +134,10 @@ export const exportsUnsubscribeModalLogic = kea ({
// Usage guide:
- // const { startUnsubscribe } = useActions(exportsUnsubscribeModalLogic)
- // const { loading } = useValues(exportsUnsubscribeModalLogic)
+ // const { startUnsubscribe } = useActions(ExportsUnsubscribeTableLogic)
+ // const { loading } = useValues(ExportsUnsubscribeTableLogic)
// return (<>
- //
+ //
// Unsubscribe from data pipelines
// >)
startUnsubscribe() {
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts
new file mode 100644
index 0000000000000..bd188e4fae179
--- /dev/null
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts
@@ -0,0 +1,2 @@
+export { ExportsUnsubscribeTable } from './ExportsUnsubscribeTable'
+export { exportsUnsubscribeTableLogic } from './exportsUnsubscribeTableLogic'
From 06c7e85d7e7d9f31338d000dcbd8c35e04d2c756 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 10:35:05 -0800
Subject: [PATCH 10/33] add text and disabled reason
---
.../layout/navigation/TopBar/topBarLogic.ts | 20 +++++++++++++++
.../scenes/billing/UnsubscribeSurveyModal.tsx | 25 +++++++++++++++++--
.../exportsUnsubscribeTableLogic.tsx | 4 ++-
3 files changed, 46 insertions(+), 3 deletions(-)
create mode 100644 frontend/src/layout/navigation/TopBar/topBarLogic.ts
diff --git a/frontend/src/layout/navigation/TopBar/topBarLogic.ts b/frontend/src/layout/navigation/TopBar/topBarLogic.ts
new file mode 100644
index 0000000000000..a1c2f8ce00a70
--- /dev/null
+++ b/frontend/src/layout/navigation/TopBar/topBarLogic.ts
@@ -0,0 +1,20 @@
+import { actions, kea, path, reducers } from 'kea'
+
+import type { topBarLogicType } from './topBarLogicType'
+
+export const topBarLogic = kea([
+ path(['layout', 'navigation', 'TopBar', 'topBarLogic']),
+ actions({
+ toggleProjectSwitcher: true,
+ hideProjectSwitcher: true,
+ }),
+ reducers({
+ isProjectSwitcherShown: [
+ false,
+ {
+ toggleProjectSwitcher: (state) => !state,
+ hideProjectSwitcher: () => false,
+ },
+ ],
+ }),
+])
diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
index f45d294f89ea2..3e03326674f4c 100644
--- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
+++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
@@ -1,6 +1,6 @@
import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
-import { ExportsUnsubscribeTable } from 'scenes/pipeline/ExportsUnsubscribeTable'
+import { ExportsUnsubscribeTable, exportsUnsubscribeTableLogic } from 'scenes/pipeline/ExportsUnsubscribeTable'
import { BillingProductV2AddonType, BillingProductV2Type } from '~/types'
@@ -15,8 +15,15 @@ export const UnsubscribeSurveyModal = ({
const { surveyID, surveyResponse } = useValues(billingProductLogic({ product }))
const { setSurveyResponse, reportSurveySent, reportSurveyDismissed } = useActions(billingProductLogic({ product }))
const { deactivateProduct } = useActions(billingLogic)
+ const { unsubscribeDisabledReason, itemsToDisable } = useValues(exportsUnsubscribeTableLogic)
const textAreaNotEmpty = surveyResponse['$survey_response']?.length > 0
+ const includesExportsAddon =
+ product.type == 'group_analytics' ||
+ (product.type == 'product_analytics' &&
+ (product as BillingProductV2Type)?.addons?.filter((addon) => addon.type === 'group_analytics')[0]
+ ?.subscribed)
+
return (
{
@@ -25,7 +32,20 @@ export const UnsubscribeSurveyModal = ({
width={'max(40vw)'}
>
- {product.type === 'group_analytics' ?
: <>>}
+ {includesExportsAddon && itemsToDisable.length > 0 ? (
+
+
+
{`Important: Disable remaining export apps`}
+
+ To avoid unexpected impact on your data, you must explicitly disable the following apps
+ and exports before unsubscribing:
+
+
+
+
+ ) : (
+ <>>
+ )}
{`Why are you unsubscribing from ${product.name}?`}
{
textAreaNotEmpty
? reportSurveySent(surveyID, surveyResponse)
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
index 775a296b6253a..4a5464f88a3d8 100644
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
@@ -11,6 +11,8 @@ import { BatchExportConfiguration, PluginConfigTypeNew } from '~/types'
import { pipelineTransformationsLogic } from '../transformationsLogic'
import { RenderApp } from '../utils'
+import type { exportsUnsubscribeTableLogicType } from './exportsUnsubscribeTableLogicType'
+
export interface ItemToDisable {
plugin_config_id: number | undefined // exactly one of plugin_config_id or batch_export_id is set
batch_export_id: string | undefined
@@ -21,7 +23,7 @@ export interface ItemToDisable {
disabled: boolean
}
-export const exportsUnsubscribeTableLogic = kea([
+export const exportsUnsubscribeTableLogic = kea([
path(['scenes', 'pipeline', 'ExportsUnsubscribeTableLogic']),
connect({
values: [pluginsLogic, ['plugins'], pipelineTransformationsLogic, ['canConfigurePlugins'], userLogic, ['user']],
From a1967e9626957d2977d451e2cedfca894c8e1ebf Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 10:42:37 -0800
Subject: [PATCH 11/33] improve disabled state
---
.../ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
index bf512fe0ef520..177e6d4bf1cdb 100644
--- a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
+++ b/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
@@ -1,3 +1,4 @@
+import { IconCheckCircle } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown'
@@ -61,8 +62,9 @@ export function ExportsUnsubscribeTable(): JSX.Element {
}
}}
disabledReason={item.disabled ? 'Already disabled' : null}
+ icon={item.disabled ? : undefined}
>
- Disable
+ {item.disabled ? 'Disabled' : 'Disable'}
)
},
From 599b077c1b46d2cdabecfbeaf89a61547df6fb36 Mon Sep 17 00:00:00 2001
From: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 22 Dec 2023 18:42:36 +0000
Subject: [PATCH 12/33] Update UI snapshots for `chromium` (1)
---
python-xmlsec | 1 +
1 file changed, 1 insertion(+)
create mode 160000 python-xmlsec
diff --git a/python-xmlsec b/python-xmlsec
new file mode 160000
index 0000000000000..156394743a0c7
--- /dev/null
+++ b/python-xmlsec
@@ -0,0 +1 @@
+Subproject commit 156394743a0c712e6638fe6e7e300c2f24b4fb12
From 6c7ccacdb0f751af6e5be0ed25fc60e91a2a53b1 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 10:54:13 -0800
Subject: [PATCH 13/33] move into billing
---
.../ExportsUnsubscribeTable.stories.tsx | 0
.../ExportsUnsubscribeTable.tsx | 0
.../exportsUnsubscribeTableLogic.tsx | 126 ++++++++++++++++++
.../ExportsUnsubscribeTable/index.ts | 0
.../scenes/billing/UnsubscribeSurveyModal.tsx | 2 +-
5 files changed, 127 insertions(+), 1 deletion(-)
rename frontend/src/scenes/{pipeline => billing}/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx (100%)
rename frontend/src/scenes/{pipeline => billing}/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx (100%)
create mode 100644 frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
rename frontend/src/scenes/{pipeline => billing}/ExportsUnsubscribeTable/index.ts (100%)
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx b/frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
similarity index 100%
rename from frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
rename to frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx b/frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
similarity index 100%
rename from frontend/src/scenes/pipeline/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
rename to frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.tsx
diff --git a/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx b/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
new file mode 100644
index 0000000000000..ea5ccad6cbf55
--- /dev/null
+++ b/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
@@ -0,0 +1,126 @@
+import { actions, afterMount, connect, kea, path, selectors } from 'kea'
+import { loaders } from 'kea-loaders'
+import api from 'lib/api'
+import { IconDatabase } from 'lib/lemon-ui/icons'
+import { pluginsLogic } from 'scenes/plugins/pluginsLogic'
+import { userLogic } from 'scenes/userLogic'
+
+import { BatchExportConfiguration, PluginConfigTypeNew } from '~/types'
+
+import { pipelineTransformationsLogic } from '../../pipeline/transformationsLogic'
+import { RenderApp } from '../../pipeline/utils'
+import type { exportsUnsubscribeTableLogicType } from './exportsUnsubscribeTableLogicType'
+
+export interface ItemToDisable {
+ plugin_config_id: number | undefined // exactly one of plugin_config_id or batch_export_id is set
+ batch_export_id: string | undefined
+ team_id: number
+ name: string
+ description: string | undefined
+ icon: JSX.Element
+ disabled: boolean
+}
+
+export const exportsUnsubscribeTableLogic = kea([
+ path(['scenes', 'pipeline', 'ExportsUnsubscribeTableLogic']),
+ connect({
+ values: [pluginsLogic, ['plugins'], pipelineTransformationsLogic, ['canConfigurePlugins'], userLogic, ['user']],
+ }),
+
+ actions({
+ disablePlugin: (id: number) => ({ id }),
+ pauseBatchExport: (id: string) => ({ id }),
+ }),
+ loaders(({ values }) => ({
+ pluginConfigsToDisable: [
+ {} as Record,
+ {
+ loadPluginConfigs: async () => {
+ const res = await api.get(
+ `api/organizations/@current/plugins/exports_unsubscribe_configs`
+ )
+ return Object.fromEntries(res.map((pluginConfig) => [pluginConfig.id, pluginConfig]))
+ },
+ disablePlugin: async ({ id }) => {
+ if (!values.canConfigurePlugins) {
+ return values.pluginConfigsToDisable
+ }
+ const response = await api.update(`api/plugin_config/${id}`, { enabled: false })
+ return { ...values.pluginConfigsToDisable, [id]: response }
+ },
+ },
+ ],
+ batchExportConfigs: [
+ {} as Record,
+ {
+ loadBatchExportConfigs: async () => {
+ const res = await api.loadPaginatedResults(`api/organizations/@current/batch_exports`)
+ return Object.fromEntries(
+ res
+ .filter((batchExportConfig) => !batchExportConfig.paused)
+ .map((batchExportConfig) => [batchExportConfig.id, batchExportConfig])
+ )
+ },
+ pauseBatchExport: async ({ id }) => {
+ await api.create(`api/organizations/@current/batch_exports/${id}/pause`)
+ return { ...values.batchExportConfigs, [id]: { ...values.batchExportConfigs[id], paused: true } }
+ },
+ },
+ ],
+ })),
+ selectors({
+ loading: [
+ (s) => [s.batchExportConfigsLoading, s.pluginConfigsToDisableLoading],
+ (batchExportsLoading, pluginConfigsLoading) => batchExportsLoading || pluginConfigsLoading,
+ ],
+ unsubscribeDisabledReason: [
+ (s) => [s.loading, s.pluginConfigsToDisable, s.batchExportConfigs],
+ (loading, pluginConfigsToDisable, batchExportConfigs) => {
+ // TODO: check for permissions first - that the user has access to all the projects for this org
+ return loading
+ ? 'Loading...'
+ : Object.values(pluginConfigsToDisable).some((pluginConfig) => pluginConfig.enabled)
+ ? 'All apps above must be disabled first'
+ : Object.values(batchExportConfigs).some((batchExportConfig) => !batchExportConfig.paused)
+ ? 'All batch exports must be disabled first'
+ : null
+ },
+ ],
+ itemsToDisable: [
+ (s) => [s.pluginConfigsToDisable, s.batchExportConfigs, s.plugins],
+ (pluginConfigsToDisable, batchExportConfigs, plugins) => {
+ const pluginConfigs = Object.values(pluginConfigsToDisable).map((pluginConfig) => {
+ return {
+ plugin_config_id: pluginConfig.id,
+ team_id: pluginConfig.team_id,
+ name: pluginConfig.name,
+ description: pluginConfig.description,
+ icon: ,
+ disabled: !pluginConfig.enabled,
+ } as ItemToDisable
+ })
+ const batchExports = Object.values(batchExportConfigs).map((batchExportConfig) => {
+ return {
+ batch_export_id: batchExportConfig.id,
+ team_id: batchExportConfig.team_id,
+ name: batchExportConfig.name,
+ description: batchExportConfig.destination.type,
+ icon: (
+
+ ),
+ disabled: batchExportConfig.paused,
+ } as ItemToDisable
+ })
+ return [...pluginConfigs, ...batchExports]
+ },
+ ],
+ }),
+ afterMount(({ actions }) => {
+ actions.loadPluginConfigs()
+ actions.loadBatchExportConfigs()
+ }),
+])
diff --git a/frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts b/frontend/src/scenes/billing/ExportsUnsubscribeTable/index.ts
similarity index 100%
rename from frontend/src/scenes/pipeline/ExportsUnsubscribeTable/index.ts
rename to frontend/src/scenes/billing/ExportsUnsubscribeTable/index.ts
diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
index 3e03326674f4c..5b518d0f6cc0d 100644
--- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
+++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
@@ -1,11 +1,11 @@
import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
-import { ExportsUnsubscribeTable, exportsUnsubscribeTableLogic } from 'scenes/pipeline/ExportsUnsubscribeTable'
import { BillingProductV2AddonType, BillingProductV2Type } from '~/types'
import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'
+import { ExportsUnsubscribeTable, exportsUnsubscribeTableLogic } from './ExportsUnsubscribeTable'
export const UnsubscribeSurveyModal = ({
product,
From 102bbaeb17044ae240f3217fb52f2d4e46c48d83 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 10:55:46 -0800
Subject: [PATCH 14/33] use correct product name
---
frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
index 5b518d0f6cc0d..f48b2b41a48f0 100644
--- a/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
+++ b/frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
@@ -18,10 +18,10 @@ export const UnsubscribeSurveyModal = ({
const { unsubscribeDisabledReason, itemsToDisable } = useValues(exportsUnsubscribeTableLogic)
const textAreaNotEmpty = surveyResponse['$survey_response']?.length > 0
- const includesExportsAddon =
- product.type == 'group_analytics' ||
+ const includesPipelinesAddon =
+ product.type == 'data_pipelines' ||
(product.type == 'product_analytics' &&
- (product as BillingProductV2Type)?.addons?.filter((addon) => addon.type === 'group_analytics')[0]
+ (product as BillingProductV2Type)?.addons?.filter((addon) => addon.type === 'data_pipelines')[0]
?.subscribed)
return (
@@ -32,7 +32,7 @@ export const UnsubscribeSurveyModal = ({
width={'max(40vw)'}
>
- {includesExportsAddon && itemsToDisable.length > 0 ? (
+ {includesPipelinesAddon && itemsToDisable.length > 0 ? (
{`Important: Disable remaining export apps`}
@@ -107,7 +107,7 @@ export const UnsubscribeSurveyModal = ({
{
textAreaNotEmpty
? reportSurveySent(surveyID, surveyResponse)
From 9609f0981668c509a4c402307b80b092fe9ad989 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 14:09:43 -0800
Subject: [PATCH 15/33] stories
---
.../api/organizations/@current/@current.json | 22 ++
.../organizations/@current/batchExports.json | 34 +++
.../plugins/exportsUnsubscribeConfigs.json | 24 ++
.../@current/plugins/plugins.json | 219 ++++++++++++++++++
.../src/scenes/billing/Billing.stories.tsx | 60 +++++
.../ExportsUnsubscribeTable.stories.tsx | 21 --
6 files changed, 359 insertions(+), 21 deletions(-)
create mode 100644 frontend/src/mocks/fixtures/api/organizations/@current/@current.json
create mode 100644 frontend/src/mocks/fixtures/api/organizations/@current/batchExports.json
create mode 100644 frontend/src/mocks/fixtures/api/organizations/@current/plugins/exportsUnsubscribeConfigs.json
create mode 100644 frontend/src/mocks/fixtures/api/organizations/@current/plugins/plugins.json
delete mode 100644 frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
diff --git a/frontend/src/mocks/fixtures/api/organizations/@current/@current.json b/frontend/src/mocks/fixtures/api/organizations/@current/@current.json
new file mode 100644
index 0000000000000..ed8ca1eac9e38
--- /dev/null
+++ b/frontend/src/mocks/fixtures/api/organizations/@current/@current.json
@@ -0,0 +1,22 @@
+{
+ "id": "0178a3ab-d163-0000-4b55-bceadebb03fa",
+ "name": "Hogflix Movies",
+ "created_at": "2021-04-05T20:14:09.763753Z",
+ "updated_at": "2021-04-05T20:14:25.443181Z",
+ "membership_level": 15,
+ "plugins_access_level": 9,
+ "teams": [
+ {
+ "id": 2,
+ "uuid": "0178a3ab-d1e5-0000-c5ca-da746c68f506",
+ "organization": "0178a3ab-d163-0000-4b55-bceadebb03fa",
+ "api_token": "tJy-b6mTLwvNP_ZJHrfgn99pQCYOGFE3-nwpb8utFa8",
+ "name": "Hogflix Demo App",
+ "completed_snippet_onboarding": true,
+ "ingested_event": true,
+ "is_demo": true,
+ "timezone": "Europe/Kiev"
+ }
+ ],
+ "available_features": []
+}
diff --git a/frontend/src/mocks/fixtures/api/organizations/@current/batchExports.json b/frontend/src/mocks/fixtures/api/organizations/@current/batchExports.json
new file mode 100644
index 0000000000000..1926188189785
--- /dev/null
+++ b/frontend/src/mocks/fixtures/api/organizations/@current/batchExports.json
@@ -0,0 +1,34 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": "018c8dcd-1598-0001-a082-fa2d1e2dbb74",
+ "team_id": 2,
+ "name": "my export",
+ "destination": {
+ "type": "Postgres",
+ "config": {
+ "host": "sdsdd.domc.com",
+ "port": 5432,
+ "user": "sdssd",
+ "schema": "sddd",
+ "database": "sdsd",
+ "password": "sdsdsd",
+ "table_name": "ssss",
+ "exclude_events": ["sdd"],
+ "include_events": ["sdddddd"]
+ }
+ },
+ "interval": "day",
+ "paused": false,
+ "created_at": "2023-12-21T19:14:37.135878Z",
+ "last_updated_at": "2023-12-22T18:42:43.863292Z",
+ "last_paused_at": "2023-12-22T18:41:50.122946Z",
+ "start_at": null,
+ "end_at": "2023-12-30T08:00:00Z",
+ "latest_runs": []
+ }
+ ]
+}
diff --git a/frontend/src/mocks/fixtures/api/organizations/@current/plugins/exportsUnsubscribeConfigs.json b/frontend/src/mocks/fixtures/api/organizations/@current/plugins/exportsUnsubscribeConfigs.json
new file mode 100644
index 0000000000000..e6cfa6a1bdfcc
--- /dev/null
+++ b/frontend/src/mocks/fixtures/api/organizations/@current/plugins/exportsUnsubscribeConfigs.json
@@ -0,0 +1,24 @@
+[
+ {
+ "id": 2,
+ "plugin": 4,
+ "enabled": true,
+ "order": 2,
+ "config": {
+ "host": "eu.posthog.com",
+ "replication": "1",
+ "disable_geoip": "No",
+ "project_api_key": "sdsdd",
+ "events_to_ignore": ""
+ },
+ "error": null,
+ "team_id": 2,
+ "plugin_info": null,
+ "delivery_rate_24h": null,
+ "created_at": "2023-12-22T18:23:03.907137Z",
+ "updated_at": "2023-12-22T18:42:54.074571Z",
+ "name": "Replicator",
+ "description": "Replicate PostHog event stream in another PostHog instance",
+ "deleted": false
+ }
+]
diff --git a/frontend/src/mocks/fixtures/api/organizations/@current/plugins/plugins.json b/frontend/src/mocks/fixtures/api/organizations/@current/plugins/plugins.json
new file mode 100644
index 0000000000000..cade709d2180f
--- /dev/null
+++ b/frontend/src/mocks/fixtures/api/organizations/@current/plugins/plugins.json
@@ -0,0 +1,219 @@
+{
+ "count": 4,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 4,
+ "plugin_type": "custom",
+ "name": "Replicator",
+ "description": "Replicate PostHog event stream in another PostHog instance",
+ "url": "https://github.com/PostHog/posthog-plugin-replicator",
+ "icon": null,
+ "config_schema": [
+ {
+ "key": "host",
+ "hint": "E.g. posthog.yourcompany.com",
+ "name": "Host",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "project_api_key",
+ "hint": "Grab it from e.g. https://posthog.yourcompany.com/project/settings",
+ "name": "Project API Key",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "replication",
+ "hint": "How many times should each event be sent",
+ "name": "Replication",
+ "type": "string",
+ "default": "1",
+ "required": false
+ },
+ {
+ "key": "events_to_ignore",
+ "hint": "Comma-separated list of events to ignore, e.g. $pageleave, purchase",
+ "name": "Events to ignore",
+ "type": "string",
+ "default": "",
+ "required": false
+ },
+ {
+ "key": "disable_geoip",
+ "hint": "Add $disable_geoip so that the receiving PostHog instance doesn't try to resolve the IP address.",
+ "name": "Disable Geo IP?",
+ "type": "choice",
+ "choices": ["Yes", "No"],
+ "default": "No",
+ "required": false
+ }
+ ],
+ "tag": "cec29cd0dea20465839dd301894e4798d6dd6356",
+ "latest_tag": "cec29cd0dea20465839dd301894e4798d6dd6356",
+ "is_global": false,
+ "organization_id": "018aaa96-00d3-0000-b845-8eb60884ff76",
+ "organization_name": "test 123",
+ "capabilities": {},
+ "metrics": {},
+ "public_jobs": {}
+ },
+ {
+ "id": 3,
+ "plugin_type": "custom",
+ "name": "GeoIP",
+ "description": "Enrich PostHog events and persons with IP location data",
+ "url": "https://github.com/PostHog/posthog-plugin-geoip",
+ "icon": null,
+ "config_schema": [],
+ "tag": "2dd67e1dec9c8b5febd7a6d9235c51072950cd37",
+ "latest_tag": "2dd67e1dec9c8b5febd7a6d9235c51072950cd37",
+ "is_global": false,
+ "organization_id": "018aaa96-00d3-0000-b845-8eb60884ff76",
+ "organization_name": "test 123",
+ "capabilities": {},
+ "metrics": {},
+ "public_jobs": {}
+ },
+ {
+ "id": 2,
+ "plugin_type": "custom",
+ "name": "Customer.io",
+ "description": "Send event data and emails into Customer.io.",
+ "url": "https://github.com/PostHog/customerio-plugin",
+ "icon": null,
+ "config_schema": [
+ {
+ "key": "customerioSiteId",
+ "hint": "Provided during Customer.io setup.",
+ "name": "Customer.io Site ID",
+ "type": "string",
+ "secret": true,
+ "default": "",
+ "required": true
+ },
+ {
+ "key": "customerioToken",
+ "hint": "Provided during Customer.io setup.",
+ "name": "Customer.io API Key",
+ "type": "string",
+ "secret": true,
+ "default": "",
+ "required": true
+ },
+ {
+ "key": "host",
+ "hint": "Use the EU variant if your Customer.io account is based in the EU region.",
+ "name": "Tracking Endpoint",
+ "type": "choice",
+ "choices": ["track.customer.io", "track-eu.customer.io"],
+ "default": "track.customer.io"
+ },
+ {
+ "key": "identifyByEmail",
+ "hint": "If enabled, the plugin will identify users by email instead of ID, whenever an email is available.",
+ "name": "Identify by email",
+ "type": "choice",
+ "choices": ["Yes", "No"],
+ "default": "No"
+ },
+ {
+ "key": "sendEventsFromAnonymousUsers",
+ "hint": "Customer.io pricing is based on the number of customers. This is an option to only send events from users that have been identified. Take into consideration that merging after identification won't work (as those previously anonymous events won't be there).",
+ "name": "Filtering of Anonymous Users",
+ "type": "choice",
+ "choices": [
+ "Send all events",
+ "Only send events from users that have been identified",
+ "Only send events from users with emails"
+ ],
+ "default": "Send all events"
+ },
+ {
+ "key": "eventsToSend",
+ "hint": "If this is set, only the specified events (comma-separated) will be sent to Customer.io.",
+ "name": "PostHog Event Allowlist",
+ "type": "string"
+ }
+ ],
+ "tag": "0b86074d53aa11617290f12501b56cfc27c7abde",
+ "latest_tag": "0b86074d53aa11617290f12501b56cfc27c7abde",
+ "is_global": false,
+ "organization_id": "018aaa96-00d3-0000-b845-8eb60884ff76",
+ "organization_name": "test 123",
+ "capabilities": {},
+ "metrics": {},
+ "public_jobs": {}
+ },
+ {
+ "id": 1,
+ "plugin_type": "custom",
+ "name": "BigQuery Export",
+ "description": "Sends events to a BigQuery database on ingestion.",
+ "url": "https://github.com/PostHog/bigquery-plugin",
+ "icon": null,
+ "config_schema": [
+ {
+ "key": "googleCloudKeyJson",
+ "name": "JSON file with your google cloud key",
+ "type": "attachment",
+ "secret": true,
+ "required": true
+ },
+ {
+ "key": "datasetId",
+ "hint": "In case Google Cloud tells you \"my-project-123245:Something\", use \"Something\" as the ID.",
+ "name": "Dataset ID",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "tableId",
+ "hint": "A table will be created if it does not exist.",
+ "name": "Table ID",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "exportEventsToIgnore",
+ "hint": "Comma separated list of events to ignore",
+ "name": "Events to ignore",
+ "type": "string",
+ "default": "$feature_flag_called,$autocapture"
+ },
+ {
+ "key": "exportEventsBufferBytes",
+ "hint": "Default 1MB. Upload events after buffering this many of them. The value must be between 1 MB and 10 MB.",
+ "name": "Maximum upload size in bytes",
+ "type": "string",
+ "default": "1048576"
+ },
+ {
+ "key": "exportEventsBufferSeconds",
+ "hint": "Default 30 seconds. If there are events to upload and this many seconds has passed since the last upload, then upload the queued events. The value must be between 1 and 600 seconds.",
+ "name": "Export events at least every X seconds",
+ "type": "string",
+ "default": "30"
+ },
+ {
+ "key": "exportElementsOnAnyEvent",
+ "hint": "Advanced",
+ "name": "Export the property $elements on events that aren't called `$autocapture`?",
+ "type": "choice",
+ "choices": ["Yes", "No"],
+ "default": "No"
+ }
+ ],
+ "tag": "5f5dcbc2f6a36ea7e9700ab36cc9397d92742ca3",
+ "latest_tag": "5f5dcbc2f6a36ea7e9700ab36cc9397d92742ca3",
+ "is_global": false,
+ "organization_id": "018aaa96-00d3-0000-b845-8eb60884ff76",
+ "organization_name": "test 123",
+ "capabilities": {},
+ "metrics": {},
+ "public_jobs": {}
+ }
+ ]
+}
diff --git a/frontend/src/scenes/billing/Billing.stories.tsx b/frontend/src/scenes/billing/Billing.stories.tsx
index ab6cbae8ea895..dc003872c4df9 100644
--- a/frontend/src/scenes/billing/Billing.stories.tsx
+++ b/frontend/src/scenes/billing/Billing.stories.tsx
@@ -4,8 +4,14 @@ import { mswDecorator, useStorybookMocks } from '~/mocks/browser'
import billingJson from '~/mocks/fixtures/_billing_v2.json'
import billingJsonWithDiscount from '~/mocks/fixtures/_billing_v2_with_discount.json'
import preflightJson from '~/mocks/fixtures/_preflight.json'
+import organizationCurrent from '~/mocks/fixtures/api/organizations/@current/@current.json'
+import batchExports from '~/mocks/fixtures/api/organizations/@current/batchExports.json'
+import exportsUnsubscribeConfigs from '~/mocks/fixtures/api/organizations/@current/plugins/exportsUnsubscribeConfigs.json'
+import organizationPlugins from '~/mocks/fixtures/api/organizations/@current/plugins/plugins.json'
+import { BillingProductV2Type } from '~/types'
import { Billing } from './Billing'
+import { UnsubscribeSurveyModal } from './UnsubscribeSurveyModal'
const meta: Meta = {
title: 'Scenes-Other/Billing v2',
@@ -13,6 +19,7 @@ const meta: Meta = {
layout: 'fullscreen',
viewMode: 'story',
mockDate: '2023-05-25',
+ testOptions: { waitForSelector: '.LemonTable__content' },
},
decorators: [
mswDecorator({
@@ -50,3 +57,56 @@ export const BillingV2WithDiscount = (): JSX.Element => {
return
}
+
+export const BillingUnsubscribeModal = (): JSX.Element => {
+ useStorybookMocks({
+ get: {
+ '/api/billing-v2/': {
+ ...billingJson,
+ },
+ },
+ })
+
+ return
+}
+export const BillingUnsubscribeModal_DataPipelines = (): JSX.Element => {
+ useStorybookMocks({
+ get: {
+ '/api/billing-v2/': {
+ ...billingJson,
+ },
+ '/api/organizations/@current/plugins/exports_unsubscribe_configs/': exportsUnsubscribeConfigs,
+ '/api/organizations/@current/batch_exports': batchExports,
+ '/api/organizations/@current/plugins': {
+ ...organizationPlugins,
+ },
+ '/api/organizations/@current/': {
+ ...organizationCurrent,
+ },
+ },
+ })
+ const product = billingJson.products[0] as BillingProductV2Type
+ product.addons = [
+ {
+ type: 'data_pipelines',
+ subscribed: true,
+ name: 'Data Pipelines',
+ description: 'Add-on description',
+ price_description: 'Add-on price description',
+ image_url: 'Add-on image URL',
+ docs_url: 'Add-on documentation URL',
+ tiers: [],
+ tiered: false,
+ unit: '',
+ unit_amount_usd: '0',
+ current_amount_usd: '0',
+ current_usage: 0,
+ projected_usage: 0,
+ projected_amount_usd: '0',
+ plans: [],
+ usage_key: '',
+ },
+ ]
+
+ return
+}
diff --git a/frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx b/frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
deleted file mode 100644
index 6e72bd0488d48..0000000000000
--- a/frontend/src/scenes/billing/ExportsUnsubscribeTable/ExportsUnsubscribeTable.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Meta, StoryFn } from '@storybook/react'
-import { useActions } from 'kea'
-import { useEffect } from 'react'
-
-import { ExportsUnsubscribeTable } from './ExportsUnsubscribeTable'
-import { exportsUnsubscribeTableLogic } from './exportsUnsubscribeTableLogic'
-
-const meta: Meta = {
- title: 'Components/Exports Unsubscribe Table',
-}
-export default meta
-
-export const _ExportsUnsubscribeTable: StoryFn = () => {
- const { openModal } = useActions(exportsUnsubscribeTableLogic)
-
- useEffect(() => {
- openModal()
- })
-
- return
-}
From 0354840ef345891360ac8b7e406cc29da3efc104 Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 14:13:24 -0800
Subject: [PATCH 16/33] fix
---
frontend/src/scenes/billing/Billing.stories.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/scenes/billing/Billing.stories.tsx b/frontend/src/scenes/billing/Billing.stories.tsx
index dc003872c4df9..86634b18679c4 100644
--- a/frontend/src/scenes/billing/Billing.stories.tsx
+++ b/frontend/src/scenes/billing/Billing.stories.tsx
@@ -19,7 +19,6 @@ const meta: Meta = {
layout: 'fullscreen',
viewMode: 'story',
mockDate: '2023-05-25',
- testOptions: { waitForSelector: '.LemonTable__content' },
},
decorators: [
mswDecorator({
@@ -110,3 +109,6 @@ export const BillingUnsubscribeModal_DataPipelines = (): JSX.Element => {
return
}
+BillingUnsubscribeModal_DataPipelines.parameters = {
+ testOptions: { waitForSelector: '.LemonTable__content' },
+}
From 001db20c0ad5e4b0b785546317fe1870fc28a6ee Mon Sep 17 00:00:00 2001
From: Raquel Smith
Date: Fri, 22 Dec 2023 14:38:47 -0800
Subject: [PATCH 17/33] make images smaller
---
.../exportsUnsubscribeTableLogic.tsx | 4 ++--
frontend/src/scenes/pipeline/utils.tsx | 9 +++++----
frontend/src/scenes/plugins/plugin/PluginImage.tsx | 6 ++++--
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx b/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
index ea5ccad6cbf55..300680e9ac8d1 100644
--- a/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
+++ b/frontend/src/scenes/billing/ExportsUnsubscribeTable/exportsUnsubscribeTableLogic.tsx
@@ -95,7 +95,7 @@ export const exportsUnsubscribeTableLogic = kea,
+ icon: ,
disabled: !pluginConfig.enabled,
} as ItemToDisable
})
@@ -108,7 +108,7 @@ export const exportsUnsubscribeTableLogic = kea
),
diff --git a/frontend/src/scenes/pipeline/utils.tsx b/frontend/src/scenes/pipeline/utils.tsx
index 2e71867f523b0..19908b8b735b3 100644
--- a/frontend/src/scenes/pipeline/utils.tsx
+++ b/frontend/src/scenes/pipeline/utils.tsx
@@ -2,7 +2,7 @@ import api from 'lib/api'
import { Link } from 'lib/lemon-ui/Link'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import posthog from 'posthog-js'
-import { PluginImage } from 'scenes/plugins/plugin/PluginImage'
+import { PluginImage, PluginImageSize } from 'scenes/plugins/plugin/PluginImage'
import { PluginConfigTypeNew, PluginType } from '~/types'
@@ -34,9 +34,10 @@ export async function loadPaginatedResults(
type RenderAppProps = {
plugin: PluginType
+ imageSize?: PluginImageSize
}
-export function RenderApp({ plugin }: RenderAppProps): JSX.Element {
+export function RenderApp({ plugin, imageSize }: RenderAppProps): JSX.Element {
return (
{plugin.url ? (
-
+
) : (
- // TODO: tooltip doesn't work on this
+ // TODO: tooltip doesn't work on this
)}
diff --git a/frontend/src/scenes/plugins/plugin/PluginImage.tsx b/frontend/src/scenes/plugins/plugin/PluginImage.tsx
index b7401e2e6561d..a0081e46c9ec1 100644
--- a/frontend/src/scenes/plugins/plugin/PluginImage.tsx
+++ b/frontend/src/scenes/plugins/plugin/PluginImage.tsx
@@ -5,18 +5,20 @@ import { useEffect, useState } from 'react'
import { PluginType } from '~/types'
+export type PluginImageSize = 'medium' | 'large' | 'small'
+
export function PluginImage({
plugin,
size = 'medium',
}: {
plugin: Partial>
- size?: 'medium' | 'large' | 'small'
+ size?: PluginImageSize
}): JSX.Element {
const { plugin_type: pluginType, url, icon } = plugin
const [state, setState] = useState({ image: imgPluginDefault })
const pixelSize = {
- medium: 60,
large: 100,
+ medium: 60,
small: 30,
}[size]
From c92b09dee01d75d6746de52ea739490bde08fec6 Mon Sep 17 00:00:00 2001
From: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 22 Dec 2023 22:21:01 +0000
Subject: [PATCH 18/33] Update UI snapshots for `chromium` (1)
---
...ling-v2--billing-unsubscribe-modal--dark.png | Bin 0 -> 24965 bytes
...ing-v2--billing-unsubscribe-modal--light.png | Bin 0 -> 25385 bytes
...g-unsubscribe-modal-data-pipelines--dark.png | Bin 0 -> 57417 bytes
...-unsubscribe-modal-data-pipelines--light.png | Bin 0 -> 57524 bytes
...billing-unsubscribe-modal-data-pipelines.png | Bin 0 -> 55429 bytes
...er-billing-v2--billing-unsubscribe-modal.png | Bin 0 -> 24843 bytes
6 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--light.png
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--dark.png
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines--light.png
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal-data-pipelines.png
create mode 100644 frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal.png
diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-unsubscribe-modal--dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ee60a6dff9647791168418795e11072eba0da78
GIT binary patch
literal 24965
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