diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index 6d90866fe208e..ed33ce8d45716 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -29,9 +29,11 @@ import { } from '../Notebook/utils' export interface NodeWrapperProps { - title: string | ((attributes: CustomNotebookNodeAttributes) => Promise) nodeType: NotebookNodeType children?: ReactNode | ((isEdit: boolean, isPreview: boolean) => ReactNode) + + // Meta properties - these should never be too advanced - more advanced should be done via updateAttributes in the component + defaultTitle: string href?: string | ((attributes: NotebookNodeAttributes) => string | undefined) // Sizing @@ -48,7 +50,7 @@ export interface NodeWrapperProps { } export function NodeWrapper({ - title: titleOrGenerator, + defaultTitle, nodeType, children, selected, @@ -80,13 +82,12 @@ export function NodeWrapper({ nodeId, notebookLogic: mountedNotebookLogic, getPos, - title: titleOrGenerator, resizeable: resizeableOrGenerator, widgets, startExpanded, } const nodeLogic = useMountedLogic(notebookNodeLogic(nodeLogicProps)) - const { title, resizeable, expanded } = useValues(nodeLogic) + const { resizeable, expanded } = useValues(nodeLogic) const { setExpanded, deleteNode } = useActions(nodeLogic) const [ref, inView] = useInView({ triggerOnce: true }) @@ -115,6 +116,8 @@ export function NodeWrapper({ }, [resizeable, updateAttributes]) const parsedHref = typeof href === 'function' ? href(attributes) : href + // If a title is set on the attrs we use it. Otherwise we use the base component title. + const title = attributes.title ? attributes.title : defaultTitle // Element is resizable if resizable is set to true. If expandable is set to true then is is only resizable if expanded is true const isResizeable = resizeable && (!expandable || expanded) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx index 801970c380b51..c698e115379d9 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx @@ -7,13 +7,13 @@ import { urls } from 'scenes/urls' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { notebookNodeLogic } from './notebookNodeLogic' import { JSONContent, NotebookNodeViewProps } from '../Notebook/utils' -import api from 'lib/api' import { EarlyAccessFeatureLogicProps, earlyAccessFeatureLogic, } from 'scenes/early-access-features/earlyAccessFeatureLogic' import { PersonList } from 'scenes/early-access-features/EarlyAccessFeature' import { buildFlagContent } from './NotebookNodeFlag' +import { useEffect } from 'react' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes @@ -21,6 +21,14 @@ const Component = (props: NotebookNodeViewProps { + props.updateAttributes({ + title: earlyAccessFeature.name + ? `Early Access Management: ${earlyAccessFeature.name}` + : 'Early Access Management', + }) + }, [earlyAccessFeature?.name]) + return (
@@ -109,18 +117,7 @@ type NotebookNodeEarlyAccessAttributes = { export const NotebookNodeEarlyAccessFeature = createPostHogWidgetNode({ nodeType: NotebookNodeType.EarlyAccessFeature, - title: async (attributes) => { - const mountedEarlyAccessFeatureLogic = earlyAccessFeatureLogic.findMounted({ id: attributes.id }) - let title = mountedEarlyAccessFeatureLogic?.values.earlyAccessFeature.name || null - if (title === null) { - const retrievedEarlyAccessFeature: EarlyAccessFeatureType = await api.earlyAccessFeatures.get(attributes.id) - if (retrievedEarlyAccessFeature) { - title = retrievedEarlyAccessFeature.name - } - } - - return title ? `Early Access Management: ${title}` : 'Early Access Management' - }, + defaultTitle: 'Early Access Management', Component, heightEstimate: '3rem', href: (attrs) => urls.earlyAccessFeature(attrs.id), diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx index cb62cd17f301f..15aa9a5443948 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx @@ -122,7 +122,7 @@ type NotebookNodeExperimentAttributes = { export const NotebookNodeExperiment = createPostHogWidgetNode({ nodeType: NotebookNodeType.Experiment, - title: 'Experiment', + defaultTitle: 'Experiment', Component, heightEstimate: '3rem', href: (attrs) => urls.experiment(attrs.id), diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx index 066917f6f3c9a..e498cb7add7cf 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx @@ -1,5 +1,5 @@ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' -import { FeatureFlagType, NotebookNodeType } from '~/types' +import { NotebookNodeType } from '~/types' import { BindLogic, useActions, useValues } from 'kea' import { featureFlagLogic, FeatureFlagLogicProps } from 'scenes/feature-flags/featureFlagLogic' import { IconFlag, IconRecording, IconRocketLaunch, IconSurveys } from 'lib/lemon-ui/icons' @@ -12,10 +12,10 @@ import { JSONContent, NotebookNodeViewProps } from '../Notebook/utils' import { buildPlaylistContent } from './NotebookNodePlaylist' import { buildCodeExampleContent } from './NotebookNodeFlagCodeExample' import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions' -import api from 'lib/api' import { buildEarlyAccessFeatureContent } from './NotebookNodeEarlyAccessFeature' import { notebookNodeFlagLogic } from './NotebookNodeFlagLogic' import { buildSurveyContent } from './NotebookNodeSurvey' +import { useEffect } from 'react' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes @@ -37,6 +37,10 @@ const Component = (props: NotebookNodeViewProps): JS notebookNodeFlagLogic({ id, insertAfter }) ) + useEffect(() => { + props.updateAttributes({ title: featureFlag.key ? `Feature flag: ${featureFlag.key}` : 'Feature flag' }) + }, [featureFlag.key]) + return (
@@ -173,18 +177,7 @@ type NotebookNodeFlagAttributes = { export const NotebookNodeFlag = createPostHogWidgetNode({ nodeType: NotebookNodeType.FeatureFlag, - title: async (attributes) => { - const mountedFlagLogic = featureFlagLogic.findMounted({ id: attributes.id }) - let title = mountedFlagLogic?.values.featureFlag.key || null - if (title === null) { - const retrievedFlag: FeatureFlagType = await api.featureFlags.get(Number(attributes.id)) - if (retrievedFlag) { - title = retrievedFlag.key - } - } - - return title ? `Feature flag: ${title}` : 'Feature flag' - }, + defaultTitle: 'Feature flag', Component, heightEstimate: '3rem', href: (attrs) => urls.featureFlag(attrs.id), diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx index 6249b17f51349..6cc5b69a459c6 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx @@ -1,18 +1,24 @@ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' -import { FeatureFlagType, NotebookNodeType } from '~/types' +import { NotebookNodeType } from '~/types' import { useValues } from 'kea' import { FeatureFlagLogicProps, featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' import { FeatureFlagCodeExample } from 'scenes/feature-flags/FeatureFlagCodeExample' import { urls } from 'scenes/urls' import { JSONContent, NotebookNodeViewProps } from '../Notebook/utils' import { notebookNodeLogic } from './notebookNodeLogic' -import api from 'lib/api' +import { useEffect } from 'react' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes const { featureFlag } = useValues(featureFlagLogic({ id })) const { expanded } = useValues(notebookNodeLogic) + useEffect(() => { + props.updateAttributes({ + title: featureFlag.key ? `Feature flag code example: ${featureFlag.key}` : 'Feature flag code example', + }) + }, [featureFlag?.key]) + return
{expanded && }
} @@ -22,19 +28,7 @@ type NotebookNodeFlagCodeExampleAttributes = { export const NotebookNodeFlagCodeExample = createPostHogWidgetNode({ nodeType: NotebookNodeType.FeatureFlagCodeExample, - title: async (attributes) => { - const mountedFlagLogic = featureFlagLogic.findMounted({ id: attributes.id }) - let title = mountedFlagLogic?.values.featureFlag.key || null - - if (title === null) { - const retrievedFlag: FeatureFlagType = await api.featureFlags.get(Number(attributes.id)) - if (retrievedFlag) { - title = retrievedFlag.key - } - } - - return title ? `Feature flag code example: ${title}` : 'Feature flag code example' - }, + defaultTitle: 'Feature flag code example', Component, heightEstimate: '3rem', startExpanded: true, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx index 8dc4e00839409..ea8bf40d9afa4 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx @@ -77,7 +77,7 @@ type NotebookNodeImageAttributes = { export const NotebookNodeImage = createPostHogWidgetNode({ nodeType: NotebookNodeType.Image, - title: 'Image', + defaultTitle: 'Image', Component, serializedText: (attrs) => { // TODO file is null when this runs... should it be? diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx index d582171f9690a..21c21ff016543 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx @@ -10,7 +10,7 @@ import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { notebookNodeLogic } from './notebookNodeLogic' import { NotebookNodeViewProps } from '../Notebook/utils' import { asDisplay } from 'scenes/persons/person-utils' -import api from 'lib/api' +import { useEffect } from 'react' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes @@ -18,6 +18,12 @@ const Component = (props: NotebookNodeViewProps): const { person, personLoading } = useValues(logic) const { expanded } = useValues(notebookNodeLogic) + useEffect(() => { + props.updateAttributes({ + title: person ? `Person: ${asDisplay(person)}` : 'Person', + }) + }, [person]) + return (
@@ -51,17 +57,7 @@ type NotebookNodePersonAttributes = { export const NotebookNodePerson = createPostHogWidgetNode({ nodeType: NotebookNodeType.Person, - title: async (attributes) => { - const theMountedPersonLogic = personLogic.findMounted({ id: attributes.id }) - let person = theMountedPersonLogic?.values.person || null - - if (person === null) { - const response = await api.persons.list({ distinct_id: attributes.id }) - person = response.results[0] - } - - return person ? `Person: ${asDisplay(person)}` : 'Person' - }, + defaultTitle: 'Person', Component, heightEstimate: 300, minHeight: 100, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx index 0b0e3b7ca4ee8..6c84426c19d52 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx @@ -105,7 +105,7 @@ type NotebookNodePlaylistAttributes = { export const NotebookNodePlaylist = createPostHogWidgetNode({ nodeType: NotebookNodeType.RecordingPlaylist, - title: 'Session replays', + defaultTitle: 'Session replays', Component, heightEstimate: 'calc(100vh - 20rem)', href: (attrs) => { diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx index 8cc364293fd49..6e1d5681f95a8 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx @@ -3,17 +3,17 @@ import { DataTableNode, InsightVizNode, NodeKind, QuerySchema } from '~/queries/ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' import { InsightLogicProps, InsightShortId, NotebookNodeType } from '~/types' import { useMountedLogic, useValues } from 'kea' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { notebookNodeLogic } from './notebookNodeLogic' import { NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' import { containsHogQLQuery, isHogQLQuery, isNodeWithSource } from '~/queries/utils' import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' import { urls } from 'scenes/urls' -import api from 'lib/api' import './NotebookNodeQuery.scss' import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' const DEFAULT_QUERY: QuerySchema = { kind: NodeKind.DataTableNode, @@ -31,6 +31,31 @@ const Component = (props: NotebookNodeViewProps): J const nodeLogic = useMountedLogic(notebookNodeLogic) const { expanded } = useValues(nodeLogic) + useEffect(() => { + let title = 'Query' + + if (query.kind === NodeKind.DataTableNode) { + if (query.source.kind) { + title = query.source.kind.replace('Node', '').replace('Query', '') + } else { + title = 'Data exploration' + } + } + if (query.kind === NodeKind.InsightVizNode) { + if (query.source.kind) { + title = query.source.kind.replace('Node', '').replace('Query', '') + } else { + title = 'Insight' + } + } + if (query.kind === NodeKind.SavedInsightNode) { + const logic = insightLogic.findMounted({ dashboardItemId: query.shortId }) + title = (logic?.values.insight.name || logic?.values.insight.derived_name) ?? 'Saved Insight' + } + + props.updateAttributes({ title: title }) + }, [query]) + const modifiedQuery = useMemo(() => { const modifiedQuery = { ...query, full: false } @@ -158,29 +183,7 @@ export const Settings = ({ export const NotebookNodeQuery = createPostHogWidgetNode({ nodeType: NotebookNodeType.Query, - title: async (attributes) => { - const query = attributes.query - let title = 'HogQL' - if (NodeKind.SavedInsightNode === query.kind) { - const response = await api.insights.loadInsight(query.shortId) - title = response.results[0].name?.length - ? response.results[0].name - : response.results[0].derived_name || 'Saved insight' - } else if (NodeKind.DataTableNode === query.kind) { - if (query.source.kind) { - title = query.source.kind.replace('Node', '').replace('Query', '') - } else { - title = 'Data exploration' - } - } else if (NodeKind.InsightVizNode === query.kind) { - if (query.source.kind) { - title = query.source.kind.replace('Node', '').replace('Query', '') - } else { - title = 'Insight' - } - } - return Promise.resolve(title) - }, + defaultTitle: 'Query', Component, heightEstimate: 500, minHeight: 200, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx index 5004ba492124a..338d958ffc199 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx @@ -77,7 +77,7 @@ type NotebookNodeRecordingAttributes = { export const NotebookNodeRecording = createPostHogWidgetNode({ nodeType: NotebookNodeType.Recording, - title: 'Session replay', + defaultTitle: 'Session replay', Component, heightEstimate: HEIGHT, minHeight: MIN_HEIGHT, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx index d0b0cf87742b5..c53f655d9ea6e 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx @@ -13,7 +13,7 @@ import { StatusTag } from 'scenes/surveys/Surveys' import { SurveyResult } from 'scenes/surveys/SurveyView' import { SurveyAppearance } from 'scenes/surveys/SurveyAppearance' import { SurveyReleaseSummary } from 'scenes/surveys/Survey' -import api from 'lib/api' +import { useEffect } from 'react' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes @@ -21,6 +21,10 @@ const Component = (props: NotebookNodeViewProps): const { expanded, nextNode } = useValues(notebookNodeLogic) const { insertAfter } = useActions(notebookNodeLogic) + useEffect(() => { + props.updateAttributes({ title: survey.name ? `Survey: ${survey.name}` : 'Survey' }) + }, [survey.name]) + return (
@@ -115,17 +119,7 @@ type NotebookNodeSurveyAttributes = { export const NotebookNodeSurvey = createPostHogWidgetNode({ nodeType: NotebookNodeType.Survey, - title: async (attributes) => { - const mountedLogic = surveyLogic.findMounted({ id: attributes.id }) - let title = mountedLogic?.values.survey.name || null - if (title === null) { - const retrievedSurvey: Survey = await api.surveys.get(attributes.id) - if (retrievedSurvey) { - title = retrievedSurvey.name - } - } - return title ? `Survey: ${title}` : 'Survey' - }, + defaultTitle: 'Survey', Component, heightEstimate: '3rem', href: (attrs) => urls.survey(attrs.id), diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index 4d5adb4470745..d20b3e52765d6 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -33,23 +33,11 @@ export type NotebookNodeLogicProps = { nodeType: NotebookNodeType notebookLogic: BuiltLogic getPos: () => number - title: string | ((attributes: CustomNotebookNodeAttributes) => Promise) resizeable: boolean | ((attributes: CustomNotebookNodeAttributes) => boolean) widgets: NotebookNodeWidget[] startExpanded: boolean } & NotebookNodeAttributeProperties -async function renderTitle( - title: NotebookNodeLogicProps['title'], - attrs: NotebookNodeLogicProps['attributes'] -): Promise { - if (typeof attrs.title === 'string' && attrs.title.length > 0) { - return attrs.title - } - - return title instanceof Function ? await title(attrs) : title -} - const computeResizeable = ( resizeable: NotebookNodeLogicProps['resizeable'], attrs: NotebookNodeLogicProps['attributes'] @@ -61,7 +49,6 @@ export const notebookNodeLogic = kea([ key(({ nodeId }) => nodeId || 'no-node-id-set'), actions({ setExpanded: (expanded: boolean) => ({ expanded }), - setTitle: (title: string) => ({ title }), setResizeable: (resizeable: boolean) => ({ resizeable }), insertAfter: (content: JSONContent) => ({ content }), insertAfterLastNodeOfType: (nodeType: string, content: JSONContent) => ({ content, nodeType }), @@ -87,12 +74,6 @@ export const notebookNodeLogic = kea([ setExpanded: (_, { expanded }) => expanded, }, ], - title: [ - '', - { - setTitle: (_, { title }) => title, - }, - ], resizeable: [ false, { @@ -170,11 +151,8 @@ export const notebookNodeLogic = kea([ afterMount(async (logic) => { logic.props.notebookLogic.actions.registerNodeLogic(logic as any) - const renderedTitle = await renderTitle(logic.props.title, logic.props.attributes) - logic.actions.setTitle(renderedTitle) const resizeable = computeResizeable(logic.props.resizeable, logic.props.attributes) logic.actions.setResizeable(resizeable) - logic.actions.updateAttributes({ title: renderedTitle }) }), beforeUnmount((logic) => { diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx index c4becf3bd6d23..d3a96d59b23eb 100644 --- a/frontend/src/scenes/notebooks/Nodes/utils.tsx +++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx @@ -129,8 +129,8 @@ export function useSyncedAttributes( }), {} ) - - props.updateAttributes(stringifiedAttrs) + // NOTE: queueMicrotask protects us from TipTap's flushSync calls, ensuring we never modify the state whilst the flush is happening + queueMicrotask(() => props.updateAttributes(stringifiedAttrs)) }, [props.updateAttributes] ) diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index ed78f61d20f89..2e787c310e52c 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -25,8 +25,8 @@ export type CustomNotebookNodeAttributes = Record export type NotebookNodeAttributes = T & { nodeId: string - title: string | ((attributes: T) => Promise) height?: string | number + title: string } // NOTE: Pushes users to use the parsed "attributes" instead