Skip to content

Commit

Permalink
feat(notebook): Add survey node (#17357)
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar authored Sep 11, 2023
1 parent 2cea440 commit 0fe269b
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 175 deletions.
142 changes: 142 additions & 0 deletions frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper'
import { FeatureFlagBasicType, NotebookNodeType, Survey, SurveyQuestionType } from '~/types'
import { BindLogic, useActions, useValues } from 'kea'
import { IconFlag, IconSurveys } from 'lib/lemon-ui/icons'
import { LemonButton, LemonDivider } from '@posthog/lemon-ui'
import { urls } from 'scenes/urls'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { notebookNodeLogic } from './notebookNodeLogic'
import { NotebookNodeViewProps } from '../Notebook/utils'
import { buildFlagContent } from './NotebookNodeFlag'
import { defaultSurveyAppearance, surveyLogic } from 'scenes/surveys/surveyLogic'
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'

const Component = (props: NotebookNodeViewProps<NotebookNodeSurveyAttributes>): JSX.Element => {
const { id } = props.node.attrs
const { survey, surveyLoading, hasTargetingFlag } = useValues(surveyLogic({ id }))
const { expanded, nextNode } = useValues(notebookNodeLogic)
const { insertAfter } = useActions(notebookNodeLogic)

return (
<div>
<BindLogic logic={surveyLogic} props={{ id }}>
<div className="flex items-center gap-2 p-3">
<IconSurveys className="text-lg" />
{surveyLoading ? (
<LemonSkeleton className="h-6 flex-1" />
) : (
<>
<span className="flex-1 font-semibold truncate">{survey.name}</span>
{/* survey has to exist in notebooks */}
<StatusTag survey={survey as Survey} />
</>
)}
</div>

{expanded ? (
<>
{survey.description && (
<>
<LemonDivider className="my-0" />
<span className="p-2">{survey.description}</span>
</>
)}
{!survey.start_date ? (
<>
<LemonDivider className="my-0" />
<div className="p-2">
<SurveyReleaseSummary id={id} survey={survey} hasTargetingFlag={hasTargetingFlag} />

<div className="w-full flex flex-col items-center">
<SurveyAppearance
type={survey.questions[0].type}
surveyQuestionItem={survey.questions[0]}
appearance={survey.appearance || defaultSurveyAppearance}
question={survey.questions[0].question}
description={survey.questions[0].description}
link={
survey.questions[0].type === SurveyQuestionType.Link
? survey.questions[0].link
: undefined
}
readOnly={true}
onAppearanceChange={() => {}}
/>
</div>
</div>
</>
) : (
<>
{/* show results when the survey is running */}
<LemonDivider className="my-0" />
<div className="p-2">
<SurveyResult disableEventsTable />
</div>
</>
)}
</>
) : null}

<LemonDivider className="my-0" />
<div className="p-2 mr-1 flex justify-end gap-2">
{survey.linked_flag && (
<LemonButton
type="secondary"
size="small"
icon={<IconFlag />}
onClick={(e) => {
e.stopPropagation()

if (nextNode?.type.name !== NotebookNodeType.FeatureFlag) {
insertAfter(buildFlagContent((survey.linked_flag as FeatureFlagBasicType).id))
}
}}
disabledReason={
nextNode?.type.name === NotebookNodeType.FeatureFlag &&
'Feature flag already exists below'
}
>
View Linked Flag
</LemonButton>
)}
</div>
</BindLogic>
</div>
)
}

type NotebookNodeSurveyAttributes = {
id: string
}

export const NotebookNodeSurvey = createPostHogWidgetNode<NotebookNodeSurveyAttributes>({
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'
},
Component,
heightEstimate: '3rem',
href: (attrs) => urls.survey(attrs.id),
resizeable: false,
attributes: {
id: {},
},
pasteOptions: {
find: urls.survey('') + '(.+)',
getAttributes: async (match) => {
return { id: match[1] }
},
},
})
2 changes: 2 additions & 0 deletions frontend/src/scenes/notebooks/Notebook/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { JSONContent, NotebookEditor, EditorFocusPosition, EditorRange, Node } f
import { SlashCommandsExtension } from './SlashCommands'
import { BacklinkCommandsExtension } from './BacklinkCommands'
import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature'
import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey'

const CustomDocument = ExtensionDocument.extend({
content: 'heading block*',
Expand Down Expand Up @@ -92,6 +93,7 @@ export function Editor({
NotebookNodeFlag,
NotebookNodeExperiment,
NotebookNodeEarlyAccessFeature,
NotebookNodeSurvey,
NotebookNodeImage,
SlashCommandsExtension,
BacklinkCommandsExtension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const fromNodeTypeToLabel: Omit<Record<NotebookNodeType, string>, Noteboo
[NotebookNodeType.FeatureFlagCodeExample]: 'Feature flag Code Examples',
[NotebookNodeType.Experiment]: 'Experiments',
[NotebookNodeType.EarlyAccessFeature]: 'Early Access Features',
[NotebookNodeType.Survey]: 'Surveys',
[NotebookNodeType.Image]: 'Images',
[NotebookNodeType.Insight]: 'Insights',
[NotebookNodeType.Person]: 'Persons',
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/scenes/surveys/Survey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function SurveyComponent({ id }: { id?: string } = {}): JSX.Element {

export function SurveyForm({ id }: { id: string }): JSX.Element {
const { survey, surveyLoading, isEditingSurvey, hasTargetingFlag } = useValues(surveyLogic)
const { loadSurvey, editingSurvey, setHasTargetingFlag } = useActions(surveyLogic)
const { loadSurvey, editingSurvey, setSurveyValue } = useActions(surveyLogic)
const { featureFlags } = useValues(enabledFeaturesLogic)

return (
Expand Down Expand Up @@ -374,7 +374,9 @@ export function SurveyForm({ id }: { id: string }): JSX.Element {
<LemonButton
type="secondary"
className="w-max"
onClick={() => setHasTargetingFlag(true)}
onClick={() => {
setSurveyValue('targeting_flag_filters', { groups: [] })
}}
>
Add user targeting
</LemonButton>
Expand All @@ -389,7 +391,10 @@ export function SurveyForm({ id }: { id: string }): JSX.Element {
type="secondary"
status="danger"
className="w-max"
onClick={() => setHasTargetingFlag(false)}
onClick={() => {
setSurveyValue('targeting_flag_filters', undefined)
setSurveyValue('targeting_flag', null)
}}
>
Remove all user properties
</LemonButton>
Expand Down
92 changes: 40 additions & 52 deletions frontend/src/scenes/surveys/SurveyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,9 @@ import { SurveyQuestionType, SurveyType } from '~/types'
import { SurveyAPIEditor } from './SurveyAPIEditor'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { IconOpenInNew } from 'lib/lemon-ui/icons'
import { NodeKind } from '~/queries/schema'

export function SurveyView({ id }: { id: string }): JSX.Element {
const {
survey,
dataTableQuery,
surveyLoading,
surveyPlugin,
surveyMetricsQueries,
surveyDataVizQuery,
showSurveyAppWarning,
} = useValues(surveyLogic)
const { survey, surveyLoading, surveyPlugin, showSurveyAppWarning } = useValues(surveyLogic)
// TODO: survey results logic
// const { surveyImpressionsCount, surveyStartedCount, surveyCompletedCount } = useValues(surveyResultsLogic)
const { editingSurvey, updateSurvey, launchSurvey, stopSurvey, archiveSurvey, resumeSurvey } =
Expand Down Expand Up @@ -134,48 +125,7 @@ export function SurveyView({ id }: { id: string }): JSX.Element {
? {
content: (
<div>
{surveyMetricsQueries && (
<div className="flex flex-row gap-4 mb-4">
<div className="flex-1">
<Query query={surveyMetricsQueries.surveysShown} />
</div>
<div className="flex-1">
<Query query={surveyMetricsQueries.surveysDismissed} />
</div>
</div>
)}
{survey.questions[0].type === SurveyQuestionType.Rating && (
<div className="mb-4">
<Query query={surveyDataVizQuery} />
</div>
)}
{(survey.questions[0].type === SurveyQuestionType.SingleChoice ||
survey.questions[0].type === SurveyQuestionType.MultipleChoice) && (
<div className="mb-4">
{survey.questions[0].type === SurveyQuestionType.SingleChoice ? (
<Query
query={{
kind: NodeKind.DataTableNode,
source: {
kind: NodeKind.HogQLQuery,
query: `select count(), properties.$survey_response as choice from events where event == 'survey sent' and properties.$survey_id == '${survey.id}' group by choice order by count() desc`,
},
}}
/>
) : (
<Query
query={{
kind: NodeKind.DataTableNode,
source: {
kind: NodeKind.HogQLQuery,
query: `select count(), arrayJoin(JSONExtractArrayRaw(properties, '$survey_response')) as choice from events where event == 'survey sent' and properties.$survey_id == '${survey.id}' group by choice order by count() desc`,
},
}}
/>
)}
</div>
)}
{surveyLoading ? <LemonSkeleton /> : <Query query={dataTableQuery} />}
<SurveyResult />
</div>
),
key: 'results',
Expand Down Expand Up @@ -305,6 +255,44 @@ export function SurveyView({ id }: { id: string }): JSX.Element {
)
}

export function SurveyResult({ disableEventsTable }: { disableEventsTable?: boolean }): JSX.Element {
const {
survey,
dataTableQuery,
surveyLoading,
surveyMetricsQueries,
surveyRatingQuery,
surveyMultipleChoiceQuery,
} = useValues(surveyLogic)

return (
<>
{surveyMetricsQueries && (
<div className="flex flex-row gap-4 mb-4">
<div className="flex-1">
<Query query={surveyMetricsQueries.surveysShown} />
</div>
<div className="flex-1">
<Query query={surveyMetricsQueries.surveysDismissed} />
</div>
</div>
)}
{survey.questions[0].type === SurveyQuestionType.Rating && (
<div className="mb-4">
<Query query={surveyRatingQuery} />
</div>
)}
{(survey.questions[0].type === SurveyQuestionType.SingleChoice ||
survey.questions[0].type === SurveyQuestionType.MultipleChoice) && (
<div className="mb-4">
<Query query={surveyMultipleChoiceQuery} />
</div>
)}
{!disableEventsTable && (surveyLoading ? <LemonSkeleton /> : <Query query={dataTableQuery} />)}
</>
)
}

const OPT_IN_SNIPPET = `posthog.init('YOUR_PROJECT_API_KEY', {
api_host: 'YOUR API HOST',
opt_in_site_apps: true // <--- Add this line
Expand Down
26 changes: 15 additions & 11 deletions frontend/src/scenes/surveys/Surveys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,7 @@ export function Surveys(): JSX.Element {
title: 'Status',
width: 100,
render: function Render(_, survey: Survey) {
const statusColors = {
running: 'success',
draft: 'default',
complete: 'completion',
} as Record<ProgressStatus, LemonTagType>
const status = getSurveyStatus(survey)
return (
<LemonTag type={statusColors[status]} style={{ fontWeight: 600 }}>
{status.toUpperCase()}
</LemonTag>
)
return <StatusTag survey={survey} />
},
},
{
Expand Down Expand Up @@ -243,3 +233,17 @@ export function Surveys(): JSX.Element {
</div>
)
}

export function StatusTag({ survey }: { survey: Survey }): JSX.Element {
const statusColors = {
running: 'success',
draft: 'default',
complete: 'completion',
} as Record<ProgressStatus, LemonTagType>
const status = getSurveyStatus(survey)
return (
<LemonTag type={statusColors[status]} style={{ fontWeight: 600 }}>
{status.toUpperCase()}
</LemonTag>
)
}
Loading

0 comments on commit 0fe269b

Please sign in to comment.