Skip to content

Commit

Permalink
feat: CDP Templates (#22896)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Jun 13, 2024
1 parent 86080cf commit 23147c0
Show file tree
Hide file tree
Showing 39 changed files with 1,166 additions and 213 deletions.
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"BILLING_SERVICE_URL": "https://billing.dev.posthog.dev",
"CLOUD_DEPLOYMENT": "dev"
},
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"python": "${workspaceFolder}/env/bin/python",
"cwd": "${workspaceFolder}",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 21 additions & 3 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import {
FeatureFlagType,
Group,
GroupListParams,
HogFunctionIconResponse,
HogFunctionTemplateType,
HogFunctionType,
InsightModel,
IntegrationType,
Expand Down Expand Up @@ -330,6 +332,14 @@ class ApiRequest {
return this.hogFunctions(teamId).addPathComponent(id)
}

public hogFunctionTemplates(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('hog_function_templates')
}

public hogFunctionTemplate(id: HogFunctionTemplateType['id'], teamId?: TeamType['id']): ApiRequest {
return this.hogFunctionTemplates(teamId).addPathComponent(id)
}

// # Actions
public actions(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('actions')
Expand Down Expand Up @@ -1645,9 +1655,6 @@ const api = {
},

hogFunctions: {
async listTemplates(): Promise<PaginatedResponse<HogFunctionType>> {
return await new ApiRequest().hogFunctions().get()
},
async list(): Promise<PaginatedResponse<HogFunctionType>> {
return await new ApiRequest().hogFunctions().get()
},
Expand All @@ -1666,6 +1673,17 @@ const api = {
): Promise<PaginatedResponse<LogEntry>> {
return await new ApiRequest().hogFunction(id).withAction('logs').withQueryString(params).get()
},

async listTemplates(): Promise<PaginatedResponse<HogFunctionTemplateType>> {
return await new ApiRequest().hogFunctionTemplates().get()
},
async getTemplate(id: HogFunctionTemplateType['id']): Promise<HogFunctionTemplateType> {
return await new ApiRequest().hogFunctionTemplate(id).get()
},

async listIcons(params: { query?: string } = {}): Promise<HogFunctionIconResponse[]> {
return await new ApiRequest().hogFunctions().withAction('icons').withQueryString(params).get()
},
},

annotations: {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/scenes/pipeline/Destinations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AvailableFeature, PipelineNodeTab, PipelineStage, ProductKey } from '~/

import { AppMetricSparkLine } from './AppMetricSparkLine'
import { pipelineDestinationsLogic } from './destinationsLogic'
import { HogFunctionIcon } from './hogfunctions/HogFunctionIcon'
import { NewButton } from './NewButton'
import { pipelineAccessLogic } from './pipelineAccessLogic'
import { Destination } from './types'
Expand Down Expand Up @@ -60,6 +61,8 @@ export function DestinationsTable({ inOverview = false }: { inOverview?: boolean
switch (destination.backend) {
case 'plugin':
return <RenderApp plugin={destination.plugin} />
case 'hog_function':
return <HogFunctionIcon src={destination.hog_function.icon_url} size="small" />
case 'batch_export':
return <RenderBatchExportIcon type={destination.service.type} />
default:
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/scenes/pipeline/PipelineNodeNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AvailableFeature, BatchExportService, HogFunctionTemplateType, Pipeline

import { pipelineDestinationsLogic } from './destinationsLogic'
import { frontendAppsLogic } from './frontendAppsLogic'
import { HogFunctionIcon } from './hogfunctions/HogFunctionIcon'
import { PipelineHogFunctionConfiguration } from './hogfunctions/PipelineHogFunctionConfiguration'
import { PipelineBatchExportConfiguration } from './PipelineBatchExportConfiguration'
import { PIPELINE_TAB_TO_NODE_STAGE } from './PipelineNode'
Expand Down Expand Up @@ -86,7 +87,7 @@ function convertHogFunctionToTableEntry(hogFunction: HogFunctionTemplateType): T
id: `hog-${hogFunction.id}`, // TODO: This weird identifier thing isn't great
name: hogFunction.name,
description: hogFunction.description,
icon: <span>🦔</span>,
icon: <HogFunctionIcon size="small" src={hogFunction.icon_url} />,
}
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/pipeline/destinationsLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from '~/types'

import type { pipelineDestinationsLogicType } from './destinationsLogicType'
import { HOG_FUNCTION_TEMPLATES } from './hogfunctions/templates/hog-templates'
import { pipelineAccessLogic } from './pipelineAccessLogic'
import { BatchExportDestination, convertToPipelineNode, Destination, PipelineBackend } from './types'
import { captureBatchExportEvent, capturePluginEvent, loadPluginsFromUrl } from './utils'
Expand Down Expand Up @@ -124,7 +123,8 @@ export const pipelineDestinationsLogic = kea<pipelineDestinationsLogicType>([
{} as Record<string, HogFunctionTemplateType>,
{
loadHogFunctionTemplates: async () => {
return HOG_FUNCTION_TEMPLATES.reduce((acc, template) => {
const templates = await api.hogFunctions.listTemplates()
return templates.results.reduce((acc, template) => {
acc[template.id] = template
return acc
}, {} as Record<string, HogFunctionTemplateType>)
Expand Down
156 changes: 156 additions & 0 deletions frontend/src/scenes/pipeline/hogfunctions/HogFunctionIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { LemonButton, LemonFileInput, LemonInput, LemonSkeleton, lemonToast, Popover, Spinner } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { IconUploadFile } from 'lib/lemon-ui/icons'

import { hogFunctionIconLogic, HogFunctionIconLogicProps } from './hogFunctionIconLogic'

const fileToBase64 = (file?: File): Promise<string> => {
return new Promise((resolve) => {
if (!file) {
return
}

const reader = new FileReader()

reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

// Set the dimensions at the wanted size.
const wantedWidth = 128
const wantedHeight = 128
canvas.width = wantedWidth
canvas.height = wantedHeight

// Resize the image with the canvas method drawImage();
ctx!.drawImage(img, 0, 0, wantedWidth, wantedHeight)

const dataURI = canvas.toDataURL()

resolve(dataURI)
}
img.src = e.target?.result as string
}

reader.readAsDataURL(file)
})
}

export function HogFunctionIconEditable({
size = 'medium',
...props
}: HogFunctionIconLogicProps & { size?: 'small' | 'medium' | 'large' }): JSX.Element {
const { possibleIconsLoading, showPopover, possibleIcons, searchTerm } = useValues(hogFunctionIconLogic(props))
const { setShowPopover, setSearchTerm } = useActions(hogFunctionIconLogic(props))

const content = (
<span
className={clsx('relative cursor-pointer', {
'w-8 h-8': size === 'small',
'w-10 h-10': size === 'medium',
'w-12 h-12': size === 'large',
})}
onClick={() => setShowPopover(!showPopover)}
>
{possibleIconsLoading ? <Spinner className="absolute -top-1 -right-1" /> : null}
<HogFunctionIcon size={size} src={props.src} />
</span>
)

return props.onChange ? (
<Popover
showArrow
visible={showPopover}
onClickOutside={() => setShowPopover(false)}
overlay={
<div className="p-1 w-100 space-y-2">
<div className="flex items-center gap-2 justify-between">
<h2 className="m-0">Choose an icon</h2>

<LemonFileInput
multiple={false}
accept={'image/*'}
showUploadedFiles={false}
onChange={(files) => {
void fileToBase64(files[0])
.then((dataURI) => {
props.onChange?.(dataURI)
})
.catch(() => {
lemonToast.error('Error uploading image')
})
}}
callToAction={
<LemonButton size="small" type="secondary" icon={<IconUploadFile />}>
Upload image
</LemonButton>
}
/>
</div>

<LemonInput
size="small"
type="search"
placeholder="Search for company logos"
fullWidth
value={searchTerm ?? ''}
onChange={setSearchTerm}
prefix={possibleIconsLoading ? <Spinner /> : undefined}
/>

<div className="flex flex-wrap gap-2">
{possibleIcons?.map((icon) => (
<span
key={icon.id}
className="w-14 h-14 cursor-pointer"
onClick={() => {
const nonTempUrl = icon.url.replace('&temp=true', '')
props.onChange?.(nonTempUrl)
setShowPopover(false)
}}
>
<img
src={icon.url}
title={icon.name}
className="w-full h-full rounded overflow-hidden"
/>
</span>
)) ??
(possibleIconsLoading ? (
<LemonSkeleton className="w-14 h-14" repeat={4} />
) : (
'No icons found'
))}
</div>
</div>
}
>
{content}
</Popover>
) : (
content
)
}

export function HogFunctionIcon({
src,
size = 'medium',
}: {
src?: string
size?: 'small' | 'medium' | 'large'
}): JSX.Element {
return (
<span
className={clsx('flex items-center justify-center', {
'w-8 h-8 text-2xl': size === 'small',
'w-10 h-10 text-4xl': size === 'medium',
'w-12 h-12 text-6xl': size === 'large',
})}
>
{src ? <img className="w-full h-full rounded overflow-hidden" src={src} /> : <span>🦔</span>}
</span>
)
}
Loading

0 comments on commit 23147c0

Please sign in to comment.