Files
zeekay bc4ddf332b chore: rebase the tree onto upstream master fa1a9a60
Mechanically re-derived: bin/debrand runs over a fresh import of
posthog/posthog@fa1a9a60d4 (2026-08-06).
The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker*
entrypoints, compose files, LLM.md and the squashed initial migration
are restored from the shipped tree, same as the previous import.

The prune's import-closure guard kept 21 products the new core now
imports; dropping them is a refactor now, recorded rather than forced.
2026-08-06 01:25:36 -07:00

655 lines
24 KiB
TypeScript

import { MakeLogicType, actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { router, urlToAction } from 'kea-router'
import { Sorting } from '@hanzo/elements'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic, type FeatureFlagsSet } from 'lib/logic/featureFlagLogic'
import { trackedActionToUrl } from 'lib/logic/scenes/trackedActionToUrl'
import { getCurrentTeamId } from 'lib/utils/getAppContext'
import { organizationLogic } from 'scenes/organizationLogic'
import { sceneConfigurations } from 'scenes/scenes'
import { Scene } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'
import { Breadcrumb } from '~/types'
import { subscriptionsList, subscriptionsTestDeliveryCreate } from 'products/subscriptions/frontend/generated/api'
import {
SubscriptionsListResourceType,
TargetTypeEnumApi,
type PaginatedSubscriptionListApi,
type SubscriptionsListTargetType,
} from 'products/subscriptions/frontend/generated/api.schemas'
import type { OrganizationType, UserType } from '../../../../frontend/src/types'
import { runSubscriptionTestDelivery } from '../components/Subscriptions/runSubscriptionTestDelivery'
import { toggleSubscriptionEnabled } from '../components/Subscriptions/toggleSubscriptionEnabled'
import type { SubscriptionApi } from '../generated/api.schemas'
const PAGE_SIZE = 20
export enum SubscriptionsTab {
All = 'all',
Mine = 'mine',
Dashboard = 'dashboard',
Insight = 'insight',
AI = 'ai_prompt',
}
/** Return type is full `SubscriptionsTab` so Kea typegen does not collapse state to the `All` literal. */
function getInitialSubscriptionsTab(): SubscriptionsTab {
return SubscriptionsTab.All
}
/** Query keys owned by the subscriptions list scene (merged into the router; other params are preserved). */
const SUBSCRIPTIONS_URL_KEYS = ['tab', 'search', 'created_by', 'target_type', 'page'] as const
/** Router may coerce numeric-looking query values; keep text fields as strings. */
function urlSearchParamToString(value: unknown): string {
return `${value ?? ''}`
}
export interface SubscriptionsQueryFromUrl {
tab: SubscriptionsTab
search: string
createdByUuid: string | null
targetTypeFilter: SubscriptionsListTargetType | null
page: number
}
function parseSubscriptionsSearchParams(searchParams: Record<string, unknown>): SubscriptionsQueryFromUrl {
const rawTab = searchParams['tab']
const tab: SubscriptionsTab = (Object.values(SubscriptionsTab) as string[]).includes(rawTab as string)
? (rawTab as SubscriptionsTab)
: SubscriptionsTab.All
const search = urlSearchParamToString(searchParams['search'])
let createdByUuid: string | null =
typeof searchParams['created_by'] === 'string' ? searchParams['created_by'] : null
if (tab === SubscriptionsTab.Mine) {
createdByUuid = null
}
const ttRaw = searchParams['target_type']
const targetTypeFilter: SubscriptionsListTargetType | null =
ttRaw === TargetTypeEnumApi.Email || ttRaw === TargetTypeEnumApi.Slack ? ttRaw : null
let page = 1
const pageRaw = searchParams['page']
if (pageRaw !== undefined && pageRaw !== null && `${pageRaw}` !== '') {
const n = parseInt(String(pageRaw), 10)
if (Number.isFinite(n) && n >= 1) {
page = n
}
}
return { tab, search, createdByUuid, targetTypeFilter, page }
}
function buildSubscriptionUrlParams(values: {
currentTab: SubscriptionsTab
search: string
createdByUuid: string | null
targetTypeFilter: SubscriptionsListTargetType | null
page: number
}): Record<string, string> {
const out: Record<string, string> = {}
if (values.currentTab !== SubscriptionsTab.All) {
out.tab = values.currentTab
}
const q = values.search.trim()
if (q) {
out.search = q
}
if (values.currentTab !== SubscriptionsTab.Mine && values.createdByUuid) {
out.created_by = values.createdByUuid
}
if (values.targetTypeFilter) {
out.target_type = values.targetTypeFilter
}
if (values.page > 1) {
out.page = String(values.page)
}
return out
}
function mergeSubscriptionRouterSearchParams(
current: Record<string, any>,
values: {
currentTab: SubscriptionsTab
search: string
createdByUuid: string | null
targetTypeFilter: SubscriptionsListTargetType | null
page: number
}
): Record<string, any> {
const next = { ...current }
for (const k of SUBSCRIPTIONS_URL_KEYS) {
delete next[k]
}
Object.assign(next, buildSubscriptionUrlParams(values))
return next
}
function subscriptionRouterSearchParamsEqual(
current: Record<string, any>,
values: {
currentTab: SubscriptionsTab
search: string
createdByUuid: string | null
targetTypeFilter: SubscriptionsListTargetType | null
page: number
}
): boolean {
const desired = buildSubscriptionUrlParams(values)
for (const k of SUBSCRIPTIONS_URL_KEYS) {
const cur = current[k] != null && current[k] !== '' ? String(current[k]) : ''
const des = desired[k] ?? ''
if (cur !== des) {
return false
}
}
return true
}
function subscriptionsListStateEqual(
parsed: SubscriptionsQueryFromUrl,
values: {
currentTab: SubscriptionsTab
search: string
createdByUuid: string | null
targetTypeFilter: SubscriptionsListTargetType | null
page: number
}
): boolean {
return (
parsed.tab === values.currentTab &&
parsed.search === values.search &&
(parsed.createdByUuid ?? '') === (values.createdByUuid ?? '') &&
(parsed.targetTypeFilter ?? null) === (values.targetTypeFilter ?? null) &&
parsed.page === values.page
)
}
function omitKey(state: Record<number, true>, id: number): Record<number, true> {
const next = { ...state }
delete next[id]
return next
}
/** Table columnKey → DRF `ordering` field (see SubscriptionViewSet.ordering_fields). */
function buildSubscriptionsListOrdering(sorting: Sorting | null): string {
if (!sorting) {
return '-created_at'
}
const fieldByColumnKey: Record<string, string> = {
next_delivery_date: 'next_delivery_date',
created_at: 'created_at',
created_by: 'created_by__email',
name: 'title',
}
const field = fieldByColumnKey[sorting.columnKey]
if (!field) {
return '-created_at'
}
const dir = sorting.order === -1 ? '-' : ''
return `${dir}${field}`
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionsSceneLogicValues {
featureFlags: FeatureFlagsSet // featureFlagLogic
receivedFeatureFlags: boolean // featureFlagLogic
currentOrganization: OrganizationType | null // organizationLogic
user: UserType | null // userLogic
aiSubscriptionsAvailable: boolean
breadcrumbs: Breadcrumb[]
createdByUuid: string | null
currentTab: SubscriptionsTab
deliveringSubscriptionId: number | null
page: number
pagination: {
controlled: true
currentPage: number
entryCount: number
onBackward: () => void
onForward: () => void
pageSize: number
}
search: string
subscriptionModalId: number | 'new' | null
subscriptions: SubscriptionApi[]
subscriptionsListAwaitingDebouncedFetch: boolean
subscriptionsLoading: boolean
subscriptionsResponse: PaginatedSubscriptionListApi | null
subscriptionsResponseLoading: boolean
subscriptionsSorting: Sorting | null
targetTypeFilter: SubscriptionsListTargetType | null
togglingEnabledIds: Record<number, true>
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionsSceneLogicActions {
applySubscriptionsQueryFromUrl: (query: SubscriptionsQueryFromUrl) => {
query: SubscriptionsQueryFromUrl
}
deleteSubscriptionSuccess: () => {
value: true
}
deliverSubscription: (id: number) => {
id: number
}
deliverSubscriptionFailure: () => {
value: true
}
deliverSubscriptionSuccess: () => {
value: true
}
loadSubscriptions: () => {
value: true
}
loadSubscriptionsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadSubscriptionsSuccess: (
subscriptionsResponse: PaginatedSubscriptionListApi,
payload?: {
value: true
}
) => {
subscriptionsResponse: PaginatedSubscriptionListApi
payload?: {
value: true
}
}
setCreatedByFilter: (uuid: string | null) => {
uuid: string | null
}
setCurrentTab: (tab: SubscriptionsTab) => {
tab: SubscriptionsTab
}
setPage: (page: number) => {
page: number
}
setSearch: (search: string) => {
search: string
}
setSubscriptionEnabled: (
id: number,
enabled: boolean
) => {
enabled: boolean
id: number
}
setSubscriptionEnabledFailure: (id: number) => {
id: number
}
setSubscriptionEnabledSuccess: (id: number) => {
id: number
}
setSubscriptionModalId: (id: number | 'new' | null) => {
id: number | 'new' | null
}
setSubscriptionsSorting: (sorting: Sorting | null) => {
sorting: Sorting | null
}
setTargetTypeFilter: (targetType: SubscriptionsListTargetType | null) => {
targetType: SubscriptionsListTargetType | null
}
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionsSceneLogicMeta {
__keaTypeGenInternalSelectorTypes: {
aiSubscriptionsAvailable: (
featureFlags: FeatureFlagsSet,
receivedFeatureFlags: boolean,
currentOrganization: OrganizationType | null
) => boolean
subscriptions: (subscriptionsResponse: PaginatedSubscriptionListApi | null) => SubscriptionApi[]
subscriptionsLoading: (subscriptionsResponseLoading: boolean) => boolean
pagination: (
page: number,
subscriptionsResponse: PaginatedSubscriptionListApi | null
) => {
controlled: true
currentPage: number
entryCount: number
onBackward: () => void
onForward: () => void
pageSize: number
}
}
}
export type subscriptionsSceneLogicType = MakeLogicType<
subscriptionsSceneLogicValues,
subscriptionsSceneLogicActions,
Record<string, any>,
subscriptionsSceneLogicMeta
>
export const subscriptionsSceneLogic = kea<subscriptionsSceneLogicType>([
path(['scenes', 'subscriptions', 'subscriptionsSceneLogic']),
connect(() => ({
values: [
userLogic,
['user'],
featureFlagLogic,
['featureFlags', 'receivedFeatureFlags'],
organizationLogic,
['currentOrganization'],
],
})),
actions({
loadSubscriptions: true,
setSearch: (search: string) => ({ search }),
setPage: (page: number) => ({ page }),
setCreatedByFilter: (uuid: string | null) => ({ uuid }),
setCurrentTab: (tab: SubscriptionsTab) => ({ tab }),
setSubscriptionsSorting: (sorting: Sorting | null) => ({ sorting }),
setTargetTypeFilter: (targetType: SubscriptionsListTargetType | null) => ({ targetType }),
applySubscriptionsQueryFromUrl: (query: SubscriptionsQueryFromUrl) => ({ query }),
setSubscriptionModalId: (id: number | 'new' | null) => ({ id }),
deleteSubscriptionSuccess: true,
deliverSubscription: (id: number) => ({ id }),
deliverSubscriptionSuccess: true,
deliverSubscriptionFailure: true,
setSubscriptionEnabled: (id: number, enabled: boolean) => ({ id, enabled }),
setSubscriptionEnabledSuccess: (id: number) => ({ id }),
setSubscriptionEnabledFailure: (id: number) => ({ id }),
}),
reducers({
search: [
'',
{
setSearch: (_, { search }) => search,
applySubscriptionsQueryFromUrl: (_, { query }) => query.search,
},
],
createdByUuid: [
null as string | null,
{
setCreatedByFilter: (_, { uuid }) => uuid,
setCurrentTab: (state, { tab }) => (tab === SubscriptionsTab.Mine ? null : state),
applySubscriptionsQueryFromUrl: (_, { query }) =>
query.tab === SubscriptionsTab.Mine ? null : query.createdByUuid,
},
],
currentTab: [
getInitialSubscriptionsTab(),
{
setCurrentTab: (_, { tab }) => tab,
applySubscriptionsQueryFromUrl: (_, { query }) => query.tab,
},
],
page: [
1,
{
setPage: (_, { page }) => page,
setSearch: () => 1,
setCreatedByFilter: () => 1,
setCurrentTab: () => 1,
setSubscriptionsSorting: () => 1,
setTargetTypeFilter: () => 1,
deleteSubscriptionSuccess: () => 1,
applySubscriptionsQueryFromUrl: (_, { query }) => query.page,
},
],
subscriptionsSorting: [
null as Sorting | null,
{
setSubscriptionsSorting: (_, { sorting }) => sorting,
},
],
targetTypeFilter: [
null as SubscriptionsListTargetType | null,
{
setTargetTypeFilter: (_, { targetType }) => targetType,
applySubscriptionsQueryFromUrl: (_, { query }) => query.targetTypeFilter,
},
],
// Drives the create/edit modal that now renders OVER the list (route-driven), so the
// list stays painted behind it instead of swapping to a separate scene. `null` = closed.
subscriptionModalId: [
null as number | 'new' | null,
{
setSubscriptionModalId: (_, { id }) => id,
},
],
/**
* True after `setSearch` until the debounced list request finishes. Avoids treating stale
* empty `subscriptionsResponse` as "no subscriptions in project" when filters were cleared.
*/
subscriptionsListAwaitingDebouncedFetch: [
false,
{
setSearch: () => true,
loadSubscriptionsSuccess: () => false,
loadSubscriptionsFailure: () => false,
},
],
deliveringSubscriptionId: [
null as number | null,
{
deliverSubscription: (_, { id }) => id,
deliverSubscriptionSuccess: () => null,
deliverSubscriptionFailure: () => null,
},
],
// Tracks the subscriptions whose Pause/Resume PATCH is in flight so each row's
// switch can show its own busy state. A set (not a single id) so concurrent toggles
// on different rows don't clobber each other.
togglingEnabledIds: [
{} as Record<number, true>,
{
setSubscriptionEnabled: (state, { id }) => ({ ...state, [id]: true }),
setSubscriptionEnabledSuccess: (state, { id }) => omitKey(state, id),
setSubscriptionEnabledFailure: (state, { id }) => omitKey(state, id),
},
],
}),
loaders(({ values }) => ({
subscriptionsResponse: [
null as PaginatedSubscriptionListApi | null,
{
loadSubscriptions: async () => {
const projectId = String(getCurrentTeamId())
let resourceType: SubscriptionsListResourceType | undefined
if (values.currentTab === SubscriptionsTab.Dashboard) {
resourceType = SubscriptionsListResourceType.Dashboard
} else if (values.currentTab === SubscriptionsTab.Insight) {
resourceType = SubscriptionsListResourceType.Insight
} else if (values.currentTab === SubscriptionsTab.AI) {
resourceType = SubscriptionsListResourceType.AiPrompt
}
const createdBy =
values.currentTab === SubscriptionsTab.Mine
? values.user?.uuid
: values.createdByUuid || undefined
const ordering = buildSubscriptionsListOrdering(values.subscriptionsSorting)
return await subscriptionsList(projectId, {
limit: PAGE_SIZE,
offset: (values.page - 1) * PAGE_SIZE,
search: values.search.trim() || undefined,
ordering,
created_by: createdBy,
resource_type: resourceType,
target_type: values.targetTypeFilter ?? undefined,
})
},
},
],
})),
selectors({
aiSubscriptionsAvailable: [
(s) => [s.featureFlags, s.receivedFeatureFlags, s.currentOrganization],
(
featureFlags: FeatureFlagsSet,
receivedFeatureFlags: boolean,
currentOrganization: OrganizationType | null
) =>
receivedFeatureFlags &&
Boolean(featureFlags[FEATURE_FLAGS.SUBSCRIPTION_AI_PROMPT]) &&
Boolean(currentOrganization?.is_ai_data_processing_approved),
],
subscriptions: [
(s) => [s.subscriptionsResponse],
(subscriptionsResponse: PaginatedSubscriptionListApi | null) => subscriptionsResponse?.results ?? [],
],
subscriptionsLoading: [
(s) => [s.subscriptionsResponseLoading],
(subscriptionsResponseLoading: boolean) => Boolean(subscriptionsResponseLoading),
],
breadcrumbs: [
() => [],
(): Breadcrumb[] => [
{
key: Scene.Subscriptions,
name: sceneConfigurations[Scene.Subscriptions].name,
iconType: sceneConfigurations[Scene.Subscriptions].iconType || 'default_icon_type',
},
],
],
}),
selectors(({ actions }) => ({
pagination: [
(s) => [s.page, s.subscriptionsResponse],
(page: number, subscriptionsResponse: PaginatedSubscriptionListApi | null) => {
const count = subscriptionsResponse?.count ?? 0
// usePagination uses `entryCount || null`; 0 is treated as missing and breaks pageCount / next button.
// Table shows "No …" when the page is empty, so inflating 0→1 only satisfies the hook.
const entryCount = Math.max(count, 1)
return {
controlled: true,
pageSize: PAGE_SIZE,
currentPage: page,
entryCount,
onBackward: () => actions.setPage(page - 1),
onForward: () => actions.setPage(page + 1),
}
},
],
})),
listeners(({ actions, values }) => ({
setSearch: async (_, breakpoint) => {
await breakpoint(300)
actions.loadSubscriptions()
},
setPage: () => actions.loadSubscriptions(),
setCreatedByFilter: () => actions.loadSubscriptions(),
setCurrentTab: ({ tab }) => {
if (tab === SubscriptionsTab.AI && !values.aiSubscriptionsAvailable) {
actions.setCurrentTab(SubscriptionsTab.All)
return
}
actions.loadSubscriptions()
},
setSubscriptionsSorting: () => actions.loadSubscriptions(),
setTargetTypeFilter: () => actions.loadSubscriptions(),
applySubscriptionsQueryFromUrl: () => {
if (values.currentTab === SubscriptionsTab.AI && !values.aiSubscriptionsAvailable) {
actions.setCurrentTab(SubscriptionsTab.All)
return
}
actions.loadSubscriptions()
},
[featureFlagLogic.actionTypes.setFeatureFlags]: () => {
if (values.currentTab === SubscriptionsTab.AI && !values.aiSubscriptionsAvailable) {
actions.setCurrentTab(SubscriptionsTab.All)
}
},
[organizationLogic.actionTypes.loadCurrentOrganizationSuccess]: () => {
if (values.currentTab === SubscriptionsTab.AI && !values.aiSubscriptionsAvailable) {
actions.setCurrentTab(SubscriptionsTab.All)
}
},
deleteSubscriptionSuccess: () => actions.loadSubscriptions(),
deliverSubscription: async ({ id }) => {
const result = await runSubscriptionTestDelivery(() =>
subscriptionsTestDeliveryCreate(String(getCurrentTeamId()), id)
)
if (result === 'success') {
actions.deliverSubscriptionSuccess()
} else {
actions.deliverSubscriptionFailure()
}
},
setSubscriptionEnabled: async ({ id, enabled }) => {
const ok = await toggleSubscriptionEnabled(id, enabled)
if (ok) {
actions.setSubscriptionEnabledSuccess(id)
} else {
actions.setSubscriptionEnabledFailure(id)
}
},
setSubscriptionEnabledSuccess: () => actions.loadSubscriptions(),
})),
trackedActionToUrl(({ values }) => {
const syncUrl = (
replace: boolean
): [string, Record<string, any>, Record<string, unknown> | undefined, { replace: boolean }] | undefined => {
const subscriptionValues = {
currentTab: values.currentTab,
search: values.search,
createdByUuid: values.createdByUuid,
targetTypeFilter: values.targetTypeFilter,
page: values.page,
}
const pathname = router.values.location.pathname
if (pathname !== urls.subscriptions()) {
return
}
if (subscriptionRouterSearchParamsEqual(router.values.searchParams, subscriptionValues)) {
return
}
const next = mergeSubscriptionRouterSearchParams(router.values.searchParams, subscriptionValues)
return [pathname, next, router.values.hashParams, { replace }] as const
}
return {
setSearch: () => syncUrl(true),
setCreatedByFilter: () => syncUrl(true),
setTargetTypeFilter: () => syncUrl(true),
setPage: () => syncUrl(true),
setCurrentTab: () => syncUrl(false),
}
}),
urlToAction(({ actions, values }) => ({
[urls.subscriptionNew()]: () => {
actions.setSubscriptionModalId('new')
},
[urls.subscriptionEdit(':subscriptionId')]: ({ subscriptionId }) => {
const editingId = Number(subscriptionId)
actions.setSubscriptionModalId(Number.isFinite(editingId) ? editingId : 'new')
},
[urls.subscriptions()]: (_, searchParams) => {
if (values.subscriptionModalId !== null) {
actions.setSubscriptionModalId(null)
}
const parsed = parseSubscriptionsSearchParams(searchParams)
const listState = {
currentTab: values.currentTab,
search: values.search,
createdByUuid: values.createdByUuid,
targetTypeFilter: values.targetTypeFilter,
page: values.page,
}
if (subscriptionsListStateEqual(parsed, listState)) {
return
}
actions.applySubscriptionsQueryFromUrl(parsed)
},
})),
afterMount(({ actions }) => {
actions.loadSubscriptions()
}),
])