mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-03 00:52:32 +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]>
284 lines
10 KiB
TypeScript
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
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|