diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx
index 06e51dc13a6a3..3ab54b0a7761b 100644
--- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx
+++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx
@@ -95,7 +95,9 @@ const kindToSortText = (kind: AutocompleteCompletionItem['kind'], label: string)
export interface HogQLQueryEditorProps {
query: HogQLQuery
setQuery?: (query: HogQLQuery) => void
+ onChange?: (query: string) => void
embedded?: boolean
+ editorFooter?: (hasErrors: boolean, errors: string | null, isValidView: boolean) => JSX.Element
}
let uniqueNode = 0
@@ -105,7 +107,14 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element {
null as [Monaco, importedEditor.IStandaloneCodeEditor] | null
)
const [monaco, editor] = monacoAndEditor ?? []
- const hogQLQueryEditorLogicProps = { query: props.query, setQuery: props.setQuery, key, editor, monaco }
+ const hogQLQueryEditorLogicProps = {
+ query: props.query,
+ setQuery: props.setQuery,
+ onChange: props.onChange,
+ key,
+ editor,
+ monaco,
+ }
const logic = hogQLQueryEditorLogic(hogQLQueryEditorLogicProps)
const { queryInput, hasErrors, error, prompt, aiAvailable, promptError, promptLoading, isValidView } =
useValues(logic)
@@ -357,62 +366,69 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element {
-
-
- {!props.setQuery ? 'No permission to update' : 'Update and run'}
-
-
- {featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE] ? (
-
- Save as View
-
- ) : null}
- {featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE] && (
-
}
- type="secondary"
- size="small"
- dropdown={{
- overlay: (
-
- Save a query as a view that can be referenced in another query. This is useful
- for modeling data and organizing large queries into readable chunks.{' '}
- More Info{' '}
-
- ),
- placement: 'right-start',
- fallbackPlacements: ['left-start'],
- actionable: true,
- closeParentPopoverOnClickInside: true,
- }}
- />
+ {props.editorFooter ? (
+ props.editorFooter(hasErrors, error, isValidView)
+ ) : (
+ <>
+
+
+ {!props.setQuery ? 'No permission to update' : 'Update and run'}
+
+
+ {featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE] ? (
+
+ Save as View
+
+ ) : null}
+ {featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE] && (
+
}
+ type="secondary"
+ size="small"
+ dropdown={{
+ overlay: (
+
+ Save a query as a view that can be referenced in another query. This is
+ useful for modeling data and organizing large queries into readable
+ chunks.{' '}
+ More Info{' '}
+
+ ),
+ placement: 'right-start',
+ fallbackPlacements: ['left-start'],
+ actionable: true,
+ closeParentPopoverOnClickInside: true,
+ }}
+ />
+ )}
+ >
)}
diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts
index 39e1da3ebc216..b100cd42b1b9d 100644
--- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts
+++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts
@@ -29,6 +29,7 @@ export interface HogQLQueryEditorLogicProps {
key: number
query: HogQLQuery
setQuery?: (query: HogQLQuery) => void
+ onChange?: (query: string) => void
monaco?: Monaco | null
editor?: editor.IStandaloneCodeEditor | null
}
@@ -139,6 +140,7 @@ export const hogQLQueryEditorLogic = kea([
}
actions.setIsValidView(response?.isValidView || false)
actions.setModelMarkers(markers)
+ props.onChange?.(queryInput)
},
draftFromPrompt: async () => {
if (!values.aiAvailable) {
diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx
index 26c90d15f1600..6d5969a45aee5 100644
--- a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx
+++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx
@@ -1,21 +1,14 @@
import { IconBrackets, IconDatabase } from '@posthog/icons'
-import { LemonButton, Link } from '@posthog/lemon-ui'
+import { Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { DatabaseTableTree, TreeItem } from 'lib/components/DatabaseTableTree/DatabaseTableTree'
-import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
-import { humanFriendlyDetailedTime } from 'lib/utils'
-import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable'
-import { urls } from 'scenes/urls'
-import { NodeKind } from '~/queries/schema'
-
-import { DataWarehouseRowType, DataWarehouseTableType } from '../types'
-import { viewLinkLogic } from '../viewLinkLogic'
import { ViewLinkModal } from '../ViewLinkModal'
import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic'
import SourceModal from './SourceModal'
+import { TableData } from './TableData'
export const DataWarehouseTables = (): JSX.Element => {
const {
@@ -24,52 +17,11 @@ export const DataWarehouseTables = (): JSX.Element => {
dataWarehouseLoading,
posthogTables,
savedQueriesFormatted,
- allTables,
selectedRow,
dataWarehouseSavedQueriesLoading,
} = useValues(dataWarehouseSceneLogic)
- const { toggleSourceModal, selectRow, deleteDataWarehouseSavedQuery, deleteDataWarehouseTable } =
- useActions(dataWarehouseSceneLogic)
+ const { toggleSourceModal, selectRow } = useActions(dataWarehouseSceneLogic)
const { featureFlags } = useValues(featureFlagLogic)
- const { toggleJoinTableModal, selectSourceTable } = useActions(viewLinkLogic)
-
- const deleteButton = (selectedRow: DataWarehouseTableType | null): JSX.Element => {
- if (!selectedRow) {
- return <>>
- }
-
- if (selectedRow.type === DataWarehouseRowType.View) {
- return (
- {
- deleteDataWarehouseSavedQuery(selectedRow.payload)
- }}
- >
- Delete
-
- )
- }
-
- if (selectedRow.type === DataWarehouseRowType.ExternalTable) {
- return (
- {
- deleteDataWarehouseTable(selectedRow.payload)
- }}
- >
- Delete
-
- )
- }
-
- if (selectedRow.type === DataWarehouseRowType.PostHogTable) {
- return <>>
- }
-
- return <>>
- }
const treeItems = (): TreeItem[] => {
const items: TreeItem[] = [
@@ -126,84 +78,7 @@ export const DataWarehouseTables = (): JSX.Element => {
- {selectedRow ? (
-
-
-
{selectedRow.name}
-
- {deleteButton(selectedRow)}
- {
- selectSourceTable(selectedRow.name)
- toggleJoinTableModal()
- }}
- >
- Add Join
-
- !table && !fields && !chain)
- .map(({ key }) => key)} FROM ${selectedRow.name} LIMIT 100`,
- },
- })
- )}
- >
- Query
-
-
-
- {selectedRow.type == DataWarehouseRowType.ExternalTable && (
-
- <>
- Last Synced At
-
- {selectedRow.payload.external_schema?.last_synced_at
- ? humanFriendlyDetailedTime(
- selectedRow.payload.external_schema?.last_synced_at,
- 'MMMM DD, YYYY',
- 'h:mm A'
- )
- : 'Not yet synced'}
-
- >
-
- <>
- Files URL pattern
- {selectedRow.payload.url_pattern}
- >
-
- <>
- File format
- {selectedRow.payload.format}
- >
-
- )}
-
-
- Columns
-
-
-
- ) : (
-
-
-
- )}
+
toggleSourceModal(false)} />
diff --git a/frontend/src/scenes/data-warehouse/external/TableData.tsx b/frontend/src/scenes/data-warehouse/external/TableData.tsx
new file mode 100644
index 0000000000000..81d6c141001ee
--- /dev/null
+++ b/frontend/src/scenes/data-warehouse/external/TableData.tsx
@@ -0,0 +1,212 @@
+import { LemonButton, Link } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
+import { humanFriendlyDetailedTime } from 'lib/utils'
+import { useEffect, useState } from 'react'
+import { DatabaseTable } from 'scenes/data-management/database/DatabaseTable'
+import { urls } from 'scenes/urls'
+
+import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor'
+import { HogQLQuery, NodeKind } from '~/queries/schema'
+import { DataWarehouseSavedQuery } from '~/types'
+
+import { DataWarehouseRowType, DataWarehouseTableType } from '../types'
+import { viewLinkLogic } from '../viewLinkLogic'
+import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic'
+
+export function TableData(): JSX.Element {
+ const {
+ allTables,
+ selectedRow: table,
+ isEditingSavedQuery,
+ dataWarehouseSavedQueriesLoading,
+ } = useValues(dataWarehouseSceneLogic)
+ const { toggleJoinTableModal, selectSourceTable } = useActions(viewLinkLogic)
+ const {
+ deleteDataWarehouseSavedQuery,
+ deleteDataWarehouseTable,
+ setIsEditingSavedQuery,
+ updateDataWarehouseSavedQuery,
+ } = useActions(dataWarehouseSceneLogic)
+ const [localQuery, setLocalQuery] = useState()
+
+ useEffect(() => {
+ if (table && 'query' in table.payload) {
+ setLocalQuery(table.payload.query)
+ }
+ }, [table])
+
+ const deleteButton = (selectedRow: DataWarehouseTableType | null): JSX.Element => {
+ if (!selectedRow) {
+ return <>>
+ }
+
+ if (selectedRow.type === DataWarehouseRowType.View) {
+ return (
+ {
+ deleteDataWarehouseSavedQuery(selectedRow.payload)
+ }}
+ >
+ Delete
+
+ )
+ }
+
+ if (selectedRow.type === DataWarehouseRowType.ExternalTable) {
+ return (
+ {
+ deleteDataWarehouseTable(selectedRow.payload)
+ }}
+ >
+ Delete
+
+ )
+ }
+
+ if (selectedRow.type === DataWarehouseRowType.PostHogTable) {
+ return <>>
+ }
+
+ return <>>
+ }
+
+ return table ? (
+
+
+
{table.name}
+ {isEditingSavedQuery ? (
+
setIsEditingSavedQuery(false)}>
+ Cancel
+
+ ) : (
+
+ {deleteButton(table)}
+ {
+ selectSourceTable(table.name)
+ toggleJoinTableModal()
+ }}
+ >
+ Add Join
+
+ !table && !fields && !chain)
+ .map(({ key }) => key)} FROM ${table.name} LIMIT 100`,
+ },
+ })
+ )}
+ >
+ Query
+
+ {'query' in table.payload && (
+ setIsEditingSavedQuery(true)}>
+ Edit
+
+ )}
+
+ )}
+
+ {table.type == DataWarehouseRowType.ExternalTable && (
+
+ <>
+ Last Synced At
+
+ {table.payload.external_schema?.last_synced_at
+ ? humanFriendlyDetailedTime(
+ table.payload.external_schema?.last_synced_at,
+ 'MMMM DD, YYYY',
+ 'h:mm A'
+ )
+ : 'Not yet synced'}
+
+ >
+
+ <>
+ Files URL pattern
+ {table.payload.url_pattern}
+ >
+
+ <>
+ File format
+ {table.payload.format}
+ >
+
+ )}
+
+ {!isEditingSavedQuery && (
+
+ Columns
+
+
+ )}
+
+ {'query' in table.payload && isEditingSavedQuery && (
+
+ Update View Definition
+ {
+ setLocalQuery({
+ kind: NodeKind.HogQLQuery,
+ query: queryInput,
+ })
+ }}
+ editorFooter={(hasErrors, error, isValidView) => (
+ {
+ localQuery &&
+ updateDataWarehouseSavedQuery({
+ ...(table.payload as DataWarehouseSavedQuery),
+ query: localQuery,
+ })
+ }}
+ loading={dataWarehouseSavedQueriesLoading}
+ type="primary"
+ center
+ disabledReason={
+ hasErrors
+ ? error ?? 'Query has errors'
+ : !isValidView
+ ? 'All fields must have an alias'
+ : ''
+ }
+ data-attr="hogql-query-editor-save-as-view"
+ >
+ Save as View
+
+ )}
+ />
+
+ )}
+
+ ) : (
+
+
+
+ )
+}
diff --git a/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.ts b/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.ts
index fabedbd034724..7bcd02f4e34de 100644
--- a/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.ts
+++ b/frontend/src/scenes/data-warehouse/external/dataWarehouseSceneLogic.ts
@@ -32,7 +32,12 @@ export const dataWarehouseSceneLogic = kea([
],
actions: [
dataWarehouseSavedQueriesLogic,
- ['deleteDataWarehouseSavedQuery'],
+ [
+ 'loadDataWarehouseSavedQueries',
+ 'deleteDataWarehouseSavedQuery',
+ 'updateDataWarehouseSavedQuery',
+ 'updateDataWarehouseSavedQuerySuccess',
+ ],
databaseTableListLogic,
['loadDataWarehouse', 'deleteDataWarehouseTable'],
],
@@ -41,6 +46,7 @@ export const dataWarehouseSceneLogic = kea([
toggleSourceModal: (isOpen?: boolean) => ({ isOpen }),
selectRow: (row: DataWarehouseTableType | null) => ({ row }),
setSceneTab: (tab: DataWarehouseSceneTab) => ({ tab }),
+ setIsEditingSavedQuery: (isEditingSavedQuery: boolean) => ({ isEditingSavedQuery }),
}),
reducers({
isSourceModalOpen: [
@@ -61,6 +67,12 @@ export const dataWarehouseSceneLogic = kea([
setSceneTab: (_state, { tab }) => tab,
},
],
+ isEditingSavedQuery: [
+ false,
+ {
+ setIsEditingSavedQuery: (_, { isEditingSavedQuery }) => isEditingSavedQuery,
+ },
+ ],
}),
selectors({
externalTables: [
@@ -168,6 +180,13 @@ export const dataWarehouseSceneLogic = kea([
actions.selectRow(null)
lemonToast.success(`${table.name} successfully deleted`)
},
+ selectRow: () => {
+ actions.setIsEditingSavedQuery(false)
+ },
+ updateDataWarehouseSavedQuerySuccess: async (query) => {
+ actions.setIsEditingSavedQuery(false)
+ lemonToast.success(`${query.name} successfully updated`)
+ },
})),
afterMount(({ actions, values }) => {
if (values.featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE]) {
diff --git a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx
index b7a2a9b2dff83..abcd589b7a153 100644
--- a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx
+++ b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseSavedQueriesLogic.tsx
@@ -39,13 +39,27 @@ export const dataWarehouseSavedQueriesLogic = kea {
+ await api.dataWarehouseSavedQueries.update(view.id, view)
+ return {
+ ...values.dataWarehouseSavedQueries,
+ results: values.dataWarehouseSavedQueries
+ ? values.dataWarehouseSavedQueries.results.map((v) =>
+ v.id === view.id ? { ...v, ...view } : v
+ )
+ : [],
+ }
+ },
},
],
})),
- listeners(() => ({
+ listeners(({ actions }) => ({
createDataWarehouseSavedQuerySuccess: () => {
router.actions.push(urls.dataWarehouse())
},
+ updateDataWarehouseSavedQuerySuccess: () => {
+ actions.loadDataWarehouseSavedQueries()
+ },
})),
selectors({
savedQueries: [