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