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

284 lines
10 KiB
TypeScript

import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { Button, Dialog, Menu, Switch, Link, Tooltip } from '@hanzo/elements'
import { IconEllipsis } from '@hanzo/icons'
import { pngMascot } from 'lib/brand/mascot'
import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction'
import { TZLabel } from 'lib/components/TZLabel'
import type { MenuItems } from 'lib/elements/Menu'
import { Table, TableColumn, TableColumns } from 'lib/elements/Table'
import { createdByColumn } from 'lib/elements/Table/columnUtils'
import { TableLink } from 'lib/elements/Table/TableLink'
import { urls } from 'scenes/urls'
import { AlertState, ProductKey } from '~/queries/schema/schema-general'
import { AlertStateIndicator } from '../components/AlertDefinition'
import { AlertsFiltersBar } from '../components/AlertsFiltersBar'
import { alertIntervalDisplayLabel } from '../logic/alertIntervalHelpers'
import { alertLogic } from '../logic/alertLogic'
import { alertNotificationLogic } from '../logic/alertNotificationLogic'
import { alertsLogic } from '../logic/alertsLogic'
import { AlertType } from '../types'
import { EditAlertModal } from './EditAlertModal'
const MascotMagnifyingGlass = pngMascot()
interface InsightAlertsProps {
alertId: AlertType['id'] | null
}
interface AlertRowMenuProps {
alert: AlertType
destinationCount: number
deleting: boolean
onDelete: () => void
}
function AlertRowMenu({ alert, destinationCount, deleting, onDelete }: AlertRowMenuProps): JSX.Element {
const notificationLogic = alertNotificationLogic({ alertId: alert.id, loadDestinations: false })
const { testDeliveryResultLoading } = useValues(notificationLogic)
const { sendTestDelivery } = useActions(notificationLogic)
const canTestDelivery = alert.subscribed_users.some((user) => Boolean(user.email)) || destinationCount > 0
const menuItems: MenuItems = [
canTestDelivery && {
label: 'Test delivery',
'data-attr': 'insight-alert-row-send-test',
disabledReason: testDeliveryResultLoading ? 'Sending test delivery…' : null,
onClick: sendTestDelivery,
},
{
label: 'Delete',
status: 'danger' as const,
'data-attr': 'insight-alert-row-delete',
disabledReason: deleting ? 'Deleting…' : null,
onClick: onDelete,
},
]
return (
<Menu items={menuItems} placement="bottom-end">
<Button
type="tertiary"
size="small"
icon={<IconEllipsis />}
aria-label={`More options for ${alert.name}`}
/>
</Menu>
)
}
export function InsightAlerts({ alertId }: InsightAlertsProps): JSX.Element {
const { push } = useActions(router)
const logic = alertsLogic()
const { deleteAlert, loadAlerts, toggleAlertEnabled } = useActions(logic)
const {
alertsSortedByState,
alertsResponseLoading,
deletingAlertIds,
pagination,
alertsCount,
isFiltering,
togglingAlertIds,
alertDestinationCounts,
} = useValues(logic)
const { alert } = useValues(alertLogic({ alertId }))
const columns: TableColumns<AlertType> = [
{
key: 'id',
width: 32,
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: function renderName(name: any, alert) {
return (
<>
<TableLink
to={urls.alert(alert.id)}
className={alert.enabled ? '' : 'text-muted'}
title={
<div className="flex flex-row gap-3 items-center">
<div>{name}</div>
</div>
}
/>
</>
)
},
},
{
title: 'Status',
dataIndex: 'state',
render: function renderStateIndicator(_, alert: AlertType) {
return alert.enabled ? <AlertStateIndicator alert={alert} /> : null
},
},
{
title: 'Interval',
dataIndex: 'calculation_interval',
key: 'calculation_interval',
render: function renderInterval(_, alert: AlertType) {
return <div className="whitespace-nowrap">{alertIntervalDisplayLabel(alert.calculation_interval)}</div>
},
},
{
title: 'Last checked',
sorter: true,
defaultSortOrder: -1,
dataIndex: 'last_checked_at',
render: function renderLastChecked(_, alert: AlertType) {
return (
<div className="whitespace-nowrap">
{alert.last_checked_at ? (
<TZLabel time={alert.last_checked_at} />
) : (
<span className="text-muted">N/A</span>
)}
</div>
)
},
},
{
title: 'Last notified',
sorter: true,
defaultSortOrder: -1,
dataIndex: 'last_notified_at',
render: function renderLastModified(_, alert: AlertType) {
return (
<div className="whitespace-nowrap">
{alert.last_notified_at ? (
<TZLabel time={alert.last_notified_at} />
) : (
<span className="text-muted">N/A</span>
)}
</div>
)
},
},
createdByColumn() as TableColumn<AlertType, keyof AlertType | undefined>,
{
title: 'Insight',
dataIndex: 'insight',
key: 'insight',
render: function renderInsightLink(insight: any) {
return (
<TableLink
to={urls.insightView(insight.short_id)}
title={
<Tooltip title={insight.name}>
<div>{insight.name || insight.derived_name}</div>
</Tooltip>
}
/>
)
},
},
{
title: 'Enabled',
dataIndex: 'enabled',
key: 'enabled',
render: (_, alert) => (
<Switch
checked={alert.enabled}
onChange={() => toggleAlertEnabled(alert)}
loading={togglingAlertIds.has(alert.id)}
aria-label={`${alert.enabled ? 'Disable' : 'Enable'} ${alert.name}`}
data-attr="insight-alert-row-toggle"
/>
),
},
{
title: '',
render: (_, alert) => (
<AlertRowMenu
alert={alert}
destinationCount={alertDestinationCounts[alert.id] ?? 0}
deleting={deletingAlertIds.has(alert.id)}
onDelete={() => {
Dialog.open({
title: `Delete "${alert.name}"?`,
description: 'This alert will be permanently deleted. This action cannot be undone.',
primaryButton: {
children: 'Delete',
type: 'primary',
status: 'danger',
onClick: () => deleteAlert(alert),
'data-attr': 'insight-alert-delete-confirm',
},
secondaryButton: {
children: 'Cancel',
},
})
}}
/>
),
},
]
const isEmpty = alertsCount === 0 && !alertsResponseLoading && !isFiltering
const alertForEditModal = alert ?? alertsSortedByState.find((candidate) => candidate.id === alertId)
return (
<>
{isEmpty && (
<ProductIntroduction
productName="Alerts"
productKey={ProductKey.ALERTS}
thingName="alert"
description="Alerts enable you to monitor your insight and notify you when certain conditions are met."
isEmpty
customScript={MascotMagnifyingGlass}
actionElementOverride={
<span className="italic">
To get started, open a <Link to={urls.insights()}>saved insight</Link>, then select Alerts
from the Actions sidebar.
</span>
}
mcpSurfaceKey="alerts.create"
/>
)}
{alertForEditModal && (
<EditAlertModal
onClose={() => push(urls.alerts())}
isOpen
alert={alertForEditModal}
useAlertCheckPreview
onEditSuccess={() => {
loadAlerts()
push(urls.alerts())
}}
/>
)}
{isEmpty ? null : (
<>
<AlertsFiltersBar />
<Table
loading={alertsResponseLoading}
columns={columns}
dataSource={alertsSortedByState}
noSortingCancellation
rowKey="id"
loadingSkeletonRows={5}
nouns={['alert', 'alerts']}
pagination={pagination}
rowClassName={(alert) => (alert.state === AlertState.NOT_FIRING ? null : 'highlighted')}
emptyState={
isFiltering ? (
<div className="py-8 text-center text-secondary">No alerts match your filters</div>
) : undefined
}
/>
</>
)}
</>
)
}