diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--dark.png b/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--dark.png new file mode 100644 index 00000000000..b54d4facb0b Binary files /dev/null and b/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--light.png b/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--light.png new file mode 100644 index 00000000000..332ea24ce30 Binary files /dev/null and b/frontend/__snapshots__/exporter-exporter--trends-line-insight-detailed--light.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png index d0d092e7f08..d5637e0b03a 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png differ diff --git a/frontend/src/layout/navigation-3000/Navigation.scss b/frontend/src/layout/navigation-3000/Navigation.scss index 882e4c5acac..07bdfe66828 100644 --- a/frontend/src/layout/navigation-3000/Navigation.scss +++ b/frontend/src/layout/navigation-3000/Navigation.scss @@ -54,6 +54,15 @@ flex-direction: column; } + &.Navigation3000__scene--raw-no-header { + --scene-padding: 0px; + --scene-padding-bottom: 0px; + + display: flex; + flex-direction: column; + height: 100vh; + } + &.Navigation3000__scene--canvas { --scene-padding: 0px; diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index a53769d74e6..11b0e928d4f 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -48,12 +48,13 @@ export function Navigation({ {activeNavbarItem && }
- + {sceneConfig?.layout !== 'app-raw-no-header' && }
diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 09fd77527c4..ca43417d405 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -511,6 +511,14 @@ export const navigation3000Logic = kea([ icon: , to: isUsingSidebar ? undefined : urls.dataWarehouse(), }, + featureFlags[FEATURE_FLAGS.SQL_EDITOR] + ? { + identifier: Scene.SQLEditor, + label: 'SQL editor', + icon: , + to: isUsingSidebar ? undefined : urls.sqlEditor(), + } + : null, featureFlags[FEATURE_FLAGS.DATA_MODELING] && hasOnboardedAnyProduct ? { identifier: Scene.DataModel, diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 35ab2bcddc1..1eff8fcc2c5 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -176,6 +176,7 @@ export const FEATURE_FLAGS = { AI_SESSION_SUMMARY: 'ai-session-summary', // owner: #team-replay AI_SESSION_PERMISSIONS: 'ai-session-permissions', // owner: #team-replay PRODUCT_INTRO_PAGES: 'product-intro-pages', // owner: @raquelmsmith + SQL_EDITOR: 'sql-editor', // owner: @EDsCODE #team-data-warehouse SESSION_REPLAY_DOCTOR: 'session-replay-doctor', // owner: #team-replay REPLAY_SIMILAR_RECORDINGS: 'session-replay-similar-recordings', // owner: #team-replay SAVED_NOT_PINNED: 'saved-not-pinned', // owner: #team-replay diff --git a/frontend/src/lib/monaco/codeEditorLogic.tsx b/frontend/src/lib/monaco/codeEditorLogic.tsx index f7dbdbf232c..54000263dfb 100644 --- a/frontend/src/lib/monaco/codeEditorLogic.tsx +++ b/frontend/src/lib/monaco/codeEditorLogic.tsx @@ -169,7 +169,7 @@ export const codeEditorLogic = kea([ }), listeners(({ props, values, actions }) => ({ addModel: () => { - if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR]) { + if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR] || values.featureFlags[FEATURE_FLAGS.SQL_EDITOR]) { const queries = values.allModels.map((model) => { return { query: @@ -182,7 +182,7 @@ export const codeEditorLogic = kea([ } }, removeModel: () => { - if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR]) { + if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR] || values.featureFlags[FEATURE_FLAGS.SQL_EDITOR]) { const queries = values.allModels.map((model) => { return { query: @@ -200,7 +200,7 @@ export const codeEditorLogic = kea([ props.editor?.setModel(model) } - if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR]) { + if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR] || values.featureFlags[FEATURE_FLAGS.SQL_EDITOR]) { const path = modelName.path.split('/').pop() path && props.multitab && actions.setLocalState(activemodelStateKey(props.key), path) } @@ -225,7 +225,7 @@ export const codeEditorLogic = kea([ }, updateState: async (_, breakpoint) => { await breakpoint(100) - if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR]) { + if (values.featureFlags[FEATURE_FLAGS.MULTITAB_EDITOR] || values.featureFlags[FEATURE_FLAGS.SQL_EDITOR]) { const queries = values.allModels.map((model) => { return { query: @@ -245,8 +245,11 @@ export const codeEditorLogic = kea([ } if (props.monaco) { + const defaultQuery = values.featureFlags[FEATURE_FLAGS.SQL_EDITOR] + ? '' + : 'SELECT event FROM events LIMIT 100' const uri = props.monaco.Uri.parse(currentModelCount.toString()) - const model = props.monaco.editor.createModel('SELECT event FROM events LIMIT 100', props.language, uri) + const model = props.monaco.editor.createModel(defaultQuery, props.language, uri) props.editor?.setModel(model) actions.setModel(uri) actions.addModel(uri) diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index f86655663cc..4a8e63759e1 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -40,6 +40,7 @@ export const appScenes: Record any> = { [Scene.SurveyTemplates]: () => import('./surveys/SurveyTemplates'), [Scene.DataModel]: () => import('./data-model/DataModelScene'), [Scene.DataWarehouse]: () => import('./data-warehouse/external/DataWarehouseExternalScene'), + [Scene.SQLEditor]: () => import('./data-warehouse/editor/EditorScene'), [Scene.DataWarehouseTable]: () => import('./data-warehouse/new/NewSourceWizard'), [Scene.DataWarehouseExternal]: () => import('./data-warehouse/external/DataWarehouseExternalScene'), [Scene.DataWarehouseRedirect]: () => import('./data-warehouse/redirect/DataWarehouseRedirectScene'), diff --git a/frontend/src/scenes/data-warehouse/editor/EditorScene.tsx b/frontend/src/scenes/data-warehouse/editor/EditorScene.tsx new file mode 100644 index 00000000000..1b36477047c --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/EditorScene.tsx @@ -0,0 +1,36 @@ +import { BindLogic } from 'kea' +import { useRef } from 'react' + +import { editorSizingLogic } from './editorSizingLogic' +import { QueryWindow } from './QueryWindow' +import { SourceNavigator } from './SourceNavigator' + +export function EditorScene(): JSX.Element { + const ref = useRef(null) + const navigatorRef = useRef(null) + const queryPaneRef = useRef(null) + + const editorSizingLogicProps = { + editorSceneRef: ref, + navigatorRef, + sourceNavigatorResizerProps: { + containerRef: navigatorRef, + logicKey: 'source-navigator', + placement: 'right', + }, + queryPaneResizerProps: { + containerRef: queryPaneRef, + logicKey: 'query-pane', + placement: 'bottom', + }, + } + + return ( + +
+ + +
+
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/QueryPane.tsx b/frontend/src/scenes/data-warehouse/editor/QueryPane.tsx new file mode 100644 index 00000000000..8d28cf57614 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/QueryPane.tsx @@ -0,0 +1,58 @@ +import { useValues } from 'kea' +import { Resizer } from 'lib/components/Resizer/Resizer' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { CodeEditor, CodeEditorProps } from 'lib/monaco/CodeEditor' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' + +import { editorSizingLogic } from './editorSizingLogic' + +interface QueryPaneProps { + queryInput: string + promptError: string | null + codeEditorProps: Partial +} + +export function QueryPane(props: QueryPaneProps): JSX.Element { + const { queryPaneHeight, queryPaneResizerProps } = useValues(editorSizingLogic) + + return ( +
+
+ {props.promptError ? {props.promptError} : null} + + {({ height, width }) => ( + + )} + +
+ +
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/QueryTabs.tsx b/frontend/src/scenes/data-warehouse/editor/QueryTabs.tsx new file mode 100644 index 00000000000..b49acba9584 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/QueryTabs.tsx @@ -0,0 +1,61 @@ +import { IconPlus, IconX } from '@posthog/icons' +import { LemonButton } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { Uri } from 'monaco-editor' + +interface QueryTabsProps { + models: Uri[] + onClick: (model: Uri) => void + onClear: (model: Uri) => void + onAdd: () => void + activeModelUri: Uri | null +} + +export function QueryTabs({ models, onClear, onClick, onAdd, activeModelUri }: QueryTabsProps): JSX.Element { + return ( +
+ {models.map((model: Uri) => ( + 1 ? onClear : undefined} + onClick={onClick} + active={activeModelUri?.path === model.path} + /> + ))} + } /> +
+ ) +} + +interface QueryTabProps { + model: Uri + onClick: (model: Uri) => void + onClear?: (model: Uri) => void + active: boolean +} + +function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Element { + return ( + + ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/QueryWindow.tsx b/frontend/src/scenes/data-warehouse/editor/QueryWindow.tsx new file mode 100644 index 00000000000..1fd177989be --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/QueryWindow.tsx @@ -0,0 +1,175 @@ +import { Monaco } from '@monaco-editor/react' +import { BindLogic, useActions, useValues } from 'kea' +import { router } from 'kea-router' +import { + activemodelStateKey, + codeEditorLogic, + CodeEditorLogicProps, + editorModelsStateKey, +} from 'lib/monaco/codeEditorLogic' +import type { editor as importedEditor, Uri } from 'monaco-editor' +import { useCallback, useEffect, useState } from 'react' + +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' +import { hogQLQueryEditorLogic } from '~/queries/nodes/HogQLQuery/hogQLQueryEditorLogic' +import { HogQLQuery, NodeKind } from '~/queries/schema' + +import { QueryPane } from './QueryPane' +import { QueryTabs } from './QueryTabs' +import { ResultPane } from './ResultPane' + +export function QueryWindow(): JSX.Element { + const [monacoAndEditor, setMonacoAndEditor] = useState( + null as [Monaco, importedEditor.IStandaloneCodeEditor] | null + ) + const [monaco, editor] = monacoAndEditor ?? [] + + const key = router.values.location.pathname + + const [query, setActiveQueryInput] = useState({ + kind: NodeKind.HogQLQuery, + query: '', + }) + + const hogQLQueryEditorLogicProps = { + query, + setQuery: (query: HogQLQuery) => { + setActiveQueryInput(query) + }, + onChange: () => {}, + key, + } + const logic = hogQLQueryEditorLogic(hogQLQueryEditorLogicProps) + const { queryInput, promptError } = useValues(logic) + const { setQueryInput, saveQuery, saveAsView } = useActions(logic) + + const codeEditorKey = `hogQLQueryEditor/${router.values.location.pathname}` + + const codeEditorLogicProps: CodeEditorLogicProps = { + key: codeEditorKey, + sourceQuery: query, + query: queryInput, + language: 'hogQL', + metadataFilters: query.filters, + monaco, + editor, + multitab: true, + } + const { activeModelUri, allModels, hasErrors, error, isValidView } = useValues( + codeEditorLogic(codeEditorLogicProps) + ) + + const { createModel, setModel, deleteModel, setModels, addModel, updateState } = useActions( + codeEditorLogic(codeEditorLogicProps) + ) + + const modelKey = `hogQLQueryEditor/${activeModelUri?.path}` + + useEffect(() => { + if (monaco && activeModelUri) { + const _model = monaco.editor.getModel(activeModelUri) + const val = _model?.getValue() + setQueryInput(val ?? '') + saveQuery() + } + }, [activeModelUri]) + + const onAdd = useCallback(() => { + createModel() + }, [createModel]) + + return ( +
+ + { + setQueryInput(v ?? '') + updateState() + }, + onMount: (editor, monaco) => { + setMonacoAndEditor([monaco, editor]) + + const allModelQueries = localStorage.getItem(editorModelsStateKey(codeEditorKey)) + const activeModelUri = localStorage.getItem(activemodelStateKey(codeEditorKey)) + + if (allModelQueries) { + // clear existing models + monaco.editor.getModels().forEach((model) => { + model.dispose() + }) + + const models = JSON.parse(allModelQueries || '[]') + const newModels: Uri[] = [] + + models.forEach((model: Record) => { + if (monaco) { + const uri = monaco.Uri.parse(model.path) + const newModel = monaco.editor.createModel(model.query, 'hogQL', uri) + editor?.setModel(newModel) + newModels.push(uri) + } + }) + + setModels(newModels) + + if (activeModelUri) { + const uri = monaco.Uri.parse(activeModelUri) + const activeModel = monaco.editor + .getModels() + .find((model) => model.uri.path === uri.path) + activeModel && editor?.setModel(activeModel) + const val = activeModel?.getValue() + if (val) { + setQueryInput(val) + saveQuery() + } + setModel(uri) + } else if (newModels.length) { + setModel(newModels[0]) + } + } else { + const model = editor.getModel() + + if (model) { + addModel(model.uri) + setModel(model.uri) + } + } + }, + onPressCmdEnter: (value, selectionType) => { + if (value && selectionType === 'selection') { + saveQuery(value) + } else { + saveQuery() + } + }, + }} + /> + + + +
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/ResultPane.tsx b/frontend/src/scenes/data-warehouse/editor/ResultPane.tsx new file mode 100644 index 00000000000..215a116d07a --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/ResultPane.tsx @@ -0,0 +1,88 @@ +import 'react-data-grid/lib/styles.css' + +import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { useMemo } from 'react' +import DataGrid from 'react-data-grid' + +import { themeLogic } from '~/layout/navigation-3000/themeLogic' +import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' + +enum ResultsTab { + Results = 'results', + Visualization = 'visualization', +} + +interface ResultPaneProps { + onSave: () => void + saveDisabledReason?: string + onQueryInputChange: () => void +} + +export function ResultPane({ onQueryInputChange, onSave, saveDisabledReason }: ResultPaneProps): JSX.Element { + const { isDarkModeOn } = useValues(themeLogic) + const { response, responseLoading } = useValues(dataNodeLogic) + + const columns = useMemo(() => { + return ( + response?.columns?.map((column: string) => ({ + key: column, + name: column, + resizable: true, + })) ?? [] + ) + }, [response]) + + const rows = useMemo(() => { + if (!response?.results) { + return [] + } + return response?.results?.map((row: any[]) => { + const rowObject: Record = {} + response.columns.forEach((column: string, i: number) => { + rowObject[column] = row[i] + }) + return rowObject + }) + }, [response]) + + return ( +
+
+ {}} + tabs={[ + { + key: ResultsTab.Results, + label: 'Results', + }, + ]} + /> +
+ onSave()} disabledReason={saveDisabledReason}> + Save + + onQueryInputChange()}> + Run + +
+
+
+ {responseLoading ? ( + + ) : !response ? ( + Query results will appear here + ) : ( +
+ +
+ )} +
+
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/SchemaSearch.tsx b/frontend/src/scenes/data-warehouse/editor/SchemaSearch.tsx new file mode 100644 index 00000000000..ec03ca0a1b8 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/SchemaSearch.tsx @@ -0,0 +1,15 @@ +import { LemonInput } from '@posthog/lemon-ui' + +export const SchemaSearch = (): JSX.Element => { + return ( +
+ +
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/SourceNavigator.tsx b/frontend/src/scenes/data-warehouse/editor/SourceNavigator.tsx new file mode 100644 index 00000000000..ca9f4991245 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/SourceNavigator.tsx @@ -0,0 +1,25 @@ +import { useValues } from 'kea' +import { Resizer } from 'lib/components/Resizer/Resizer' + +import { DatabaseTableTreeWithItems } from '../external/DataWarehouseTables' +import { editorSizingLogic } from './editorSizingLogic' +import { SchemaSearch } from './SchemaSearch' + +export function SourceNavigator(): JSX.Element { + const { sourceNavigatorWidth, sourceNavigatorResizerProps } = useValues(editorSizingLogic) + + return ( +
+ + + +
+ ) +} diff --git a/frontend/src/scenes/data-warehouse/editor/editorSizingLogic.tsx b/frontend/src/scenes/data-warehouse/editor/editorSizingLogic.tsx new file mode 100644 index 00000000000..25bc04388e5 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/editorSizingLogic.tsx @@ -0,0 +1,50 @@ +import { connect, kea, path, props, selectors } from 'kea' +import { resizerLogic, ResizerLogicProps } from 'lib/components/Resizer/resizerLogic' + +import type { editorSizingLogicType } from './editorSizingLogicType' + +export interface EditorSizingLogicProps { + editorSceneRef: React.RefObject + navigatorRef: React.RefObject + sourceNavigatorResizerProps: ResizerLogicProps + queryPaneResizerProps: ResizerLogicProps +} + +const MINIMUM_NAVIGATOR_WIDTH = 100 +const NAVIGATOR_DEFAULT_WIDTH = 350 +const MINIMUM_QUERY_PANE_HEIGHT = 100 +const DEFAULT_QUERY_PANE_HEIGHT = 600 + +export const editorSizingLogic = kea([ + path(['scenes', 'data-warehouse', 'editor', 'editorSizingLogic']), + props({} as EditorSizingLogicProps), + connect((props: EditorSizingLogicProps) => ({ + values: [ + resizerLogic(props.sourceNavigatorResizerProps), + ['desiredSize as sourceNavigatorDesiredSize'], + resizerLogic(props.queryPaneResizerProps), + ['desiredSize as queryPaneDesiredSize'], + ], + })), + selectors({ + editorSceneRef: [() => [(_, props) => props.editorSceneRef], (editorSceneRef) => editorSceneRef], + sourceNavigatorWidth: [ + (s) => [s.sourceNavigatorDesiredSize], + (desiredSize) => Math.max(desiredSize || NAVIGATOR_DEFAULT_WIDTH, MINIMUM_NAVIGATOR_WIDTH), + ], + queryPaneHeight: [ + (s) => [s.queryPaneDesiredSize], + (queryPaneDesiredSize) => + Math.max(queryPaneDesiredSize || DEFAULT_QUERY_PANE_HEIGHT, MINIMUM_QUERY_PANE_HEIGHT), + ], + queryTabsWidth: [(s) => [s.queryPaneDesiredSize], (desiredSize) => desiredSize || NAVIGATOR_DEFAULT_WIDTH], + sourceNavigatorResizerProps: [ + () => [(_, props) => props.sourceNavigatorResizerProps], + (sourceNavigatorResizerProps) => sourceNavigatorResizerProps, + ], + queryPaneResizerProps: [ + () => [(_, props) => props.queryPaneResizerProps], + (queryPaneResizerProps) => queryPaneResizerProps, + ], + }), +]) diff --git a/frontend/src/scenes/data-warehouse/editor/queryWindowLogic.ts b/frontend/src/scenes/data-warehouse/editor/queryWindowLogic.ts new file mode 100644 index 00000000000..2af354bb9e1 --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/queryWindowLogic.ts @@ -0,0 +1,58 @@ +import { actions, events, kea, listeners, path, reducers } from 'kea' +import { uuid } from 'lib/utils' + +import type { queryWindowLogicType } from './queryWindowLogicType' + +export interface Tab { + key: string + label: string +} + +export const queryWindowLogic = kea([ + path(['scenes', 'data-warehouse', 'editor', 'queryWindowLogic']), + actions({ + selectTab: (tab: Tab) => ({ tab }), + addTab: true, + _addTab: (tab: Tab) => ({ tab }), + deleteTab: (tab: Tab) => ({ tab }), + _deleteTab: (tab: Tab) => ({ tab }), + }), + reducers({ + tabs: [ + [] as Tab[], + { + _addTab: (state, { tab }) => [...state, tab], + _deleteTab: (state, { tab }) => state.filter((t) => t.key !== tab.key), + }, + ], + activeTabKey: [ + 'none', + { + selectTab: (_, { tab }) => tab.key, + }, + ], + }), + listeners(({ values, actions }) => ({ + addTab: () => { + const tab = { + key: uuid(), + label: 'Untitled', + } + actions._addTab(tab) + actions.selectTab(tab) + }, + deleteTab: ({ tab }) => { + if (tab.key === values.activeTabKey) { + const indexOfTab = values.tabs.findIndex((t) => t.key === tab.key) + const nextTab = values.tabs[indexOfTab + 1] || values.tabs[indexOfTab - 1] || values.tabs[0] + actions.selectTab(nextTab) + } + actions._deleteTab(tab) + }, + })), + events(({ actions }) => ({ + afterMount: () => { + actions.addTab() + }, + })), +]) diff --git a/frontend/src/scenes/data-warehouse/editor/sourceNavigatorLogic.ts b/frontend/src/scenes/data-warehouse/editor/sourceNavigatorLogic.ts new file mode 100644 index 00000000000..406ae6e0d4e --- /dev/null +++ b/frontend/src/scenes/data-warehouse/editor/sourceNavigatorLogic.ts @@ -0,0 +1,18 @@ +import { kea } from 'kea' + +import type { sourceNavigatorLogicType } from './sourceNavigatorLogicType' + +export const sourceNavigatorLogic = kea({ + path: ['scenes', 'data-warehouse', 'editor', 'sourceNavigatorLogic'], + actions: { + setWidth: (width: number) => ({ width }), + }, + reducers: { + navigatorWidth: [ + 200, + { + setWidth: (_, { width }: { width: number }) => width, + }, + ], + }, +}) diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx index 03b4da5e3a1..debe5cf980b 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx @@ -54,9 +54,10 @@ export const DataWarehouseTables = ({ insightProps }: DataWarehousetTablesProps) interface DatabaseTableTreeProps { inline?: boolean + collapsible?: boolean } -export const DatabaseTableTreeWithItems = ({ inline }: DatabaseTableTreeProps): JSX.Element => { +export const DatabaseTableTreeWithItems = ({ inline, collapsible = true }: DatabaseTableTreeProps): JSX.Element => { const { dataWarehouseTablesBySourceType, posthogTables, @@ -293,13 +294,13 @@ export const DatabaseTableTreeWithItems = ({ inline }: DatabaseTableTreeProps): return (
{collapsed ? ( } onClick={() => setCollapsed(false)} /> - ) : ( + ) : collapsible ? ( <> + ) : ( + <> + Sources + + )} = { name: 'Data warehouse', defaultDocsPath: '/docs/data-warehouse', }, + [Scene.SQLEditor]: { + projectBased: true, + name: 'SQL editor', + defaultDocsPath: '/docs/data-warehouse/setup', + layout: 'app-raw-no-header', + }, [Scene.DataWarehouseExternal]: { projectBased: true, name: 'Data warehouse', @@ -552,6 +558,7 @@ export const routes: Record = { [urls.dataWarehouseView(':id')]: Scene.DataWarehouse, [urls.dataWarehouseTable()]: Scene.DataWarehouseTable, [urls.dataWarehouseRedirect(':kind')]: Scene.DataWarehouseRedirect, + [urls.sqlEditor()]: Scene.SQLEditor, [urls.featureFlags()]: Scene.FeatureFlags, [urls.featureFlag(':id')]: Scene.FeatureFlag, [urls.annotations()]: Scene.DataManagement, diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index b97dd77a1a6..ccd4b7a482f 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -173,6 +173,7 @@ export const urls = { dataWarehouse: (query?: string | Record): string => combineUrl(`/data-warehouse`, {}, query ? { q: typeof query === 'string' ? query : JSON.stringify(query) } : {}) .url, + sqlEditor: (): string => `/sql`, dataWarehouseView: (id: string): string => combineUrl(`/data-warehouse/view/${id}`).url, dataWarehouseTable: (): string => `/data-warehouse/new`, dataWarehouseRedirect: (kind: string): string => `/data-warehouse/${kind}/redirect`, diff --git a/package.json b/package.json index f7749c5aa49..e3374ce653b 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "re2js": "^0.4.1", "react": "^18.2.0", "react-color": "^2.19.3", + "react-data-grid": "7.0.0-beta.47", "react-dom": "^18.2.0", "react-draggable": "^4.2.0", "react-email-editor": "^1.7.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87fbc7380dd..e52e37d9ae5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,6 +313,9 @@ dependencies: react-color: specifier: ^2.19.3 version: 2.19.3(react@18.2.0) + react-data-grid: + specifier: 7.0.0-beta.47 + version: 7.0.0-beta.47(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -10413,6 +10416,11 @@ packages: engines: {node: '>=6'} dev: false + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: false + /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -18321,6 +18329,17 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /react-data-grid@7.0.0-beta.47(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-28kjsmwQGD/9RXYC50zn5Zv/SQMhBBoSvG5seq0fM8XXi9TZ0zr9Z5T3YJqLwcEtoNzTOq3y0njkmdujGkIwQQ==} + peerDependencies: + react: ^18.0 || ^19.0 + react-dom: ^18.0 || ^19.0 + dependencies: + clsx: 2.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-docgen-typescript@2.2.2(typescript@4.9.5): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: