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: [