mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
Each of these was checked against the live database before it moved,
because a member name is free to rename and a stored value is not.
Members only -- the value never said hog, so nothing in Postgres changes:
TaggerType.HOG / EvaluationType.HOG the column already holds `script`;
`HOG = "script", "Script"` was a
member disagreeing with its own value
FEATURE_FLAGS.HOG the flag key `script` is read by the
flag service and does not move
HEDGEFN_SKIN_SPIDERHOG same, key `mascot-skin-spiderscript`
_HOGLAND_MARKER already `/var/lib/script`; renamed to
_BOX_MARKER, which is what the
function returns and what the comment
beside it already called it
Value too, having established nothing stores it: `InsightType.HOG = 'HOG'`
is derived from `query.kind === 'ScriptQuery'`, never written -- the one
writer, queryNodeToFilter, has no ScriptQuery entry, and zero of the 28
insight rows carry it in `filters.insight`.
Two readers keyed on that value had to move with it, and neither would
have been caught by renaming the enum alone: INSIGHT_TYPE_URLS spells
`HOG:` as a plain key while its siblings are computed, and the Slack
unfurl map keys the same string in Python.
The rest is copy a person reads -- "HOG code exceeds maximum size" is an
API error message, not an identifier.
Left alone deliberately: `ReviewHog` and `HogDesk`, which survive only in
applied migrations' `choices=` lists. The live members are already
`REVIEW = "review"` and `Desk = "desk"`, and Django compares choices in
Python without ever writing them, so those lists are a snapshot of a
field that has not looked like that for some time.
Co-authored-by: Hanzo Dev <[email protected]>
156 lines
7.7 KiB
TypeScript
156 lines
7.7 KiB
TypeScript
import { useActions, useValues } from 'kea'
|
|
import { BindLogic } from 'kea'
|
|
|
|
import { IconEndpoints, IconPlus } from '@hanzo/icons'
|
|
|
|
import { IconInsightRetention, IconInsightTrends } from 'lib/elements/icons'
|
|
import { Button } from 'lib/elements/Button'
|
|
import { Modal } from 'lib/elements/Modal'
|
|
import { Popover } from 'lib/elements/Popover'
|
|
import { addSavedInsightsModalLogic } from 'scenes/saved-insights/addSavedInsightsModalLogic'
|
|
import { INSIGHT_TYPES_METADATA } from 'scenes/saved-insights/SavedInsights'
|
|
import { SavedInsightsTable } from 'scenes/saved-insights/SavedInsightsTable'
|
|
import { urls } from 'scenes/urls'
|
|
|
|
import { EndpointQueryNode, InsightsQLQuery, NodeKind } from '~/queries/schema/schema-general'
|
|
import { isNodeWithSource } from '~/queries/utils'
|
|
import { InsightType, QueryBasedInsightModel } from '~/types'
|
|
|
|
import { EndpointFromInsightModal } from './EndpointFromInsightModal'
|
|
import { endpointLogic } from './endpointLogic'
|
|
import { insightPickerEndpointModalLogic } from './insightPickerEndpointModalLogic'
|
|
|
|
const UNSUPPORTED_INSIGHT_TYPES = new Set([
|
|
InsightType.FUNNELS,
|
|
InsightType.PATHS,
|
|
InsightType.STICKINESS,
|
|
InsightType.JSON,
|
|
InsightType.SCRIPT,
|
|
])
|
|
|
|
const UNSUPPORTED_QUERY_KINDS = new Set([NodeKind.FunnelsQuery, NodeKind.PathsQuery, NodeKind.StickinessQuery])
|
|
|
|
function isInsightSupported(insight: QueryBasedInsightModel): boolean {
|
|
const query = insight.query
|
|
if (!query) {
|
|
return true
|
|
}
|
|
const kind = isNodeWithSource(query) ? (query as { source?: { kind?: string } }).source?.kind : query.kind
|
|
return !kind || !UNSUPPORTED_QUERY_KINDS.has(kind as NodeKind)
|
|
}
|
|
|
|
const QUICK_CREATE_TYPES = [
|
|
{ type: InsightType.TRENDS, icon: IconInsightTrends, label: 'Trend' },
|
|
{ type: InsightType.RETENTION, icon: IconInsightRetention, label: 'Retention' },
|
|
]
|
|
|
|
export function InsightPickerEndpointModal(): JSX.Element {
|
|
const { isOpen, selectedInsight, showMoreInsightTypes } = useValues(insightPickerEndpointModalLogic)
|
|
const { closeModal, selectInsight, toggleShowMoreInsightTypes } = useActions(insightPickerEndpointModalLogic)
|
|
const { openCreateFromInsightModal } = useActions(endpointLogic)
|
|
|
|
// Safe cast: unsupported query types (FunnelsQuery, PathsQuery, StickinessQuery)
|
|
// are filtered out via isInsightSupported on the SavedInsightsTable
|
|
const insightQuery: InsightsQLQuery | EndpointQueryNode | null = selectedInsight?.query
|
|
? isNodeWithSource(selectedInsight.query)
|
|
? (selectedInsight.query.source as unknown as InsightsQLQuery | EndpointQueryNode)
|
|
: (selectedInsight.query as unknown as InsightsQLQuery | EndpointQueryNode)
|
|
: null
|
|
|
|
const additionalTypes = Object.entries(INSIGHT_TYPES_METADATA).filter(
|
|
([type, meta]) =>
|
|
meta.inMenu &&
|
|
!UNSUPPORTED_INSIGHT_TYPES.has(type as InsightType) &&
|
|
!QUICK_CREATE_TYPES.some((qt) => qt.type === type)
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<BindLogic logic={addSavedInsightsModalLogic} props={{}}>
|
|
<Modal
|
|
title="New insight-based endpoint"
|
|
onClose={closeModal}
|
|
isOpen={isOpen}
|
|
width="min(80vw, 64rem)"
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap items-center gap-3 p-4 bg-surface-secondary rounded-lg">
|
|
<IconPlus className="text-2xl text-secondary shrink-0" />
|
|
<div className="flex-1 min-w-[200px]">
|
|
<div className="font-semibold text-base">Create an endpoint from a new insight</div>
|
|
<div className="text-sm text-secondary">
|
|
<>
|
|
Once the insight is saved, open the right side panel and click
|
|
<br /> <IconEndpoints /> <code>Create endpoint</code>.
|
|
</>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{QUICK_CREATE_TYPES.map(({ type, icon: Icon, label }) => (
|
|
<Button
|
|
key={type}
|
|
type="primary"
|
|
icon={<Icon />}
|
|
to={urls.insightNew({ type, sceneSource: 'endpoints' })}
|
|
tooltip={INSIGHT_TYPES_METADATA[type]?.description}
|
|
data-attr={`endpoint-quick-create-${type.toLowerCase()}`}
|
|
>
|
|
{label}
|
|
</Button>
|
|
))}
|
|
<Popover
|
|
visible={showMoreInsightTypes}
|
|
onClickOutside={() => toggleShowMoreInsightTypes()}
|
|
overlay={
|
|
<div className="p-2 space-y-1 min-w-48">
|
|
{additionalTypes.map(([type, metadata]) => {
|
|
const Icon = metadata.icon
|
|
return (
|
|
<Button
|
|
key={type}
|
|
type="tertiary"
|
|
fullWidth
|
|
icon={Icon ? <Icon /> : undefined}
|
|
to={urls.insightNew({
|
|
type: type as InsightType,
|
|
sceneSource: 'endpoints',
|
|
})}
|
|
data-attr={`endpoint-create-${type.toLowerCase()}`}
|
|
>
|
|
{metadata.name}
|
|
</Button>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
>
|
|
<Button type="secondary" onClick={() => toggleShowMoreInsightTypes()}>
|
|
More
|
|
</Button>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="font-semibold text-base mb-2">
|
|
Create an endpoint from an existing insight
|
|
</div>
|
|
<SavedInsightsTable
|
|
onToggle={(insight: QueryBasedInsightModel) => {
|
|
selectInsight(insight)
|
|
openCreateFromInsightModal()
|
|
}}
|
|
filterFn={isInsightSupported}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</BindLogic>
|
|
|
|
{insightQuery && (
|
|
<EndpointFromInsightModal insightQuery={insightQuery} insightShortId={selectedInsight?.short_id} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|