0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 09:14:46 +01:00

feat(data-warehouse): new SQL layout (#25686)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Eric Duong 2024-11-11 15:30:08 -05:00 committed by GitHub
parent 29628b3e78
commit 6be1ada7d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 652 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -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;

View File

@ -48,12 +48,13 @@ export function Navigation({
{activeNavbarItem && <Sidebar key={activeNavbarItem.identifier} navbarItem={activeNavbarItem} />}
</FlaggedFeature>
<main>
<TopBar />
{sceneConfig?.layout !== 'app-raw-no-header' && <TopBar />}
<div
className={clsx(
'Navigation3000__scene',
// Hack - once we only have 3000 the "minimal" scenes should become "app-raw"
sceneConfig?.layout === 'app-raw' && 'Navigation3000__scene--raw',
sceneConfig?.layout === 'app-raw-no-header' && 'Navigation3000__scene--raw-no-header',
sceneConfig?.layout === 'app-canvas' && 'Navigation3000__scene--canvas'
)}
>

View File

@ -511,6 +511,14 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
icon: <IconServer />,
to: isUsingSidebar ? undefined : urls.dataWarehouse(),
},
featureFlags[FEATURE_FLAGS.SQL_EDITOR]
? {
identifier: Scene.SQLEditor,
label: 'SQL editor',
icon: <IconServer />,
to: isUsingSidebar ? undefined : urls.sqlEditor(),
}
: null,
featureFlags[FEATURE_FLAGS.DATA_MODELING] && hasOnboardedAnyProduct
? {
identifier: Scene.DataModel,

View File

@ -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

View File

@ -169,7 +169,7 @@ export const codeEditorLogic = kea<codeEditorLogicType>([
}),
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<codeEditorLogicType>([
}
},
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<codeEditorLogicType>([
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<codeEditorLogicType>([
},
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<codeEditorLogicType>([
}
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)

View File

@ -40,6 +40,7 @@ export const appScenes: Record<Scene, () => 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'),

View File

@ -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 (
<BindLogic logic={editorSizingLogic} props={editorSizingLogicProps}>
<div className="w-full h-full flex flex-row overflow-hidden" ref={ref}>
<SourceNavigator />
<QueryWindow />
</div>
</BindLogic>
)
}

View File

@ -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<CodeEditorProps>
}
export function QueryPane(props: QueryPaneProps): JSX.Element {
const { queryPaneHeight, queryPaneResizerProps } = useValues(editorSizingLogic)
return (
<div
className="relative flex flex-col w-full bg-bg-3000"
// eslint-disable-next-line react/forbid-dom-props
style={{
height: `${queryPaneHeight}px`,
}}
ref={queryPaneResizerProps.containerRef}
>
<div className="flex-1">
{props.promptError ? <LemonBanner type="warning">{props.promptError}</LemonBanner> : null}
<AutoSizer>
{({ height, width }) => (
<CodeEditor
className="border"
language="hogQL"
value={props.queryInput}
height={height}
width={width}
{...props.codeEditorProps}
options={{
minimap: {
enabled: false,
},
wordWrap: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
fixedOverflowWidgets: true,
suggest: {
showInlineDetails: true,
},
quickSuggestionsDelay: 300,
}}
/>
)}
</AutoSizer>
</div>
<Resizer {...queryPaneResizerProps} />
</div>
)
}

View File

@ -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 (
<div className="flex flex-row overflow-scroll hide-scrollbar">
{models.map((model: Uri) => (
<QueryTab
key={model.path}
model={model}
onClear={models.length > 1 ? onClear : undefined}
onClick={onClick}
active={activeModelUri?.path === model.path}
/>
))}
<LemonButton onClick={onAdd} icon={<IconPlus fontSize={14} />} />
</div>
)
}
interface QueryTabProps {
model: Uri
onClick: (model: Uri) => void
onClear?: (model: Uri) => void
active: boolean
}
function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Element {
return (
<button
onClick={() => onClick?.(model)}
className={clsx(
'space-y-px rounded-t p-1 flex flex-row items-center gap-1 hover:bg-[var(--bg-light)] cursor-pointer',
active ? 'bg-[var(--bg-light)] border' : 'bg-bg-3000',
onClear ? 'pl-3 pr-2' : 'px-3'
)}
>
Untitled
{onClear && (
<LemonButton
onClick={(e) => {
e.stopPropagation()
onClear(model)
}}
size="xsmall"
icon={<IconX />}
/>
)}
</button>
)
}

View File

@ -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<HogQLQuery>({
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 (
<div className="flex flex-1 flex-col h-full">
<QueryTabs
models={allModels}
onClick={setModel}
onClear={deleteModel}
onAdd={onAdd}
activeModelUri={activeModelUri}
/>
<QueryPane
queryInput={queryInput}
promptError={promptError}
codeEditorProps={{
onChange: (v) => {
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<string, any>) => {
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()
}
},
}}
/>
<BindLogic
logic={dataNodeLogic}
props={{
key: modelKey,
query: query,
doNotLoad: !query.query,
}}
>
<ResultPane
onQueryInputChange={saveQuery}
onSave={saveAsView}
saveDisabledReason={
hasErrors ? error ?? 'Query has errors' : !isValidView ? 'All fields must have an alias' : ''
}
/>
</BindLogic>
</div>
)
}

View File

@ -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<string, any> = {}
response.columns.forEach((column: string, i: number) => {
rowObject[column] = row[i]
})
return rowObject
})
}, [response])
return (
<div className="flex flex-col w-full flex-1 bg-bg-3000">
<div className="flex flex-row justify-between align-center py-2 px-4 w-full h-[55px]">
<LemonTabs
activeKey={ResultsTab.Results}
onChange={() => {}}
tabs={[
{
key: ResultsTab.Results,
label: 'Results',
},
]}
/>
<div className="flex gap-1">
<LemonButton type="secondary" onClick={() => onSave()} disabledReason={saveDisabledReason}>
Save
</LemonButton>
<LemonButton type="primary" onClick={() => onQueryInputChange()}>
Run
</LemonButton>
</div>
</div>
<div className="flex flex-1 relative bg-dark justify-center items-center">
{responseLoading ? (
<Spinner className="text-3xl" />
) : !response ? (
<span className="text-muted mt-3">Query results will appear here</span>
) : (
<div className="flex-1 absolute top-0 left-0 right-0 bottom-0">
<DataGrid
className={isDarkModeOn ? 'rdg-dark h-full' : 'rdg-light h-full'}
columns={columns}
rows={rows}
/>
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,15 @@
import { LemonInput } from '@posthog/lemon-ui'
export const SchemaSearch = (): JSX.Element => {
return (
<div className="flex items-center">
<LemonInput
className="rounded-none"
type="search"
placeholder="Search for schema"
data-attr="schema-search"
fullWidth
/>
</div>
)
}

View File

@ -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 (
<div
ref={sourceNavigatorResizerProps.containerRef}
className="relative flex flex-col bg-bg-3000 h-full overflow-hidden"
// eslint-disable-next-line react/forbid-dom-props
style={{
width: `${sourceNavigatorWidth}px`,
}}
>
<SchemaSearch />
<DatabaseTableTreeWithItems inline collapsible={false} />
<Resizer {...sourceNavigatorResizerProps} />
</div>
)
}

View File

@ -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<HTMLDivElement>
navigatorRef: React.RefObject<HTMLDivElement>
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<editorSizingLogicType>([
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,
],
}),
])

View File

@ -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<queryWindowLogicType>([
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()
},
})),
])

View File

@ -0,0 +1,18 @@
import { kea } from 'kea'
import type { sourceNavigatorLogicType } from './sourceNavigatorLogicType'
export const sourceNavigatorLogic = kea<sourceNavigatorLogicType>({
path: ['scenes', 'data-warehouse', 'editor', 'sourceNavigatorLogic'],
actions: {
setWidth: (width: number) => ({ width }),
},
reducers: {
navigatorWidth: [
200,
{
setWidth: (_, { width }: { width: number }) => width,
},
],
},
})

View File

@ -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 (
<div
className={clsx(
`bg-bg-light space-y-px rounded border p-2 overflow-y-auto`,
`bg-bg-light rounded space-y-px border p-2 overflow-y-auto`,
!collapsed ? 'min-w-80 flex-1' : ''
)}
>
{collapsed ? (
<LemonButton icon={<IconDatabase />} onClick={() => setCollapsed(false)} />
) : (
) : collapsible ? (
<>
<LemonButton
size="xsmall"
@ -312,6 +313,11 @@ export const DatabaseTableTreeWithItems = ({ inline }: DatabaseTableTreeProps):
</LemonButton>
<DatabaseTableTree onSelectRow={selectRow} items={treeItems()} selectedRow={selectedRow} />
</>
) : (
<>
<span className="text-muted-alt tracking-wider font-normal">Sources</span>
<DatabaseTableTree onSelectRow={selectRow} items={treeItems()} selectedRow={selectedRow} />
</>
)}
<LemonModal
width="50rem"

View File

@ -43,6 +43,7 @@ export enum Scene {
Survey = 'Survey',
SurveyTemplates = 'SurveyTemplates',
DataWarehouse = 'DataWarehouse',
SQLEditor = 'SQLEditor',
DataModel = 'DataModel',
DataWarehouseExternal = 'DataWarehouseExternal',
DataWarehouseTable = 'DataWarehouseTable',
@ -130,7 +131,7 @@ export interface SceneConfig {
* If `plain`, there's no navigation present, and the scene has no padding.
* @default 'app'
*/
layout?: 'app' | 'app-raw' | 'app-canvas' | 'app-container' | 'plain'
layout?: 'app' | 'app-raw' | 'app-canvas' | 'app-container' | 'app-raw-no-header' | 'plain'
/** Hides project notice (ProjectNotice.tsx). */
hideProjectNotice?: boolean
/** Hides billing notice (BillingAlertsV2.tsx). */

View File

@ -234,6 +234,12 @@ export const sceneConfigurations: Record<Scene, SceneConfig> = {
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<string, Scene> = {
[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,

View File

@ -173,6 +173,7 @@ export const urls = {
dataWarehouse: (query?: string | Record<string, any>): 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`,

View File

@ -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",

View File

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