Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

238 lines
11 KiB
TypeScript

import { Banner } from '@hanzo/elements'
import { Radio } from 'lib/elements/Radio'
import { AlertFormType } from 'products/alerts/frontend/logic/alertFormLogic'
import { getDefaultAnomalyDetectorConfig } from 'products/alerts/frontend/logic/detectorConfigDefaults'
import { FunnelAlertPreview } from 'products/alerts/frontend/logic/funnelAlertPreview'
import { InsightsQLAlertPreview } from 'products/alerts/frontend/logic/insightsqlAlertPreview'
import {
AlertSimulationResult,
isFunnelsAlertConfig,
isInsightsQLAlertConfig,
isTrendsAlertConfig,
} from 'products/alerts/frontend/types'
import { DetectorSelector } from 'products/alerts/frontend/views/DetectorSelector'
import { FunnelsDefinitionFields, InsightsQLDefinitionFields, TrendsDefinitionFields } from './AlertDefinitionFields'
import { AlertSimulationSection } from './AlertSimulationSection'
import { InvestigationAgentSettings } from './InvestigationAgentSettings'
import { ThresholdDefinitionRow } from './ThresholdDefinitionRow'
import type { ThresholdRowRenderProps } from './ThresholdDefinitionRow'
export type { ThresholdRowRenderProps } from './ThresholdDefinitionRow'
export interface TrendsDefinitionProps {
/** Series in the alerted insight, for the series picker. */
alertSeries: Array<{ custom_name?: string | null; name?: string | null; event?: string | null }> | null
/** Formula nodes in the alerted insight, if any. */
formulaNodes: Array<{ formula: string; custom_name?: string | null }> | undefined
/** Whether the insight has a valid breakdown; drives the per-value monitoring banner. */
isBreakdownValid: boolean
}
export interface FunnelDefinitionProps {
/** Funnel step labels (real event/series names) for the conversion picker. */
stepLabels: string[]
/** Conversion rate(s) the alert would evaluate now; null until the result loads. */
preview: FunnelAlertPreview | null
/** A trends funnel alerts on the overall rate over time, so it skips the step picker. */
isTrendsFunnel: boolean
}
export interface InsightsQLDefinitionProps {
/** What a SQL alert would evaluate now; null until the result loads. */
preview: InsightsQLAlertPreview | null
/** Result column names, for the column pickers. */
columns: string[] | null
/** Evaluated-column picker options (numeric columns, with fallbacks). */
valueColumnOptions: { label: string; value: string }[]
/** Label-column picker options (every column except the evaluated one). */
labelColumnOptions: { label: string; value: string }[]
}
export interface AlertDefinitionSectionProps {
alertForm: AlertFormType
alertMode: 'detector' | 'threshold'
thresholdBoundsFormError?: string
isNonTimeSeriesDisplay: boolean
// Kind-specific inputs, grouped so the shared section only carries the bundle for the active kind.
trends: TrendsDefinitionProps
funnel: FunnelDefinitionProps
insightsql: InsightsQLDefinitionProps
supportsAnomalyDetection: boolean
twoColumnLayout?: boolean
investigationAgentEnabled: boolean
simulationResult: AlertSimulationResult | null
simulationResultLoading: boolean
simulationDateFrom: string | null
onSetAlertFormValue: <K extends keyof AlertFormType>(key: K, value: AlertFormType[K]) => void
/** Override the threshold row renderer. The legacy modal uses the inline wrapping row; the
* redesigned modal passes a stacked, labeled variant. Omit to keep the legacy row. */
thresholdRowRenderer?: (props: ThresholdRowRenderProps) => JSX.Element
onSimulateAlert: () => void
onSetSimulationDateFrom: (value: string) => void
onClearSimulation: () => void
onClearSimulationOverlay: () => void
}
export function AlertDefinitionSection({
alertForm,
alertMode,
thresholdBoundsFormError,
isNonTimeSeriesDisplay,
trends,
funnel,
insightsql,
supportsAnomalyDetection,
twoColumnLayout = false,
investigationAgentEnabled,
simulationResult,
simulationResultLoading,
simulationDateFrom,
onSetAlertFormValue,
thresholdRowRenderer,
onSimulateAlert,
onSetSimulationDateFrom,
onClearSimulation,
onClearSimulationOverlay,
}: AlertDefinitionSectionProps): JSX.Element {
// A steps funnel evaluates a single conversion-rate snapshot, so relative conditions have no prior
// value to compare against. A historical-trend funnel is a time series, so it does support them.
const isFunnelAlert = isFunnelsAlertConfig(alertForm.config)
const supportsRelativeConditions = !isFunnelAlert || funnel.isTrendsFunnel
let definitionFields: JSX.Element | null = null
if (isTrendsAlertConfig(alertForm.config)) {
definitionFields = (
<TrendsDefinitionFields
alertSeries={trends.alertSeries}
formulaNodes={trends.formulaNodes}
isBreakdownValid={trends.isBreakdownValid}
alertMode={alertMode}
/>
)
} else if (isFunnelAlert) {
definitionFields = (
<FunnelsDefinitionFields
alertForm={alertForm}
stepLabels={funnel.stepLabels}
funnelPreview={funnel.preview}
isTrendsFunnel={funnel.isTrendsFunnel}
showInlinePreview={!twoColumnLayout}
onSetAlertFormValue={onSetAlertFormValue}
/>
)
} else if (isInsightsQLAlertConfig(alertForm.config)) {
definitionFields = (
<InsightsQLDefinitionFields
alertForm={alertForm}
insightsqlPreview={insightsql.preview}
insightsqlColumns={insightsql.columns}
insightsqlValueColumnOptions={insightsql.valueColumnOptions}
insightsqlLabelColumnOptions={insightsql.labelColumnOptions}
showInlinePreview={!twoColumnLayout}
onSetAlertFormValue={onSetAlertFormValue}
/>
)
}
return (
<>
{/* Trends-specific copy; funnels have their own breakdown messaging in the preview banner. */}
{trends.isBreakdownValid && isTrendsAlertConfig(alertForm.config) && (
<Banner type="warning">
{alertMode === 'detector'
? 'For trends with breakdown, the detector will independently monitor each breakdown value (up to 25) and fire if any is anomalous.'
: 'For trends with breakdown, the alert will fire if any of the breakdown values breaches the threshold.'}
</Banner>
)}
<div
className={
twoColumnLayout ? 'grid items-start gap-6 md:grid-cols-[minmax(0,55%)_minmax(0,1fr)]' : 'space-y-3'
}
>
<div className="space-y-3">
{definitionFields}
{supportsAnomalyDetection && (
<Radio
radioPosition="top"
value={alertMode}
onChange={(value) =>
onSetAlertFormValue(
'detector_config',
value === 'detector'
? getDefaultAnomalyDetectorConfig(alertForm.calculation_interval)
: null
)
}
options={[
{
value: 'threshold',
label: 'Threshold',
description: 'Alert when a value goes above or below a fixed value you set.',
'data-attr': 'alertForm-mode-threshold',
},
{
value: 'detector',
label: 'Anomaly detection',
description:
'Automatically flag unusual changes using statistical models. No fixed value needed.',
'data-attr': 'alertForm-mode-detector',
},
]}
/>
)}
</div>
<div className="space-y-3">
{alertMode === 'threshold' ? (
thresholdRowRenderer ? (
thresholdRowRenderer({
alertForm,
thresholdBoundsFormError,
isNonTimeSeriesDisplay,
supportsRelativeConditions,
onSetAlertFormValue,
})
) : (
<ThresholdDefinitionRow
alertForm={alertForm}
thresholdBoundsFormError={thresholdBoundsFormError}
isNonTimeSeriesDisplay={isNonTimeSeriesDisplay}
supportsRelativeConditions={supportsRelativeConditions}
onSetAlertFormValue={onSetAlertFormValue}
/>
)
) : (
<DetectorSelector
value={alertForm.detector_config ?? null}
onChange={(config) => {
onSetAlertFormValue('detector_config', config)
onClearSimulation()
onClearSimulationOverlay()
}}
calculationInterval={alertForm.calculation_interval}
/>
)}
{alertMode === 'detector' && alertForm.detector_config && investigationAgentEnabled && (
<InvestigationAgentSettings alertForm={alertForm} onSetAlertFormValue={onSetAlertFormValue} />
)}
{alertMode === 'detector' && alertForm.detector_config && (
<AlertSimulationSection
alertForm={alertForm}
simulationResult={simulationResult}
simulationResultLoading={simulationResultLoading}
simulationDateFrom={simulationDateFrom}
onSimulateAlert={onSimulateAlert}
onSetSimulationDateFrom={onSetSimulationDateFrom}
/>
)}
</div>
</div>
</>
)
}