Skip to content

Commit

Permalink
feat(hogql): HogQLQuery node and data table support (#14265)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored Feb 22, 2023
1 parent 1d7afd3 commit 1456301
Show file tree
Hide file tree
Showing 24 changed files with 486 additions and 62 deletions.
2 changes: 1 addition & 1 deletion frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function LemonTable<T extends Record<string, any>>({
)

const columnGroups = (
'children' in rawColumns[0]
rawColumns.length > 0 && 'children' in rawColumns[0]
? rawColumns
: [
{
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/queries/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EventsNode,
EventsQuery,
FunnelsQuery,
HogQLQuery,
LegacyQuery,
LifecycleQuery,
Node,
Expand Down Expand Up @@ -290,6 +291,25 @@ const TimeToSeeDataWaterfall: TimeToSeeDataWaterfallNode = {
},
}

const HogQL: HogQLQuery = {
kind: NodeKind.HogQLQuery,
query:
' select event,\n' +
' properties.$geoip_country_name as `Country Name`,\n' +
' count() as `Event count`\n' +
' from events\n' +
' group by event,\n' +
' properties.$geoip_country_name\n' +
' order by count() desc\n' +
' limit 100',
}

const HogQLTable: DataTableNode = {
kind: NodeKind.DataTableNode,
full: true,
source: HogQL,
}

export const examples: Record<string, Node> = {
Events,
EventsTable,
Expand All @@ -310,6 +330,8 @@ export const examples: Record<string, Node> = {
TimeToSeeDataSessions,
TimeToSeeDataWaterfall,
TimeToSeeDataJSON,
HogQL,
HogQLTable,
}

export const stringifiedExamples: Record<string, string> = Object.fromEntries(
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/queries/nodes/DataNode/DataNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let uniqueNode = 0
export function DataNode(props: DataNodeProps): JSX.Element {
const [key] = useState(() => `DataNode.${uniqueNode++}`)
const logic = dataNodeLogic({ ...props, key })
const { response, responseLoading } = useValues(logic)
const { response, responseLoading, responseErrorObject } = useValues(logic)

return (
<div className="relative">
Expand All @@ -36,7 +36,7 @@ export function DataNode(props: DataNodeProps): JSX.Element {
theme="vs-light"
className="border"
language={'json'}
value={JSON.stringify(response, null, 2)}
value={JSON.stringify(response ?? responseErrorObject, null, 2)}
height={Math.max(height, 300)}
/>
)}
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/queries/nodes/DataNode/dataNodeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,24 @@ export const dataNodeLogic = kea<dataNodeLogicType>([
// Clear the response if a failure to avoid showing inconsistencies in the UI
loadDataFailure: () => null,
},
responseErrorObject: [
null as Record<string, any> | null,
{
loadData: () => null,
loadDataFailure: (_, { errorObject }) => errorObject,
loadDataSuccess: () => null,
},
],
responseError: [
null as string | null,
{
loadData: () => null,
loadDataFailure: () => 'Error loading data',
loadDataFailure: (_, { error, errorObject }) => {
if (errorObject && 'error' in errorObject) {
return errorObject.error
}
return error ?? 'Error loading data'
},
loadDataSuccess: () => null,
},
],
Expand Down
37 changes: 29 additions & 8 deletions frontend/src/queries/nodes/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './DataTable.scss'
import { DataTableNode, EventsNode, EventsQuery, Node, PersonsNode, QueryContext } from '~/queries/schema'
import { DataTableNode, EventsNode, EventsQuery, HogQLQuery, Node, PersonsNode, QueryContext } from '~/queries/schema'
import { useCallback, useState } from 'react'
import { BindLogic, useValues } from 'kea'
import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic'
Expand All @@ -21,7 +21,7 @@ import { EventBufferNotice } from 'scenes/events/EventBufferNotice'
import clsx from 'clsx'
import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal'
import { InlineEditorButton } from '~/queries/nodes/Node/InlineEditorButton'
import { isEventsQuery, isHogQlAggregation, isPersonsNode, taxonomicFilterToHogQl } from '~/queries/utils'
import { isEventsQuery, isHogQlAggregation, isHogQLQuery, isPersonsNode, taxonomicFilterToHogQl } from '~/queries/utils'
import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters'
import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch'
import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal'
Expand All @@ -34,6 +34,7 @@ import { extractExpressionComment, removeExpressionComment } from '~/queries/nod
import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates'
import { EventType } from '~/types'
import { SavedQueries } from '~/queries/nodes/DataTable/SavedQueries'
import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor'

interface DataTableProps {
query: DataTableNode
Expand Down Expand Up @@ -79,6 +80,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele
showSearch,
showEventFilter,
showPropertyFilter,
showHogQLEditor,
showReload,
showExport,
showElapsedTime,
Expand All @@ -89,22 +91,23 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele
} = queryWithDefaults

const actionsColumnShown = showActions && isEventsQuery(query.source) && columnsInResponse?.includes('*')
const columnsInLemonTable = isHogQLQuery(query.source) ? columnsInResponse ?? columnsInQuery : columnsInQuery
const lemonColumns: LemonTableColumn<DataTableRow, any>[] = [
...columnsInQuery.map((key, index) => ({
...columnsInLemonTable.map((key, index) => ({
dataIndex: key as any,
...renderColumnMeta(key, query, context),
render: function RenderDataTableColumn(_: any, { result, label }: DataTableRow) {
if (label) {
if (index === (expandable ? 1 : 0)) {
return {
children: label,
props: { colSpan: columnsInQuery.length + (actionsColumnShown ? 1 : 0) },
props: { colSpan: columnsInLemonTable.length + (actionsColumnShown ? 1 : 0) },
}
} else {
return { props: { colSpan: 0 } }
}
} else if (result) {
if (isEventsQuery(query.source)) {
if (isEventsQuery(query.source) || isHogQLQuery(query.source)) {
return renderColumn(key, result[index], result, query, setQuery, context)
}
return renderColumn(key, result[key], result, query, setQuery, context)
Expand Down Expand Up @@ -292,7 +295,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele
].filter((column) => !query.hiddenColumns?.includes(column.dataIndex) && column.dataIndex !== '*')

const setQuerySource = useCallback(
(source: EventsNode | EventsQuery | PersonsNode) => setQuery?.({ ...query, source }),
(source: EventsNode | EventsQuery | PersonsNode | HogQLQuery) => setQuery?.({ ...query, source }),
[setQuery]
)

Expand Down Expand Up @@ -337,7 +340,10 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele
return (
<BindLogic logic={dataTableLogic} props={dataTableLogicProps}>
<BindLogic logic={dataNodeLogic} props={dataNodeLogicProps}>
<div className="relative w-full h-full">
<div className="relative w-full h-full space-y-4">
{showHogQLEditor && isHogQLQuery(query.source) ? (
<HogQLQueryEditor query={query.source} setQuery={setQuerySource} />
) : null}
{showFirstRow && (
<div className="flex gap-4 items-center">
{firstRowLeft}
Expand Down Expand Up @@ -398,7 +404,22 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele
}}
sorting={null}
useURLForSorting={false}
emptyState={responseError ? <InsightErrorState /> : <InsightEmptyState />}
emptyState={
responseError ? (
isHogQLQuery(query.source) ? (
<InsightErrorState
excludeDetail
title={
response && 'error' in response ? (response as any).error : responseError
}
/>
) : (
<InsightErrorState />
)
) : (
<InsightEmptyState />
)
}
expandable={
expandable && isEventsQuery(query.source) && columnsInResponse?.includes('*')
? {
Expand Down
12 changes: 5 additions & 7 deletions frontend/src/queries/nodes/DataTable/dataTableLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ export const dataTableLogic = kea<dataTableLogicType>([
columnsInResponse: [
(s) => [s.response],
(response: AnyDataNode['response']): string[] | null =>
response &&
'columns' in response &&
Array.isArray(response.columns) &&
!response.columns.find((c) => typeof c !== 'string')
? (response?.columns as string[])
: null,
response && 'columns' in response && Array.isArray(response.columns) ? response?.columns : null,
],
dataTableRows: [
(s) => [s.sourceKind, s.orderBy, s.response, s.columnsInQuery, s.columnsInResponse],
Expand Down Expand Up @@ -143,10 +138,13 @@ export const dataTableLogic = kea<dataTableLogicType>([
showDateRange: query.showDateRange ?? showIfFull,
showExport: query.showExport ?? showIfFull,
showReload: query.showReload ?? showIfFull,
showElapsedTime: query.showElapsedTime ?? (flagQueryRunningTimeEnabled ? showIfFull : false),
showElapsedTime:
query.showElapsedTime ??
(flagQueryRunningTimeEnabled || source.kind === NodeKind.HogQLQuery ? showIfFull : false),
showColumnConfigurator: query.showColumnConfigurator ?? showIfFull,
showSavedQueries: query.showSavedQueries ?? false,
showEventsBufferWarning: query.showEventsBufferWarning ?? showIfFull,
showHogQLEditor: query.showHogQLEditor ?? showIfFull,
allowSorting: query.allowSorting ?? true,
}),
}
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useActions, useValues } from 'kea'
import { HogQLQuery } from '~/queries/schema'
import { useState } from 'react'
import { hogQLQueryEditorLogic } from './hogQLQueryEditorLogic'
import MonacoEditor from '@monaco-editor/react'
import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer'
import { LemonButton } from 'lib/lemon-ui/LemonButton'

export interface HogQLQueryEditorProps {
query: HogQLQuery
setQuery?: (query: HogQLQuery) => void
}

let uniqueNode = 0
export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element {
const [key] = useState(() => uniqueNode++)
const hogQLQueryEditorLogicProps = { query: props.query, setQuery: props.setQuery, key }
const { queryInput } = useValues(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps))
const { setQueryInput, saveQuery } = useActions(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps))

return (
<div className={'flex flex-col p-2 bg-border space-y-2 resize-y overflow-auto h-80 w-full'}>
<div className="flex-1">
<AutoSizer disableWidth>
{({ height }) => (
<MonacoEditor
theme="vs-light"
className="border"
language="mysql"
value={queryInput}
onChange={(v) => setQueryInput(v ?? '')}
height={height}
options={{
minimap: {
enabled: false,
},
wordWrap: 'on',
}}
/>
)}
</AutoSizer>
</div>
<LemonButton
onClick={saveQuery}
type="primary"
status={'muted-alt'}
disabledReason={!props.setQuery ? 'No permission to update' : undefined}
fullWidth
center
>
{!props.setQuery ? 'No permission to update' : 'Update'}
</LemonButton>
</div>
)
}
45 changes: 45 additions & 0 deletions frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { actions, kea, key, listeners, path, props, propsChanged, reducers } from 'kea'
import { format } from 'sql-formatter'
import { HogQLQuery } from '~/queries/schema'

import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType'

function formatSQL(sql: string): string {
return format(sql, {
language: 'mysql',
tabWidth: 2,
keywordCase: 'preserve',
linesBetweenQueries: 2,
indentStyle: 'tabularRight',
})
}
export interface HogQLQueryEditorLogicProps {
key: number
query: HogQLQuery
setQuery?: (query: HogQLQuery) => void
}

export const hogQLQueryEditorLogic = kea<hogQLQueryEditorLogicType>([
path(['queries', 'nodes', 'HogQLQuery', 'hogQLQueryEditorLogic']),
props({} as HogQLQueryEditorLogicProps),
key((props) => props.key),
propsChanged(({ actions, props }, oldProps) => {
if (props.query.query !== oldProps.query.query) {
actions.setQueryInput(formatSQL(props.query.query))
}
}),
actions({
saveQuery: true,
setQueryInput: (queryInput: string) => ({ queryInput }),
}),
reducers(({ props }) => ({
queryInput: [formatSQL(props.query.query), { setQueryInput: (_, { queryInput }) => queryInput }],
})),
listeners(({ actions, props, values }) => ({
saveQuery: () => {
const formattedQuery = formatSQL(values.queryInput)
actions.setQueryInput(formattedQuery)
props.setQuery?.({ ...props.query, query: formattedQuery })
},
})),
])
5 changes: 5 additions & 0 deletions frontend/src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isRecentPerformancePageViewNode,
isDataTableNode,
isTimeToSeeDataSessionsNode,
isHogQLQuery,
isInsightVizNode,
} from './utils'
import api, { ApiMethodOptions } from 'lib/api'
Expand Down Expand Up @@ -47,6 +48,8 @@ export function queryExportContext<N extends DataNode = DataNode>(
after: now().subtract(EVENTS_DAYS_FIRST_FETCH, 'day').toISOString(),
},
}
} else if (isHogQLQuery(query)) {
return { path: api.queryURL(), method: 'POST', body: query }
} else if (isPersonsNode(query)) {
return { path: getPersonsEndpoint(query) }
} else if (isInsightQueryNode(query)) {
Expand Down Expand Up @@ -118,6 +121,8 @@ export async function query<N extends DataNode = DataNode>(
}
}
return await api.query({ after: now().subtract(1, 'year').toISOString(), ...query }, methodOptions)
} else if (isHogQLQuery(query)) {
return api.query(query, methodOptions)
} else if (isPersonsNode(query)) {
return await api.get(getPersonsEndpoint(query), methodOptions)
} else if (isInsightQueryNode(query)) {
Expand Down
Loading

0 comments on commit 1456301

Please sign in to comment.