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:
parent
29628b3e78
commit
6be1ada7d1
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 |
@ -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;
|
||||
|
||||
|
@ -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'
|
||||
)}
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
|
36
frontend/src/scenes/data-warehouse/editor/EditorScene.tsx
Normal file
36
frontend/src/scenes/data-warehouse/editor/EditorScene.tsx
Normal 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>
|
||||
)
|
||||
}
|
58
frontend/src/scenes/data-warehouse/editor/QueryPane.tsx
Normal file
58
frontend/src/scenes/data-warehouse/editor/QueryPane.tsx
Normal 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>
|
||||
)
|
||||
}
|
61
frontend/src/scenes/data-warehouse/editor/QueryTabs.tsx
Normal file
61
frontend/src/scenes/data-warehouse/editor/QueryTabs.tsx
Normal 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>
|
||||
)
|
||||
}
|
175
frontend/src/scenes/data-warehouse/editor/QueryWindow.tsx
Normal file
175
frontend/src/scenes/data-warehouse/editor/QueryWindow.tsx
Normal 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>
|
||||
)
|
||||
}
|
88
frontend/src/scenes/data-warehouse/editor/ResultPane.tsx
Normal file
88
frontend/src/scenes/data-warehouse/editor/ResultPane.tsx
Normal 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>
|
||||
)
|
||||
}
|
15
frontend/src/scenes/data-warehouse/editor/SchemaSearch.tsx
Normal file
15
frontend/src/scenes/data-warehouse/editor/SchemaSearch.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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,
|
||||
],
|
||||
}),
|
||||
])
|
@ -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()
|
||||
},
|
||||
})),
|
||||
])
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
@ -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"
|
||||
|
@ -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). */
|
||||
|
@ -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,
|
||||
|
@ -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`,
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user