-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
106ba92
commit bf9c242
Showing
36 changed files
with
1,564 additions
and
354 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
188 changes: 188 additions & 0 deletions
188
frontend/src/scenes/pipeline/hogfunctions/HogFunctionTest.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import { TZLabel } from '@posthog/apps-common' | ||
import { IconInfo, IconX } from '@posthog/icons' | ||
import { LemonButton, LemonLabel, LemonSwitch, LemonTable, LemonTag, Tooltip } from '@posthog/lemon-ui' | ||
import clsx from 'clsx' | ||
import { useActions, useValues } from 'kea' | ||
import { Form } from 'kea-forms' | ||
import { CodeEditorResizeable } from 'lib/components/CodeEditors' | ||
import { LemonField } from 'lib/lemon-ui/LemonField' | ||
|
||
import { hogFunctionTestLogic, HogFunctionTestLogicProps } from './hogFunctionTestLogic' | ||
|
||
const HogFunctionTestEditor = ({ | ||
value, | ||
onChange, | ||
}: { | ||
value: string | ||
onChange?: (value?: string) => void | ||
}): JSX.Element => { | ||
return ( | ||
<CodeEditorResizeable | ||
language="json" | ||
value={value} | ||
height={300} | ||
onChange={onChange} | ||
options={{ | ||
lineNumbers: 'off', | ||
minimap: { | ||
enabled: false, | ||
}, | ||
quickSuggestions: { | ||
other: true, | ||
strings: true, | ||
}, | ||
suggest: { | ||
showWords: false, | ||
showFields: false, | ||
showKeywords: false, | ||
}, | ||
scrollbar: { | ||
vertical: 'hidden', | ||
verticalScrollbarSize: 0, | ||
}, | ||
}} | ||
/> | ||
) | ||
} | ||
|
||
export function HogFunctionTestPlaceholder(): JSX.Element { | ||
return ( | ||
<div className="border bg-accent-3000 rounded p-3 space-y-2"> | ||
<h2 className="flex-1 m-0">Testing</h2> | ||
<p>Save your configuration to enable testing</p> | ||
</div> | ||
) | ||
} | ||
|
||
export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element { | ||
const { isTestInvocationSubmitting, testResult, expanded } = useValues(hogFunctionTestLogic(props)) | ||
const { submitTestInvocation, setTestResult, toggleExpanded } = useActions(hogFunctionTestLogic(props)) | ||
|
||
return ( | ||
<Form logic={hogFunctionTestLogic} props={props} formKey="testInvocation" enableFormOnSubmit> | ||
<div className={clsx('border bg-bg-light rounded p-3 space-y-2', expanded && 'min-h-120')}> | ||
<div className="flex items-center gap-2 justify-end"> | ||
{!expanded ? ( | ||
<LemonButton className="flex-1" onClick={() => toggleExpanded()}> | ||
<h2 className="m-0">Testing</h2> | ||
</LemonButton> | ||
) : ( | ||
<h2 className="flex-1 m-0">Testing</h2> | ||
)} | ||
|
||
{expanded && ( | ||
<> | ||
{testResult ? ( | ||
<LemonButton | ||
type="primary" | ||
onClick={() => setTestResult(null)} | ||
loading={isTestInvocationSubmitting} | ||
> | ||
Clear test result | ||
</LemonButton> | ||
) : ( | ||
<> | ||
<LemonField name="mock_async_functions"> | ||
{({ value, onChange }) => ( | ||
<LemonSwitch | ||
bordered | ||
onChange={onChange} | ||
checked={value} | ||
label={ | ||
<Tooltip | ||
title={ | ||
<> | ||
When selected, async functions such as `fetch` will not | ||
actually be called but instead will be mocked out with | ||
the fetch content logged instead | ||
</> | ||
} | ||
> | ||
<span className="flex gap-2"> | ||
Mock out async functions | ||
<IconInfo className="text-lg" /> | ||
</span> | ||
</Tooltip> | ||
} | ||
/> | ||
)} | ||
</LemonField> | ||
<LemonButton | ||
type="primary" | ||
onClick={submitTestInvocation} | ||
loading={isTestInvocationSubmitting} | ||
> | ||
Test function | ||
</LemonButton> | ||
</> | ||
)} | ||
|
||
<LemonButton icon={<IconX />} onClick={() => toggleExpanded()} tooltip="Hide testing" /> | ||
</> | ||
)} | ||
</div> | ||
|
||
{expanded && ( | ||
<> | ||
{testResult ? ( | ||
<div className="space-y-2"> | ||
<div className="flex justify-between items-center"> | ||
<LemonLabel>Test invocation result </LemonLabel> | ||
<LemonTag type={testResult.status === 'success' ? 'success' : 'danger'}> | ||
{testResult.status} | ||
</LemonTag> | ||
</div> | ||
|
||
<LemonTable | ||
dataSource={testResult.logs ?? []} | ||
columns={[ | ||
{ | ||
title: 'Timestamp', | ||
key: 'timestamp', | ||
dataIndex: 'timestamp', | ||
render: (timestamp) => <TZLabel time={timestamp as string} />, | ||
width: 0, | ||
}, | ||
{ | ||
width: 100, | ||
title: 'Level', | ||
key: 'level', | ||
dataIndex: 'level', | ||
}, | ||
{ | ||
title: 'Message', | ||
key: 'message', | ||
dataIndex: 'message', | ||
render: (message) => <code className="whitespace-pre-wrap">{message}</code>, | ||
}, | ||
]} | ||
className="ph-no-capture" | ||
rowKey="timestamp" | ||
pagination={{ pageSize: 200, hideOnSinglePage: true }} | ||
/> | ||
</div> | ||
) : ( | ||
<div className="space-y-2"> | ||
<LemonField name="globals" label="Test invocation context"> | ||
{({ value, onChange }) => ( | ||
<> | ||
<div className="flex items-start justify-end"> | ||
<p className="flex-1"> | ||
The globals object is the context in which your function will be | ||
tested. It should contain all the data that your function will need | ||
to run | ||
</p> | ||
</div> | ||
|
||
<HogFunctionTestEditor value={value} onChange={onChange} /> | ||
</> | ||
)} | ||
</LemonField> | ||
</div> | ||
)} | ||
</> | ||
)} | ||
</div> | ||
</Form> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
frontend/src/scenes/pipeline/hogfunctions/hogFunctionTestLogic.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { lemonToast } from '@posthog/lemon-ui' | ||
import { actions, afterMount, connect, kea, key, path, props, reducers } from 'kea' | ||
import { forms } from 'kea-forms' | ||
import api from 'lib/api' | ||
import { tryJsonParse } from 'lib/utils' | ||
|
||
import { LogEntry } from '~/types' | ||
|
||
import type { hogFunctionTestLogicType } from './hogFunctionTestLogicType' | ||
import { pipelineHogFunctionConfigurationLogic, sanitizeConfiguration } from './pipelineHogFunctionConfigurationLogic' | ||
import { createExampleEvent } from './utils/event-conversion' | ||
|
||
export interface HogFunctionTestLogicProps { | ||
id: string | ||
} | ||
|
||
export type HogFunctionTestInvocationForm = { | ||
globals: string // HogFunctionInvocationGlobals | ||
mock_async_functions: boolean | ||
} | ||
|
||
export type HogFunctionTestInvocationResult = { | ||
status: 'success' | 'error' | ||
logs: LogEntry[] | ||
} | ||
|
||
export const hogFunctionTestLogic = kea<hogFunctionTestLogicType>([ | ||
props({} as HogFunctionTestLogicProps), | ||
key((props) => props.id), | ||
path((id) => ['scenes', 'pipeline', 'hogfunctions', 'hogFunctionTestLogic', id]), | ||
connect((props: HogFunctionTestLogicProps) => ({ | ||
values: [pipelineHogFunctionConfigurationLogic({ id: props.id }), ['configuration', 'configurationHasErrors']], | ||
actions: [pipelineHogFunctionConfigurationLogic({ id: props.id }), ['touchConfigurationField']], | ||
})), | ||
actions({ | ||
setTestResult: (result: HogFunctionTestInvocationResult | null) => ({ result }), | ||
toggleExpanded: (expanded?: boolean) => ({ expanded }), | ||
}), | ||
reducers({ | ||
expanded: [ | ||
false as boolean, | ||
{ | ||
toggleExpanded: (_, { expanded }) => (expanded === undefined ? !_ : expanded), | ||
}, | ||
], | ||
|
||
testResult: [ | ||
null as HogFunctionTestInvocationResult | null, | ||
{ | ||
setTestResult: (_, { result }) => result, | ||
}, | ||
], | ||
}), | ||
forms(({ props, actions, values }) => ({ | ||
testInvocation: { | ||
defaults: { | ||
mock_async_functions: true, | ||
} as HogFunctionTestInvocationForm, | ||
alwaysShowErrors: true, | ||
errors: ({ globals }) => { | ||
return { | ||
globals: !globals ? 'Required' : tryJsonParse(globals) ? undefined : 'Invalid JSON', | ||
} | ||
}, | ||
submit: async (data) => { | ||
// Submit the test invocation | ||
// Set the response somewhere | ||
|
||
if (values.configurationHasErrors) { | ||
lemonToast.error('Please fix the configuration errors before testing.') | ||
// TODO: How to get the form to show errors without submitting? | ||
return | ||
} | ||
|
||
const event = tryJsonParse(data.globals) | ||
const configuration = sanitizeConfiguration(values.configuration) | ||
|
||
try { | ||
const res = await api.hogFunctions.createTestInvocation(props.id, { | ||
event, | ||
mock_async_functions: data.mock_async_functions, | ||
configuration, | ||
}) | ||
|
||
actions.setTestResult(res) | ||
} catch (e) { | ||
lemonToast.error(`An unexpected serror occurred while trying to testing the function. ${e}`) | ||
} | ||
}, | ||
}, | ||
})), | ||
afterMount(({ actions }) => { | ||
actions.setTestInvocationValue('globals', JSON.stringify(createExampleEvent(), null, 2)) | ||
}), | ||
]) |
Oops, something went wrong.