mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
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]>
224 lines
9.4 KiB
TypeScript
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,
|
|
}
|