Files
zeekayandHanzo Dev 841ea1c0da no hogs: products and insights
716 files rewritten, 30 renamed, same family mapping as the frontend pass.

Held back, and each for a reason rather than by oversight:

  db_table values          insights_hogfunction, insights_hogflow,
                           insights_hogfunctiontemplate, hogflow_templates.
                           Renaming a table is ALTER TABLE, so it ships as a
                           migration with the raw SQL in nodejs that names it.
  hog_invocation_results   a Datastore table with Kafka engine tables on it
  products/review_hog      the module path is the Django app path; moving it
                           moves app_label and every migration is recorded under
                           (app_label, name). That is the same history break as
                           renaming a migration file, one level up.
  */migrations/*           five files, untouched
  whogho                   an integration vendor

Checked: 395 of 397 changed Python files compile. The two that do not —
insightsql/database/database.py and temporal/schedule.py — fail identically at
origin/main under the same interpreter, because they use `type X = ...` and
`except*`, which need the 3.13 this repo pins and not the 3.9 doing the check.
Running the check against the original is what told those apart from a real
break.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 09:16:43 -07:00

224 lines
9.4 KiB
TypeScript

import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { Button, Menu, Modal, Link } from '@hanzo/elements'
import { IconEllipsis } from '@hanzo/icons'
import { pngMascot } from 'lib/brand/mascot'
import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction'
import { Spinner } from 'lib/elements/Spinner'
import { Switch } from 'lib/elements/Switch'
import { Tab, Tabs } from 'lib/elements/Tabs'
import { deleteWithUndo } from 'lib/utils/deleteWithUndo'
import { getCurrentTeamId } from 'lib/utils/getAppContext'
import { sceneConfigurations } from 'scenes/scenes'
import { Scene, SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { SceneContent } from '~/layout/scenes/components/SceneContent'
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
import { ProductKey } from '~/queries/schema/schema-general'
import type { SubscriptionApi } from 'products/subscriptions/frontend/generated/api.schemas'
import { EditSubscription } from '../components/Subscriptions/views/EditSubscription'
import { SubscriptionsFiltersBar } from './components/SubscriptionsFiltersBar'
import {
SubscriptionsTable,
isSubscriptionEnabled,
subscriptionEditHref,
subscriptionName,
} from './components/SubscriptionsTable'
import { SubscriptionsTab, subscriptionsSceneLogic } from './subscriptionsSceneLogic'
const MascotMagnifyingGlass = pngMascot()
function SubscriptionEnabledSwitch({ sub }: { sub: SubscriptionApi }): JSX.Element {
const { setSubscriptionEnabled } = useActions(subscriptionsSceneLogic)
const { togglingEnabledIds } = useValues(subscriptionsSceneLogic)
const enabled = isSubscriptionEnabled(sub)
return (
<Switch
checked={enabled}
onChange={(newEnabled) => setSubscriptionEnabled(sub.id, newEnabled)}
loading={Boolean(togglingEnabledIds[sub.id])}
aria-label={`${enabled ? 'Disable' : 'Enable'} ${subscriptionName(sub)}`}
data-attr="subscription-row-toggle-enabled"
/>
)
}
function SubscriptionsRowActions({ sub }: { sub: SubscriptionApi }): JSX.Element {
const { push } = useActions(router)
const { deleteSubscriptionSuccess, deliverSubscription } = useActions(subscriptionsSceneLogic)
const { deliveringSubscriptionId, togglingEnabledIds } = useValues(subscriptionsSceneLogic)
const href = subscriptionEditHref(sub)
const isDelivering = deliveringSubscriptionId === sub.id
const isToggling = Boolean(togglingEnabledIds[sub.id])
const enabled = isSubscriptionEnabled(sub)
return (
<Menu
items={[
...(href
? [
{
label: 'Edit subscription',
onClick: () => push(href),
},
]
: []),
...(enabled
? [
{
label: 'Test delivery',
'data-attr': 'subscription-list-item-manual-deliver',
disabledReason: isDelivering ? 'Sending test delivery…' : null,
onClick: () => deliverSubscription(sub.id),
},
]
: []),
{
label: 'Delete',
status: 'danger' as const,
onClick: () =>
void deleteWithUndo({
endpoint: `projects/${String(getCurrentTeamId())}/subscriptions`,
object: {
id: sub.id,
name: subscriptionName(sub),
},
callback: () => deleteSubscriptionSuccess(),
}),
},
]}
>
<Button
icon={isDelivering || isToggling ? <Spinner /> : <IconEllipsis />}
size="small"
aria-label="Subscription actions"
disabled={isDelivering || isToggling}
/>
</Menu>
)
}
export function SubscriptionsScene(): JSX.Element {
const {
subscriptions,
subscriptionsLoading,
subscriptionsListAwaitingDebouncedFetch,
pagination,
search,
createdByUuid,
currentTab,
subscriptionsSorting,
targetTypeFilter,
subscriptionModalId,
aiSubscriptionsAvailable,
} = useValues(subscriptionsSceneLogic)
const { setCurrentTab, setSubscriptionsSorting } = useActions(subscriptionsSceneLogic)
const isFiltered =
Boolean(search.trim()) ||
createdByUuid !== null ||
targetTypeFilter !== null ||
currentTab !== SubscriptionsTab.All
const subscriptionTabs: Tab<SubscriptionsTab>[] = [
{ key: SubscriptionsTab.All, label: 'All subscriptions' },
{ key: SubscriptionsTab.Mine, label: 'My subscriptions' },
{ key: SubscriptionsTab.Dashboard, label: 'Dashboard' },
{ key: SubscriptionsTab.Insight, label: 'Insight' },
...(aiSubscriptionsAvailable ? [{ key: SubscriptionsTab.AI, label: 'AI prompt' }] : []),
]
const showProductIntroduction =
subscriptions.length === 0 && !subscriptionsLoading && !isFiltered && !subscriptionsListAwaitingDebouncedFetch
const emptyStateDescription = aiSubscriptionsAvailable
? 'Send scheduled dashboard and insight snapshots, or use an AI prompt to generate a recurring report. Deliver subscriptions to Slack or email.'
: 'Get recurring email or Slack digests, or scheduled exports from insights and dashboards. Use them for weekly rollups, stakeholder updates, or wiring metrics into your own systems.'
const emptyStateAction = aiSubscriptionsAvailable ? (
<span className="italic">
<Link to={urls.subscriptionNew()}>Create an AI prompt subscription</Link>, or open a{' '}
<Link to={urls.dashboards()}>dashboard</Link> or <Link to={urls.insights()}>saved insight</Link> to schedule
a snapshot.
</span>
) : (
<span className="italic">
Open a <Link to={urls.dashboards()}>dashboard</Link> or <Link to={urls.insights()}>saved insight</Link> to
schedule a snapshot.
</span>
)
return (
<SceneContent>
<SceneTitleSection
name={sceneConfigurations[Scene.Subscriptions].name}
description={sceneConfigurations[Scene.Subscriptions].description}
resourceType={{ type: 'inbox' }}
actions={
aiSubscriptionsAvailable ? (
<Button
type="primary"
data-attr="new-subscription-button"
onClick={() => router.actions.push(urls.subscriptionNew())}
>
New prompt subscription
</Button>
) : undefined
}
/>
<Tabs
activeKey={currentTab}
onChange={(newKey) => setCurrentTab(newKey)}
tabs={subscriptionTabs}
sceneInset
/>
<div className="py-8 flex-1 min-h-0 flex flex-col gap-4 max-w-full">
{showProductIntroduction ? (
<ProductIntroduction
productName="Subscriptions"
productKey={ProductKey.SUBSCRIPTIONS}
thingName="subscription"
titleOverride="No subscriptions yet"
description={emptyStateDescription}
isEmpty
customScript={MascotMagnifyingGlass}
scriptLayout="responsive"
useMainContentContainerQueries
docsURL="https://hanzo.ai/docs/user-guides/subscriptions"
actionElementOverride={emptyStateAction}
/>
) : (
<>
<SubscriptionsFiltersBar />
<SubscriptionsTable
dataSource={subscriptions}
loading={subscriptionsLoading}
pagination={pagination}
sorting={subscriptionsSorting}
onSort={setSubscriptionsSorting}
renderRowActions={(sub) => <SubscriptionsRowActions sub={sub} />}
renderEnabledToggle={(sub) => <SubscriptionEnabledSwitch sub={sub} />}
/>
</>
)}
</div>
{subscriptionModalId !== null && (
<Modal isOpen onClose={() => router.actions.push(urls.subscriptions())} simple={false} width={650}>
<EditSubscription
id={subscriptionModalId}
onCancel={() => router.actions.push(urls.subscriptions())}
onDelete={() => router.actions.push(urls.subscriptions())}
/>
</Modal>
)}
</SceneContent>
)
}
export const scene: SceneExport = {
component: SubscriptionsScene,
logic: subscriptionsSceneLogic,
}