Skip to content

Commit

Permalink
batch exports finished + modal access listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
tiina303 committed Dec 14, 2023
1 parent d1dfc83 commit 4d70e47
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 53 deletions.
5 changes: 0 additions & 5 deletions frontend/src/scenes/billing/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { capitalizeFirstLetter } from 'lib/utils'
import { useEffect } from 'react'
import { ExportsUnsubscribeModal } from 'scenes/pipeline/ExportsUnsubscribeModal'
import { exportsUnsubscribeModalLogic } from 'scenes/pipeline/ExportsUnsubscribeModal/exportsUnsubscribeModalLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { SceneExport } from 'scenes/sceneTypes'

Expand Down Expand Up @@ -45,7 +43,6 @@ export function Billing(): JSX.Element {
const { preflight } = useValues(preflightLogic)
const cloudOrDev = preflight?.cloud || preflight?.is_debug
const { openSupportForm } = useActions(supportLogic)
const { openModal } = useActions(exportsUnsubscribeModalLogic)

useEffect(() => {
if (billing) {
Expand Down Expand Up @@ -130,7 +127,6 @@ export function Billing(): JSX.Element {
<div ref={ref}>
{!isOnboarding && (
<div className="flex justify-between">
<ExportsUnsubscribeModal />
<BillingPageHeader />
{billing?.has_active_subscription && (
<div>
Expand All @@ -143,7 +139,6 @@ export function Billing(): JSX.Element {
>
Manage card details
</LemonButton>
<LemonButton onClick={openModal}>Unsubscribe from batch exports</LemonButton>
</div>
)}
</div>
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/scenes/pipeline/AppsManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export function AppsManagement(): JSX.Element {
localPlugins,
} = useValues(appsManagementLogic)
const { isDev, isCloudOrDev } = useValues(preflightLogic)
const { openModal } = useActions(exportsUnsubscribeModalLogic)
const { startUnsubscribe } = useActions(exportsUnsubscribeModalLogic)
const { loading } = useValues(exportsUnsubscribeModalLogic)

if (!canInstallPlugins || !canGloballyManagePlugins) {
return <>You don't have permission to manage apps.</>
Expand All @@ -40,7 +41,9 @@ export function AppsManagement(): JSX.Element {
return (
<div className="pipeline-apps-management-scene">
<ExportsUnsubscribeModal />
<LemonButton onClick={openModal}>Unsubscribe from batch exports</LemonButton>
<LemonButton loading={loading} onClick={startUnsubscribe}>
Unsubscribe from batch exports
</LemonButton>
{isCloudOrDev &&
(missingGlobalPlugins.length > 0 ||
shouldBeGlobalPlugins.length > 0 ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ 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 { pluginsLogic } from 'scenes/plugins/pluginsLogic'

import { RenderApp } from '../utils'
import { exportsUnsubscribeModalLogic } from './exportsUnsubscribeModalLogic'

export function ExportsUnsubscribeModal(): JSX.Element {
const { plugins } = useValues(pluginsLogic)
const { modalOpen, unsubscribeDisabledReason, loading, pluginConfigsToDisable } =
useValues(exportsUnsubscribeModalLogic)
const { closeModal, disablePlugin } = useActions(exportsUnsubscribeModalLogic)
const { modalOpen, unsubscribeDisabledReason, loading, itemsToDisable } = useValues(exportsUnsubscribeModalLogic)
const { closeModal, disablePlugin, pauseBatchExport, completeUnsubscribe } =
useActions(exportsUnsubscribeModalLogic)
const { currentOrganization } = useValues(organizationLogic)

if (!currentOrganization) {
Expand All @@ -35,7 +32,7 @@ export function ExportsUnsubscribeModal(): JSX.Element {
<LemonButton
loading={loading}
type="primary"
onClick={() => {}} // TODO: replace with the real unsubscribe callback
onClick={completeUnsubscribe}
disabledReason={unsubscribeDisabledReason}
>
Unsubscribe
Expand All @@ -44,30 +41,30 @@ export function ExportsUnsubscribeModal(): JSX.Element {
}
>
<LemonTable
dataSource={Object.values(pluginConfigsToDisable)}
dataSource={itemsToDisable}
size="xs"
loading={loading}
columns={[
{
title: 'Team',
render: function RenderTeam(_, pluginConfig) {
return currentOrganization.teams.find((team) => team.id === pluginConfig.team_id)?.name
render: function RenderTeam(_, item) {
return currentOrganization.teams.find((team) => team.id === item.team_id)?.name
},
},
{
render: function RenderAppInfo(_, pluginConfig) {
return <RenderApp plugin={plugins[pluginConfig.plugin]} />
render: function RenderAppInfo(_, item) {
return item.icon
},
},
{
title: 'Name',
render: function RenderPluginName(_, pluginConfig) {
render: function RenderPluginName(_, item) {
return (
<>
<span className="row-name">{pluginConfig.name}</span>
{pluginConfig.description && (
<span className="row-name">{item.name}</span>
{item.description && (
<LemonMarkdown className="row-description" lowKeyHeadings>
{pluginConfig.description}
{item.description}
</LemonMarkdown>
)}
</>
Expand All @@ -76,12 +73,18 @@ export function ExportsUnsubscribeModal(): JSX.Element {
},
{
title: '',
render: function RenderPluginDisable(_, pluginConfig) {
render: function RenderPluginDisable(_, item) {
return (
<LemonButton
type="secondary"
onClick={() => disablePlugin(pluginConfig.id)}
disabledReason={pluginConfig.enabled ? null : 'Already disabled'}
onClick={() => {
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
</LemonButton>
Expand All @@ -90,7 +93,6 @@ export function ExportsUnsubscribeModal(): JSX.Element {
},
]}
/>
{/* "Show a table with team(project), plugin/export and disable" */}
</LemonModal>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { actions, afterMount, connect, kea, path, reducers, selectors } from 'kea'
import { lemonToast } from '@posthog/lemon-ui'
import { actions, afterMount, connect, kea, listeners, path, reducers, 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 '../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
batch_export_id: string | undefined
team_id: number
name: string
description: string | undefined
icon: JSX.Element
disabled: boolean
}

export const exportsUnsubscribeModalLogic = kea<exportsUnsubscribeModalLogicType>([
path(['scenes', 'pipeline', 'exportsUnsubscribeModalLogic']),
connect({
Expand All @@ -19,6 +32,9 @@ export const exportsUnsubscribeModalLogic = kea<exportsUnsubscribeModalLogicType
openModal: true,
closeModal: true,
disablePlugin: (id: number) => ({ id }),
pauseBatchExport: (id: string) => ({ id }),
startUnsubscribe: true,
completeUnsubscribe: true,
}),
loaders(({ values }) => ({
pluginConfigsToDisable: [
Expand All @@ -34,24 +50,25 @@ export const exportsUnsubscribeModalLogic = kea<exportsUnsubscribeModalLogicType
if (!values.canConfigurePlugins) {
return values.pluginConfigsToDisable
}
// const { pluginConfigsToDisable, plugins } = values
// const pluginConfig = pluginConfigs[id]
// const plugin = plugins[pluginConfig.plugin]
// capturePluginEvent(`plugin ${enabled ? 'enabled' : 'disabled'}`, plugin, pluginConfig)
// Update order if enabling to be at the end of current enabled plugins
// See comment in savePluginConfigsOrder about races
const response = await api.update(`api/plugin_config/${id}`, { enabled: false })
return { ...values.pluginConfigsToDisable, [id]: response }
},
},
],
// todo batch exports api.get with the path
batchExportConfigs: [
{} as Record<BatchExportConfiguration['id'], BatchExportConfiguration>,
{
loadBatchExportConfigs: async () => {
const res = await api.get<BatchExportConfiguration[]>(`api/organizations/@current/batch_exports`)
return Object.fromEntries(res.map((batchExportConfig) => [batchExportConfig.id, batchExportConfig]))
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 } }
},
},
],
Expand All @@ -63,17 +80,49 @@ export const exportsUnsubscribeModalLogic = kea<exportsUnsubscribeModalLogicType
],
unsubscribeDisabledReason: [
(s) => [s.loading, s.pluginConfigsToDisable, s.batchExportConfigs],
(loading, pluginConfigsToDisable, batchExports) => {
(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 need to be disabled explicitly first'
: batchExports
: Object.values(batchExportConfigs).some((batchExportConfig) => !batchExportConfig.paused)
? 'All batch exports need to be deleted 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: <RenderApp plugin={plugins[pluginConfig.plugin]} />,
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: (
<IconDatabase
style={{
fontSize: 60,
}}
/>
),
disabled: batchExportConfig.paused,
} as ItemToDisable
})
return [...pluginConfigs, ...batchExports]
},
],
}),
reducers({
modalOpen: [
Expand All @@ -84,7 +133,27 @@ export const exportsUnsubscribeModalLogic = kea<exportsUnsubscribeModalLogicType
},
],
}),
// TODO: add a listener in the billing page to load plugins and open modal or go directly
listeners(({ actions, values }) => ({
// Usage guide:
// const { startUnsubscribe } = useActions(exportsUnsubscribeModalLogic)
// const { loading } = useValues(exportsUnsubscribeModalLogic)
// return (<>
// <ExportsUnsubscribeModal />
// <LemonButton loading={loading} onClick={startUnsubscribe}>Unsubscribe from data pipelines</LemonButton>
// </>)
startUnsubscribe() {
if (values.loading || values.unsubscribeDisabledReason) {
actions.openModal()
} else {
actions.completeUnsubscribe()
}
},
completeUnsubscribe() {
actions.closeModal()
lemonToast.success('Successfully unsubscribed from all data pipelines')
// TODO: whatever needs to happen for the actual unsubscription
},
})),
afterMount(({ actions }) => {
actions.loadPluginConfigs()
actions.loadBatchExportConfigs()
Expand Down
19 changes: 6 additions & 13 deletions posthog/batch_exports/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,10 @@ class BatchExportViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
serializer_class = BatchExportSerializer

def get_queryset(self):
if not isinstance(self.request.user, User) or self.request.user.current_team is None:
if not isinstance(self.request.user, User):
raise NotAuthenticated()

return (
self.queryset.filter(team_id=self.team_id)
.exclude(deleted=True)
.order_by("-created_at")
.prefetch_related("destination")
)
return super().get_queryset().exclude(deleted=True).order_by("-created_at").prefetch_related("destination")

@action(methods=["POST"], detail=True)
def backfill(self, request: request.Request, *args, **kwargs) -> response.Response:
Expand Down Expand Up @@ -279,14 +274,14 @@ def backfill(self, request: request.Request, *args, **kwargs) -> response.Respon
@action(methods=["POST"], detail=True)
def pause(self, request: request.Request, *args, **kwargs) -> response.Response:
"""Pause a BatchExport."""
if not isinstance(request.user, User) or request.user.current_team is None:
if not isinstance(request.user, User):
raise NotAuthenticated()

batch_export = self.get_object()
user_id = request.user.distinct_id
team_id = request.user.current_team.id
team_id = batch_export.team_id
note = f"Pause requested by user {user_id} from team {team_id}"

batch_export = self.get_object()
temporal = sync_connect()

try:
Expand Down Expand Up @@ -349,10 +344,8 @@ def perform_destroy(self, instance: BatchExport):
cancel_running_batch_export_backfill(temporal, backfill.workflow_id)


class BatchExportOrganizationViewSet(StructuredViewSetMixin, viewsets.ModelViewSet):
class BatchExportOrganizationViewSet(BatchExportViewSet):
permission_classes = [IsAuthenticated, OrganizationMemberPermissions]
serializer_class = BatchExportSerializer
queryset = BatchExport.objects.select_related("team")
filter_rewrite_rules = {"organization_id": "team__organization_id"}


Expand Down

0 comments on commit 4d70e47

Please sign in to comment.