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

feat(editor-3000): integrate nav3000 sidebar (#26184)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Eric Duong 2024-11-19 13:17:34 -05:00 committed by GitHub
parent a0d32aa185
commit 0f8d81b25c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 637 additions and 165 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -175,7 +175,7 @@
.Sidebar3000 { .Sidebar3000 {
--sidebar-slider-padding: 0.125rem; --sidebar-slider-padding: 0.125rem;
--sidebar-horizontal-padding: 0.5rem; --sidebar-horizontal-padding: 0.5rem;
--sidebar-row-height: 2rem; --sidebar-row-height: 2.5rem;
--sidebar-background: var(--bg-3000); --sidebar-background: var(--bg-3000);
position: relative; position: relative;
@ -451,7 +451,8 @@
} }
// Accommodate menu button by moving stuff out of the way // Accommodate menu button by moving stuff out of the way
&.SidebarListItem--has-menu:not(.SidebarListItem--extended) .SidebarListItem__link { &.SidebarListItem--has-menu:not(.SidebarListItem--extended) .SidebarListItem__link,
&.SidebarListItem--has-menu:not(.SidebarListItem--extended) .SidebarListItem__button {
padding-right: calc(var(--sidebar-horizontal-padding) + 1.25rem); padding-right: calc(var(--sidebar-horizontal-padding) + 1.25rem);
} }
@ -523,6 +524,7 @@
} }
} }
.SidebarListItem__button,
.SidebarListItem__link, .SidebarListItem__link,
.SidebarListItem__rename { .SidebarListItem__rename {
--sidebar-list-item-inset: calc( --sidebar-list-item-inset: calc(
@ -555,6 +557,17 @@
} }
} }
.SidebarListItem__button {
row-gap: 1px;
padding: 0 var(--sidebar-horizontal-padding) 0 var(--sidebar-list-item-inset);
color: inherit !important; // Disable link color
cursor: pointer;
&:hover {
background: var(--border-3000);
}
}
.SidebarListItem__rename { .SidebarListItem__rename {
// Pseudo-elements don't work on inputs, so we use a wrapper div // Pseudo-elements don't work on inputs, so we use a wrapper div
background: var(--bg-light); background: var(--bg-light);

View File

@ -27,7 +27,7 @@ export function Navbar(): JSX.Element {
const { isAccountPopoverOpen, systemStatusHealthy } = useValues(navigationLogic) const { isAccountPopoverOpen, systemStatusHealthy } = useValues(navigationLogic)
const { closeAccountPopover, toggleAccountPopover } = useActions(navigationLogic) const { closeAccountPopover, toggleAccountPopover } = useActions(navigationLogic)
const { isNavShown, isSidebarShown, activeNavbarItemId, navbarItems, mobileLayout } = useValues(navigation3000Logic) const { isNavShown, isSidebarShown, activeNavbarItemId, navbarItems, mobileLayout } = useValues(navigation3000Logic)
const { showSidebar, hideSidebar, toggleNavCollapsed, hideNavOnMobile } = useActions(navigation3000Logic) const { toggleNavCollapsed, hideNavOnMobile, showSidebar, hideSidebar } = useActions(navigation3000Logic)
const { featureFlags } = useValues(featureFlagLogic) const { featureFlags } = useValues(featureFlagLogic)
const { toggleSearchBar } = useActions(commandBarLogic) const { toggleSearchBar } = useActions(commandBarLogic)

View File

@ -19,8 +19,16 @@ const SEARCH_DEBOUNCE_MS = 300
interface SidebarProps { interface SidebarProps {
navbarItem: SidebarNavbarItem // Sidebar can only be rendered if there's an active sidebar navbar item navbarItem: SidebarNavbarItem // Sidebar can only be rendered if there's an active sidebar navbar item
sidebarOverlay?: React.ReactNode
sidebarOverlayProps?: SidebarOverlayProps
} }
export function Sidebar({ navbarItem }: SidebarProps): JSX.Element {
interface SidebarOverlayProps {
className?: string
isOpen?: boolean
}
export function Sidebar({ navbarItem, sidebarOverlay, sidebarOverlayProps }: SidebarProps): JSX.Element {
const inputElementRef = useRef<HTMLInputElement>(null) const inputElementRef = useRef<HTMLInputElement>(null)
const { const {
@ -81,6 +89,11 @@ export function Sidebar({ navbarItem }: SidebarProps): JSX.Element {
} }
}} }}
/> />
{sidebarOverlay && (
<SidebarOverlay {...sidebarOverlayProps} isOpen={isShown && sidebarOverlayProps?.isOpen} width={width}>
{sidebarOverlay}
</SidebarOverlay>
)}
</div> </div>
) )
} }
@ -199,3 +212,24 @@ function SidebarKeyboardShortcut(): JSX.Element {
</div> </div>
) )
} }
function SidebarOverlay({
className,
isOpen = false,
children,
width,
}: SidebarOverlayProps & { children: React.ReactNode; width: number }): JSX.Element | null {
if (!isOpen) {
return null
}
return (
<div
className={clsx('absolute top-0 left-0 h-full bg-bg-3000', className)}
// eslint-disable-next-line react/forbid-dom-props
style={{ width: `${width}px` }}
>
{children}
</div>
)
}

View File

@ -13,7 +13,14 @@ import { InfiniteLoader } from 'react-virtualized/dist/es/InfiniteLoader'
import { List, ListProps } from 'react-virtualized/dist/es/List' import { List, ListProps } from 'react-virtualized/dist/es/List'
import { ITEM_KEY_PART_SEPARATOR, navigation3000Logic } from '../navigationLogic' import { ITEM_KEY_PART_SEPARATOR, navigation3000Logic } from '../navigationLogic'
import { BasicListItem, ExtendedListItem, ExtraListItemContext, SidebarCategory, TentativeListItem } from '../types' import {
BasicListItem,
ButtonListItem,
ExtendedListItem,
ExtraListItemContext,
SidebarCategory,
TentativeListItem,
} from '../types'
import { KeyboardShortcut } from './KeyboardShortcut' import { KeyboardShortcut } from './KeyboardShortcut'
export function SidebarList({ category }: { category: SidebarCategory }): JSX.Element { export function SidebarList({ category }: { category: SidebarCategory }): JSX.Element {
@ -122,7 +129,7 @@ export function SidebarList({ category }: { category: SidebarCategory }): JSX.El
} }
interface SidebarListItemProps { interface SidebarListItemProps {
item: BasicListItem | ExtendedListItem | TentativeListItem item: BasicListItem | ExtendedListItem | TentativeListItem | ButtonListItem
validateName?: SidebarCategory['validateName'] validateName?: SidebarCategory['validateName']
active?: boolean active?: boolean
style: React.CSSProperties style: React.CSSProperties
@ -132,6 +139,10 @@ function isItemTentative(item: SidebarListItemProps['item']): item is TentativeL
return 'onSave' in item return 'onSave' in item
} }
function isItemClickable(item: SidebarListItemProps['item']): item is ButtonListItem {
return 'onClick' in item
}
function SidebarListItem({ item, validateName, active, style }: SidebarListItemProps): JSX.Element { function SidebarListItem({ item, validateName, active, style }: SidebarListItemProps): JSX.Element {
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const [newName, setNewName] = useState<null | string>(null) const [newName, setNewName] = useState<null | string>(null)
@ -218,7 +229,13 @@ function SidebarListItem({ item, validateName, active, style }: SidebarListItemP
}) // Intentionally run on every render so that ref value changes are picked up }) // Intentionally run on every render so that ref value changes are picked up
let content: JSX.Element let content: JSX.Element
if (!save || (!isItemTentative(item) && newName === null)) { if (isItemClickable(item)) {
content = (
<li className="SidebarListItem__button" onClick={item.onClick}>
<h5>{item.name}</h5>
</li>
)
} else if (!save || (!isItemTentative(item) && newName === null)) {
if (isItemTentative(item)) { if (isItemTentative(item)) {
throw new Error('Tentative items should not be rendered in read mode') throw new Error('Tentative items should not be rendered in read mode')
} }

View File

@ -31,6 +31,7 @@ import { LemonMenuOverlay } from 'lib/lemon-ui/LemonMenu/LemonMenu'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { isNotNil } from 'lib/utils' import { isNotNil } from 'lib/utils'
import React from 'react' import React from 'react'
import { editorSidebarLogic } from 'scenes/data-warehouse/editor/editorSidebarLogic'
import { sceneLogic } from 'scenes/sceneLogic' import { sceneLogic } from 'scenes/sceneLogic'
import { Scene } from 'scenes/sceneTypes' import { Scene } from 'scenes/sceneTypes'
import { teamLogic } from 'scenes/teamLogic' import { teamLogic } from 'scenes/teamLogic'
@ -103,9 +104,6 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
reducers({ reducers({
isSidebarShown: [ isSidebarShown: [
true, true,
{
persist: true,
},
{ {
hideSidebar: () => false, hideSidebar: () => false,
showSidebar: () => true, showSidebar: () => true,
@ -514,9 +512,10 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
featureFlags[FEATURE_FLAGS.SQL_EDITOR] featureFlags[FEATURE_FLAGS.SQL_EDITOR]
? { ? {
identifier: Scene.SQLEditor, identifier: Scene.SQLEditor,
label: 'SQL editor', label: 'Data warehouse',
icon: <IconServer />, icon: <IconServer />,
to: isUsingSidebar ? undefined : urls.sqlEditor(), to: urls.sqlEditor(),
logic: editorSidebarLogic,
} }
: null, : null,
featureFlags[FEATURE_FLAGS.DATA_MODELING] && hasOnboardedAnyProduct featureFlags[FEATURE_FLAGS.DATA_MODELING] && hasOnboardedAnyProduct
@ -598,6 +597,9 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
activeNavbarItemId: [ activeNavbarItemId: [
(s) => [s.activeNavbarItemIdRaw, featureFlagLogic.selectors.featureFlags], (s) => [s.activeNavbarItemIdRaw, featureFlagLogic.selectors.featureFlags],
(activeNavbarItemIdRaw, featureFlags): string | null => { (activeNavbarItemIdRaw, featureFlags): string | null => {
if (featureFlags[FEATURE_FLAGS.SQL_EDITOR] && activeNavbarItemIdRaw === Scene.SQLEditor) {
return Scene.SQLEditor
}
if (!featureFlags[FEATURE_FLAGS.POSTHOG_3000_NAV]) { if (!featureFlags[FEATURE_FLAGS.POSTHOG_3000_NAV]) {
return null return null
} }

View File

@ -104,6 +104,7 @@ export interface BasicListItem {
* URL within the app. In specific cases this can be null - such items are italicized. * URL within the app. In specific cases this can be null - such items are italicized.
*/ */
url: string | null url: string | null
onClick?: () => void
/** An optional marker to highlight item state. */ /** An optional marker to highlight item state. */
marker?: { marker?: {
/** A marker of type `fold` is a small triangle in the top left, `ribbon` is a narrow ribbon to the left. */ /** A marker of type `fold` is a small triangle in the top left, `ribbon` is a narrow ribbon to the left. */
@ -146,3 +147,8 @@ export interface TentativeListItem {
adding: boolean adding: boolean
ref?: BasicListItem['ref'] ref?: BasicListItem['ref']
} }
export interface ButtonListItem extends BasicListItem {
key: '__button__'
onClick: () => void
}

View File

@ -2206,7 +2206,7 @@ const api = {
}, },
async update( async update(
viewId: DataWarehouseSavedQuery['id'], viewId: DataWarehouseSavedQuery['id'],
data: Pick<DataWarehouseSavedQuery, 'name' | 'query'> data: Partial<DataWarehouseSavedQuery>
): Promise<DataWarehouseSavedQuery> { ): Promise<DataWarehouseSavedQuery> {
return await new ApiRequest().dataWarehouseSavedQuery(viewId).update({ data }) return await new ApiRequest().dataWarehouseSavedQuery(viewId).update({ data })
}, },

View File

@ -12,6 +12,7 @@ export type LemonFormDialogProps = LemonDialogFormPropsType &
Omit<LemonDialogProps, 'primaryButton' | 'secondaryButton' | 'tertiaryButton'> & { Omit<LemonDialogProps, 'primaryButton' | 'secondaryButton' | 'tertiaryButton'> & {
initialValues: Record<string, any> initialValues: Record<string, any>
onSubmit: (values: Record<string, any>) => void | Promise<void> onSubmit: (values: Record<string, any>) => void | Promise<void>
shouldAwaitSubmit?: boolean
} }
export type LemonDialogProps = Pick< export type LemonDialogProps = Pick<
@ -26,6 +27,7 @@ export type LemonDialogProps = Pick<
onClose?: () => void onClose?: () => void
onAfterClose?: () => void onAfterClose?: () => void
closeOnNavigate?: boolean closeOnNavigate?: boolean
shouldAwaitSubmit?: boolean
} }
export function LemonDialog({ export function LemonDialog({
@ -37,12 +39,14 @@ export function LemonDialog({
content, content,
initialFormValues, initialFormValues,
closeOnNavigate = true, closeOnNavigate = true,
shouldAwaitSubmit = false,
footer, footer,
...props ...props
}: LemonDialogProps): JSX.Element { }: LemonDialogProps): JSX.Element {
const [isOpen, setIsOpen] = useState(true) const [isOpen, setIsOpen] = useState(true)
const { currentLocation } = useValues(router) const { currentLocation } = useValues(router)
const lastLocation = useRef(currentLocation.pathname) const lastLocation = useRef(currentLocation.pathname)
const [isLoading, setIsLoading] = useState(false)
primaryButton = primaryButton =
primaryButton || primaryButton ||
@ -63,8 +67,20 @@ export function LemonDialog({
<LemonButton <LemonButton
type="secondary" type="secondary"
{...button} {...button}
onClick={(e) => { loading={button === primaryButton && shouldAwaitSubmit ? isLoading : undefined}
button.onClick?.(e) // eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={async (e) => {
if (button === primaryButton && shouldAwaitSubmit) {
setIsLoading(true)
try {
// eslint-disable-next-line @typescript-eslint/await-thenable
await button.onClick?.(e)
} finally {
setIsLoading(false)
}
} else {
button.onClick?.(e)
}
setIsOpen(false) setIsOpen(false)
}} }}
/> />
@ -117,7 +133,8 @@ export const LemonFormDialog = ({
type: 'primary', type: 'primary',
children: 'Submit', children: 'Submit',
htmlType: 'submit', htmlType: 'submit',
onClick: () => void onSubmit(form), // eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: props.shouldAwaitSubmit ? async () => await onSubmit(form) : () => void onSubmit(form),
disabledReason: !isFormValid ? firstError : undefined, disabledReason: !isFormValid ? firstError : undefined,
} }

View File

@ -1,14 +1,23 @@
import { BindLogic } from 'kea' import { IconArrowLeft } from '@posthog/icons'
import { BindLogic, useActions, useValues } from 'kea'
import { CopyToClipboardInline } from 'lib/components/CopyToClipboard'
import { DatabaseTableTree } from 'lib/components/DatabaseTableTree/DatabaseTableTree'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { useRef } from 'react' import { useRef } from 'react'
import { Sidebar } from '~/layout/navigation-3000/components/Sidebar'
import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic'
import { editorSceneLogic } from './editorSceneLogic'
import { editorSizingLogic } from './editorSizingLogic' import { editorSizingLogic } from './editorSizingLogic'
import { QueryWindow } from './QueryWindow' import { QueryWindow } from './QueryWindow'
import { SourceNavigator } from './SourceNavigator'
export function EditorScene(): JSX.Element { export function EditorScene(): JSX.Element {
const ref = useRef(null) const ref = useRef(null)
const navigatorRef = useRef(null) const navigatorRef = useRef(null)
const queryPaneRef = useRef(null) const queryPaneRef = useRef(null)
const { activeNavbarItem } = useValues(navigation3000Logic)
const { sidebarOverlayOpen } = useValues(editorSceneLogic)
const editorSizingLogicProps = { const editorSizingLogicProps = {
editorSceneRef: ref, editorSceneRef: ref,
@ -28,9 +37,41 @@ export function EditorScene(): JSX.Element {
return ( return (
<BindLogic logic={editorSizingLogic} props={editorSizingLogicProps}> <BindLogic logic={editorSizingLogic} props={editorSizingLogicProps}>
<div className="w-full h-full flex flex-row overflow-hidden" ref={ref}> <div className="w-full h-full flex flex-row overflow-hidden" ref={ref}>
<SourceNavigator /> {activeNavbarItem && (
<Sidebar
key={activeNavbarItem.identifier}
navbarItem={activeNavbarItem}
sidebarOverlay={<EditorSidebarOverlay />}
sidebarOverlayProps={{ isOpen: sidebarOverlayOpen }}
/>
)}
<QueryWindow /> <QueryWindow />
</div> </div>
</BindLogic> </BindLogic>
) )
} }
const EditorSidebarOverlay = (): JSX.Element => {
const { setSidebarOverlayOpen } = useActions(editorSceneLogic)
const { sidebarOverlayTreeItems, selectedSchema } = useValues(editorSceneLogic)
return (
<div className="flex flex-col">
<header className="flex flex-row h-10 border-b shrink-0 p-1 gap-2">
<LemonButton size="small" icon={<IconArrowLeft />} onClick={() => setSidebarOverlayOpen(false)} />
{selectedSchema?.name && (
<CopyToClipboardInline
className="font-mono"
tooltipMessage={null}
description="schema"
iconStyle={{ color: 'var(--muted-alt)' }}
explicitValue={selectedSchema?.name}
>
{selectedSchema?.name}
</CopyToClipboardInline>
)}
</header>
<DatabaseTableTree items={sidebarOverlayTreeItems} />
</div>
)
}

View File

@ -1,6 +1,5 @@
import { useValues } from 'kea' import { useValues } from 'kea'
import { Resizer } from 'lib/components/Resizer/Resizer' import { Resizer } from 'lib/components/Resizer/Resizer'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { CodeEditor, CodeEditorProps } from 'lib/monaco/CodeEditor' import { CodeEditor, CodeEditorProps } from 'lib/monaco/CodeEditor'
import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer'
@ -16,43 +15,44 @@ export function QueryPane(props: QueryPaneProps): JSX.Element {
const { queryPaneHeight, queryPaneResizerProps } = useValues(editorSizingLogic) const { queryPaneHeight, queryPaneResizerProps } = useValues(editorSizingLogic)
return ( return (
<div <>
className="relative flex flex-col w-full bg-bg-3000" <div
// eslint-disable-next-line react/forbid-dom-props className="relative flex flex-col w-full bg-bg-3000"
style={{ // eslint-disable-next-line react/forbid-dom-props
height: `${queryPaneHeight}px`, style={{
}} height: `${queryPaneHeight}px`,
ref={queryPaneResizerProps.containerRef} }}
> ref={queryPaneResizerProps.containerRef}
<div className="flex-1"> >
{props.promptError ? <LemonBanner type="warning">{props.promptError}</LemonBanner> : null} <div className="flex-1">
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<CodeEditor <CodeEditor
className="border" className="border"
language="hogQL" language="hogQL"
value={props.queryInput} value={props.queryInput}
height={height} height={height}
width={width} width={width}
{...props.codeEditorProps} {...props.codeEditorProps}
options={{ options={{
minimap: { minimap: {
enabled: false, enabled: false,
}, },
wordWrap: 'on', wordWrap: 'on',
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
automaticLayout: true, automaticLayout: true,
fixedOverflowWidgets: true, fixedOverflowWidgets: true,
suggest: { suggest: {
showInlineDetails: true, showInlineDetails: true,
}, },
quickSuggestionsDelay: 300, quickSuggestionsDelay: 300,
}} }}
/> />
)} )}
</AutoSizer> </AutoSizer>
</div>
<Resizer {...queryPaneResizerProps} />
</div> </div>
<Resizer {...queryPaneResizerProps} /> </>
</div>
) )
} }

View File

@ -1,41 +1,42 @@
import { IconPlus, IconX } from '@posthog/icons' import { IconPlus, IconX } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui' import { LemonButton } from '@posthog/lemon-ui'
import clsx from 'clsx' import clsx from 'clsx'
import { Uri } from 'monaco-editor'
import { QueryTab } from './multitabEditorLogic'
interface QueryTabsProps { interface QueryTabsProps {
models: Uri[] models: QueryTab[]
onClick: (model: Uri) => void onClick: (model: QueryTab) => void
onClear: (model: Uri) => void onClear: (model: QueryTab) => void
onAdd: () => void onAdd: () => void
activeModelUri: Uri | null activeModelUri: QueryTab | null
} }
export function QueryTabs({ models, onClear, onClick, onAdd, activeModelUri }: QueryTabsProps): JSX.Element { export function QueryTabs({ models, onClear, onClick, onAdd, activeModelUri }: QueryTabsProps): JSX.Element {
return ( return (
<div className="flex flex-row overflow-scroll hide-scrollbar"> <div className="flex flex-row overflow-scroll hide-scrollbar h-10">
{models.map((model: Uri) => ( {models.map((model: QueryTab) => (
<QueryTab <QueryTabComponent
key={model.path} key={model.uri.path}
model={model} model={model}
onClear={models.length > 1 ? onClear : undefined} onClear={models.length > 1 ? onClear : undefined}
onClick={onClick} onClick={onClick}
active={activeModelUri?.path === model.path} active={activeModelUri?.uri.path === model.uri.path}
/> />
))} ))}
<LemonButton onClick={onAdd} icon={<IconPlus fontSize={14} />} /> <LemonButton onClick={() => onAdd()} icon={<IconPlus fontSize={14} />} />
</div> </div>
) )
} }
interface QueryTabProps { interface QueryTabProps {
model: Uri model: QueryTab
onClick: (model: Uri) => void onClick: (model: QueryTab) => void
onClear?: (model: Uri) => void onClear?: (model: QueryTab) => void
active: boolean active: boolean
} }
function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Element { function QueryTabComponent({ model, active, onClear, onClick }: QueryTabProps): JSX.Element {
return ( return (
<button <button
onClick={() => onClick?.(model)} onClick={() => onClick?.(model)}
@ -45,7 +46,7 @@ function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Eleme
onClear ? 'pl-3 pr-2' : 'px-3' onClear ? 'pl-3 pr-2' : 'px-3'
)} )}
> >
Untitled {model.view?.name ?? 'Untitled'}
{onClear && ( {onClear && (
<LemonButton <LemonButton
onClick={(e) => { onClick={(e) => {

View File

@ -22,8 +22,17 @@ export function QueryWindow(): JSX.Element {
monaco, monaco,
editor, editor,
}) })
const { allTabs, activeModelUri, queryInput, activeQuery, activeTabKey, hasErrors, error, isValidView } = const {
useValues(logic) allTabs,
activeModelUri,
queryInput,
activeQuery,
activeTabKey,
hasErrors,
error,
isValidView,
editingView,
} = useValues(logic)
const { selectTab, deleteTab, createTab, setQueryInput, runQuery, saveAsView } = useActions(logic) const { selectTab, deleteTab, createTab, setQueryInput, runQuery, saveAsView } = useActions(logic)
return ( return (
@ -35,6 +44,11 @@ export function QueryWindow(): JSX.Element {
onAdd={createTab} onAdd={createTab}
activeModelUri={activeModelUri} activeModelUri={activeModelUri}
/> />
{editingView && (
<div className="h-7 bg-warning-highlight p-1">
<span> Editing view "{editingView.name}"</span>
</div>
)}
<QueryPane <QueryPane
queryInput={queryInput} queryInput={queryInput}
promptError={null} promptError={null}

View File

@ -1,14 +1,19 @@
import 'react-data-grid/lib/styles.css' import 'react-data-grid/lib/styles.css'
import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui' import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui'
import { useValues } from 'kea' import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { useMemo } from 'react' import { useMemo } from 'react'
import DataGrid from 'react-data-grid' import DataGrid from 'react-data-grid'
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
import { themeLogic } from '~/layout/navigation-3000/themeLogic' import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic'
import { NodeKind } from '~/queries/schema' import { NodeKind } from '~/queries/schema'
import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic'
import { multitabEditorLogic } from './multitabEditorLogic'
enum ResultsTab { enum ResultsTab {
Results = 'results', Results = 'results',
Visualization = 'visualization', Visualization = 'visualization',
@ -29,6 +34,13 @@ export function ResultPane({
logicKey, logicKey,
query, query,
}: ResultPaneProps): JSX.Element { }: ResultPaneProps): JSX.Element {
const codeEditorKey = `hogQLQueryEditor/${router.values.location.pathname}`
const { editingView, queryInput } = useValues(
multitabEditorLogic({
key: codeEditorKey,
})
)
const { isDarkModeOn } = useValues(themeLogic) const { isDarkModeOn } = useValues(themeLogic)
const { response, responseLoading } = useValues( const { response, responseLoading } = useValues(
dataNodeLogic({ dataNodeLogic({
@ -40,6 +52,8 @@ export function ResultPane({
doNotLoad: !query, doNotLoad: !query,
}) })
) )
const { dataWarehouseSavedQueriesLoading } = useValues(dataWarehouseViewsLogic)
const { updateDataWarehouseSavedQuery } = useActions(dataWarehouseViewsLogic)
const columns = useMemo(() => { const columns = useMemo(() => {
return ( return (
@ -78,11 +92,32 @@ export function ResultPane({
]} ]}
/> />
<div className="flex gap-1"> <div className="flex gap-1">
<LemonButton type="secondary" onClick={() => onSave()} disabledReason={saveDisabledReason}> {editingView ? (
Save <>
</LemonButton> <LemonButton
<LemonButton type="primary" onClick={() => onQueryInputChange()}> loading={dataWarehouseSavedQueriesLoading}
Run type="secondary"
onClick={() =>
updateDataWarehouseSavedQuery({
id: editingView.id,
query: {
kind: NodeKind.HogQLQuery,
query: queryInput,
},
})
}
>
Update
</LemonButton>
</>
) : (
<LemonButton type="secondary" onClick={() => onSave()} disabledReason={saveDisabledReason}>
Save
</LemonButton>
)}
<LemonButton loading={responseLoading} type="primary" onClick={() => onQueryInputChange()}>
<span className="mr-1">Run</span>
<KeyboardShortcut command enter />
</LemonButton> </LemonButton>
</div> </div>
</div> </div>

View File

@ -1,25 +0,0 @@
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,56 @@
import { actions, kea, path, reducers, selectors } from 'kea'
import { TreeItem } from 'lib/components/DatabaseTableTree/DatabaseTableTree'
import { DatabaseSchemaDataWarehouseTable, DatabaseSchemaTable } from '~/queries/schema'
import { DataWarehouseSavedQuery } from '~/types'
import type { editorSceneLogicType } from './editorSceneLogicType'
export const editorSceneLogic = kea<editorSceneLogicType>([
path(['scenes', 'data-warehouse', 'editor', 'editorSceneLogic']),
actions({
setSidebarOverlayOpen: (isOpen: boolean) => ({ isOpen }),
selectSchema: (schema: DatabaseSchemaDataWarehouseTable | DatabaseSchemaTable | DataWarehouseSavedQuery) => ({
schema,
}),
}),
reducers({
sidebarOverlayOpen: [
false,
{
setSidebarOverlayOpen: (_, { isOpen }) => isOpen,
selectSchema: (_, { schema }) => schema !== null,
},
],
selectedSchema: [
null as DatabaseSchemaDataWarehouseTable | DatabaseSchemaTable | DataWarehouseSavedQuery | null,
{
selectSchema: (_, { schema }) => schema,
},
],
}),
selectors({
sidebarOverlayTreeItems: [
(s) => [s.selectedSchema],
(selectedSchema): TreeItem[] => {
if (selectedSchema === null) {
return []
}
if ('fields' in selectedSchema) {
return Object.values(selectedSchema.fields).map((field) => ({
name: field.name,
type: field.type,
}))
}
if ('columns' in selectedSchema) {
return Object.values(selectedSchema.columns).map((column) => ({
name: column.name,
type: column.type,
}))
}
return []
},
],
}),
])

View File

@ -0,0 +1,203 @@
import Fuse from 'fuse.js'
import { connect, kea, path, selectors } from 'kea'
import { router } from 'kea-router'
import { subscriptions } from 'kea-subscriptions'
import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic'
import { sceneLogic } from 'scenes/sceneLogic'
import { Scene } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic'
import { FuseSearchMatch } from '~/layout/navigation-3000/sidebars/utils'
import { SidebarCategory } from '~/layout/navigation-3000/types'
import { DatabaseSchemaDataWarehouseTable, DatabaseSchemaTable } from '~/queries/schema'
import { DataWarehouseSavedQuery, PipelineTab } from '~/types'
import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic'
import { editorSceneLogic } from './editorSceneLogic'
import type { editorSidebarLogicType } from './editorSidebarLogicType'
import { multitabEditorLogic } from './multitabEditorLogic'
const dataWarehouseTablesfuse = new Fuse<DatabaseSchemaDataWarehouseTable>([], {
keys: [{ name: 'name', weight: 2 }],
threshold: 0.3,
ignoreLocation: true,
includeMatches: true,
})
const posthogTablesfuse = new Fuse<DatabaseSchemaTable>([], {
keys: [{ name: 'name', weight: 2 }],
threshold: 0.3,
ignoreLocation: true,
includeMatches: true,
})
const savedQueriesfuse = new Fuse<DataWarehouseSavedQuery>([], {
keys: [{ name: 'name', weight: 2 }],
threshold: 0.3,
ignoreLocation: true,
includeMatches: true,
})
export const editorSidebarLogic = kea<editorSidebarLogicType>([
path(['data-warehouse', 'editor', 'editorSidebarLogic']),
connect({
values: [
sceneLogic,
['activeScene', 'sceneParams'],
dataWarehouseViewsLogic,
['dataWarehouseSavedQueries', 'dataWarehouseSavedQueryMapById', 'dataWarehouseSavedQueriesLoading'],
databaseTableListLogic,
['posthogTables', 'dataWarehouseTables', 'databaseLoading', 'views', 'viewsMapById'],
],
actions: [editorSceneLogic, ['selectSchema'], dataWarehouseViewsLogic, ['deleteDataWarehouseSavedQuery']],
}),
selectors(({ actions }) => ({
contents: [
(s) => [
s.relevantSavedQueries,
s.dataWarehouseSavedQueriesLoading,
s.relevantPosthogTables,
s.relevantDataWarehouseTables,
s.databaseLoading,
],
(
relevantSavedQueries,
dataWarehouseSavedQueriesLoading,
relevantPosthogTables,
relevantDataWarehouseTables,
databaseLoading
) => [
{
key: 'data-warehouse-sources',
noun: ['source', 'external source'],
loading: databaseLoading,
items: relevantDataWarehouseTables.map(([table, matches]) => ({
key: table.id,
name: table.name,
url: '',
searchMatch: matches
? {
matchingFields: matches.map((match) => match.key),
nameHighlightRanges: matches.find((match) => match.key === 'name')?.indices,
}
: null,
onClick: () => {
actions.selectSchema(table)
},
})),
onAdd: () => {
router.actions.push(urls.pipeline(PipelineTab.Sources))
},
} as SidebarCategory,
{
key: 'data-warehouse-tables',
noun: ['table', 'tables'],
loading: databaseLoading,
items: relevantPosthogTables.map(([table, matches]) => ({
key: table.id,
name: table.name,
url: '',
searchMatch: matches
? {
matchingFields: matches.map((match) => match.key),
nameHighlightRanges: matches.find((match) => match.key === 'name')?.indices,
}
: null,
onClick: () => {
actions.selectSchema(table)
},
})),
} as SidebarCategory,
{
key: 'data-warehouse-views',
noun: ['view', 'views'],
loading: dataWarehouseSavedQueriesLoading,
items: relevantSavedQueries.map(([savedQuery, matches]) => ({
key: savedQuery.id,
name: savedQuery.name,
url: '',
searchMatch: matches
? {
matchingFields: matches.map((match) => match.key),
nameHighlightRanges: matches.find((match) => match.key === 'name')?.indices,
}
: null,
onClick: () => {
actions.selectSchema(savedQuery)
},
menuItems: [
{
label: 'Edit view definition',
onClick: () => {
multitabEditorLogic({
key: `hogQLQueryEditor/${router.values.location.pathname}`,
}).actions.createTab(savedQuery.query.query, savedQuery)
},
},
{
label: 'Delete',
status: 'danger',
onClick: () => {
actions.deleteDataWarehouseSavedQuery(savedQuery.id)
},
},
],
})),
} as SidebarCategory,
],
],
activeListItemKey: [
(s) => [s.activeScene, s.sceneParams],
(activeScene, sceneParams): [string, number] | null => {
return activeScene === Scene.DataWarehouse && sceneParams.params.id
? ['saved-queries', parseInt(sceneParams.params.id)]
: null
},
],
relevantDataWarehouseTables: [
(s) => [s.dataWarehouseTables, navigation3000Logic.selectors.searchTerm],
(dataWarehouseTables, searchTerm): [DatabaseSchemaDataWarehouseTable, FuseSearchMatch[] | null][] => {
if (searchTerm) {
return dataWarehouseTablesfuse
.search(searchTerm)
.map((result) => [result.item, result.matches as FuseSearchMatch[]])
}
return dataWarehouseTables.map((table) => [table, null])
},
],
relevantPosthogTables: [
(s) => [s.posthogTables, navigation3000Logic.selectors.searchTerm],
(posthogTables, searchTerm): [DatabaseSchemaTable, FuseSearchMatch[] | null][] => {
if (searchTerm) {
return posthogTablesfuse
.search(searchTerm)
.map((result) => [result.item, result.matches as FuseSearchMatch[]])
}
return posthogTables.map((table) => [table, null])
},
],
relevantSavedQueries: [
(s) => [s.dataWarehouseSavedQueries, navigation3000Logic.selectors.searchTerm],
(dataWarehouseSavedQueries, searchTerm): [DataWarehouseSavedQuery, FuseSearchMatch[] | null][] => {
if (searchTerm) {
return savedQueriesfuse
.search(searchTerm)
.map((result) => [result.item, result.matches as FuseSearchMatch[]])
}
return dataWarehouseSavedQueries.map((savedQuery) => [savedQuery, null])
},
],
})),
subscriptions({
dataWarehouseTables: (dataWarehouseTables) => {
dataWarehouseTablesfuse.setCollection(dataWarehouseTables)
},
posthogTables: (posthogTables) => {
posthogTablesfuse.setCollection(posthogTables)
},
dataWarehouseSavedQueries: (dataWarehouseSavedQueries) => {
savedQueriesfuse.setCollection(dataWarehouseSavedQueries)
},
}),
])

View File

@ -1,6 +1,6 @@
import { Monaco } from '@monaco-editor/react' import { Monaco } from '@monaco-editor/react'
import { LemonDialog, LemonInput } from '@posthog/lemon-ui' import { LemonDialog, LemonInput, lemonToast } from '@posthog/lemon-ui'
import { actions, kea, listeners, path, props, propsChanged, reducers, selectors } from 'kea' import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
import { subscriptions } from 'kea-subscriptions' import { subscriptions } from 'kea-subscriptions'
import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonField } from 'lib/lemon-ui/LemonField'
import { ModelMarker } from 'lib/monaco/codeEditorLogic' import { ModelMarker } from 'lib/monaco/codeEditorLogic'
@ -9,6 +9,7 @@ import { editor, MarkerSeverity, Uri } from 'monaco-editor'
import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic' import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic'
import { performQuery } from '~/queries/query' import { performQuery } from '~/queries/query'
import { HogLanguage, HogQLMetadata, HogQLMetadataResponse, HogQLNotice, HogQLQuery, NodeKind } from '~/queries/schema' import { HogLanguage, HogQLMetadata, HogQLMetadataResponse, HogQLNotice, HogQLQuery, NodeKind } from '~/queries/schema'
import { DataWarehouseSavedQuery } from '~/types'
import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic' import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic'
import type { multitabEditorLogicType } from './multitabEditorLogicType' import type { multitabEditorLogicType } from './multitabEditorLogicType'
@ -22,29 +23,41 @@ export interface MultitabEditorLogicProps {
export const editorModelsStateKey = (key: string | number): string => `${key}/editorModelQueries` export const editorModelsStateKey = (key: string | number): string => `${key}/editorModelQueries`
export const activemodelStateKey = (key: string | number): string => `${key}/activeModelUri` export const activemodelStateKey = (key: string | number): string => `${key}/activeModelUri`
export interface QueryTab {
uri: Uri
view?: DataWarehouseSavedQuery
}
export const multitabEditorLogic = kea<multitabEditorLogicType>([ export const multitabEditorLogic = kea<multitabEditorLogicType>([
path(['data-warehouse', 'editor', 'multitabEditorLogic']), path(['data-warehouse', 'editor', 'multitabEditorLogic']),
props({} as MultitabEditorLogicProps), props({} as MultitabEditorLogicProps),
key((props) => props.key),
connect({
actions: [
dataWarehouseViewsLogic,
['deleteDataWarehouseSavedQuerySuccess', 'createDataWarehouseSavedQuerySuccess'],
],
}),
actions({ actions({
setQueryInput: (queryInput: string) => ({ queryInput }), setQueryInput: (queryInput: string) => ({ queryInput }),
updateState: true, updateState: true,
runQuery: (queryOverride?: string) => ({ queryOverride }), runQuery: (queryOverride?: string) => ({ queryOverride }),
setActiveQuery: (query: string) => ({ query }), setActiveQuery: (query: string) => ({ query }),
setTabs: (tabs: Uri[]) => ({ tabs }), setTabs: (tabs: QueryTab[]) => ({ tabs }),
addTab: (tab: Uri) => ({ tab }), addTab: (tab: QueryTab) => ({ tab }),
createTab: () => null, createTab: (query?: string, view?: DataWarehouseSavedQuery) => ({ query, view }),
deleteTab: (tab: Uri) => ({ tab }), deleteTab: (tab: QueryTab) => ({ tab }),
removeTab: (tab: Uri) => ({ tab }), removeTab: (tab: QueryTab) => ({ tab }),
selectTab: (tab: Uri) => ({ tab }), selectTab: (tab: QueryTab) => ({ tab }),
setLocalState: (key: string, value: any) => ({ key, value }), setLocalState: (key: string, value: any) => ({ key, value }),
initialize: true, initialize: true,
saveAsView: true, saveAsView: true,
saveAsViewSuccess: (name: string) => ({ name }), saveAsViewSubmit: (name: string) => ({ name }),
reloadMetadata: true, reloadMetadata: true,
setMetadata: (query: string, metadata: HogQLMetadataResponse) => ({ query, metadata }), setMetadata: (query: string, metadata: HogQLMetadataResponse) => ({ query, metadata }),
}), }),
propsChanged(({ actions }, oldProps) => { propsChanged(({ actions, props }, oldProps) => {
if (!oldProps.monaco && !oldProps.editor) { if (!oldProps.monaco && !oldProps.editor && props.monaco && props.editor) {
actions.initialize() actions.initialize()
} }
}), }),
@ -62,20 +75,26 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
}, },
], ],
activeModelUri: [ activeModelUri: [
null as Uri | null, null as QueryTab | null,
{ {
selectTab: (_, { tab }) => tab, selectTab: (_, { tab }) => tab,
}, },
], ],
editingView: [
null as DataWarehouseSavedQuery | null,
{
selectTab: (_, { tab }) => tab.view ?? null,
},
],
allTabs: [ allTabs: [
[] as Uri[], [] as QueryTab[],
{ {
addTab: (state, { tab }) => { addTab: (state, { tab }) => {
const newTabs = [...state, tab] const newTabs = [...state, tab]
return newTabs return newTabs
}, },
removeTab: (state, { tab: tabToRemove }) => { removeTab: (state, { tab: tabToRemove }) => {
const newModels = state.filter((tab) => tab.toString() !== tabToRemove.toString()) const newModels = state.filter((tab) => tab.uri.toString() !== tabToRemove.uri.toString())
return newModels return newModels
}, },
setTabs: (_, { tabs }) => tabs, setTabs: (_, { tabs }) => tabs,
@ -130,25 +149,32 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
}, },
], ],
})), })),
listeners(({ values, props, actions }) => ({ listeners(({ values, props, actions, asyncActions }) => ({
createTab: () => { createTab: ({ query = '', view }) => {
let currentModelCount = 1 let currentModelCount = 1
const allNumbers = values.allTabs.map((tab) => parseInt(tab.path.split('/').pop() || '0')) const allNumbers = values.allTabs.map((tab) => parseInt(tab.uri.path.split('/').pop() || '0'))
while (allNumbers.includes(currentModelCount)) { while (allNumbers.includes(currentModelCount)) {
currentModelCount++ currentModelCount++
} }
if (props.monaco) { if (props.monaco) {
const uri = props.monaco.Uri.parse(currentModelCount.toString()) const uri = props.monaco.Uri.parse(currentModelCount.toString())
const model = props.monaco.editor.createModel('', 'hogQL', uri) const model = props.monaco.editor.createModel(query, 'hogQL', uri)
props.editor?.setModel(model) props.editor?.setModel(model)
actions.addTab(uri) actions.addTab({
actions.selectTab(uri) uri,
view,
})
actions.selectTab({
uri,
view,
})
const queries = values.allTabs.map((tab) => { const queries = values.allTabs.map((tab) => {
return { return {
query: props.monaco?.editor.getModel(tab)?.getValue() || '', query: props.monaco?.editor.getModel(tab.uri)?.getValue() || '',
path: tab.path.split('/').pop(), path: tab.uri.path.split('/').pop(),
view: uri.path === tab.uri.path ? view : tab.view,
} }
}) })
actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries)) actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries))
@ -156,18 +182,20 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
}, },
selectTab: ({ tab }) => { selectTab: ({ tab }) => {
if (props.monaco) { if (props.monaco) {
const model = props.monaco.editor.getModel(tab) const model = props.monaco.editor.getModel(tab.uri)
props.editor?.setModel(model) props.editor?.setModel(model)
} }
const path = tab.path.split('/').pop() const path = tab.uri.path.split('/').pop()
path && actions.setLocalState(activemodelStateKey(props.key), path) path && actions.setLocalState(activemodelStateKey(props.key), path)
}, },
deleteTab: ({ tab: tabToRemove }) => { deleteTab: ({ tab: tabToRemove }) => {
if (props.monaco) { if (props.monaco) {
const model = props.monaco.editor.getModel(tabToRemove) const model = props.monaco.editor.getModel(tabToRemove.uri)
if (tabToRemove == values.activeModelUri) { if (tabToRemove.uri.toString() === values.activeModelUri?.uri.toString()) {
const indexOfModel = values.allTabs.findIndex((tab) => tab.toString() === tabToRemove.toString()) const indexOfModel = values.allTabs.findIndex(
(tab) => tab.uri.toString() === tabToRemove.uri.toString()
)
const nextModel = const nextModel =
values.allTabs[indexOfModel + 1] || values.allTabs[indexOfModel - 1] || values.allTabs[0] // there will always be one values.allTabs[indexOfModel + 1] || values.allTabs[indexOfModel - 1] || values.allTabs[0] // there will always be one
actions.selectTab(nextModel) actions.selectTab(nextModel)
@ -176,8 +204,9 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
actions.removeTab(tabToRemove) actions.removeTab(tabToRemove)
const queries = values.allTabs.map((tab) => { const queries = values.allTabs.map((tab) => {
return { return {
query: props.monaco?.editor.getModel(tab)?.getValue() || '', query: props.monaco?.editor.getModel(tab.uri)?.getValue() || '',
path: tab.path.split('/').pop(), path: tab.uri.path.split('/').pop(),
view: tab.view,
} }
}) })
actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries)) actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries))
@ -197,14 +226,17 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
}) })
const models = JSON.parse(allModelQueries || '[]') const models = JSON.parse(allModelQueries || '[]')
const newModels: Uri[] = [] const newModels: QueryTab[] = []
models.forEach((model: Record<string, any>) => { models.forEach((model: Record<string, any>) => {
if (props.monaco) { if (props.monaco) {
const uri = props.monaco.Uri.parse(model.path) const uri = props.monaco.Uri.parse(model.path)
const newModel = props.monaco.editor.createModel(model.query, 'hogQL', uri) const newModel = props.monaco.editor.createModel(model.query, 'hogQL', uri)
props.editor?.setModel(newModel) props.editor?.setModel(newModel)
newModels.push(uri) newModels.push({
uri,
view: model.view,
})
} }
}) })
@ -221,9 +253,17 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
actions.setQueryInput(val) actions.setQueryInput(val)
actions.runQuery() actions.runQuery()
} }
uri && actions.selectTab(uri) const activeView = newModels.find((tab) => tab.uri.path.split('/').pop() === activeModelUri)?.view
uri &&
actions.selectTab({
uri,
view: activeView,
})
} else if (newModels.length) { } else if (newModels.length) {
actions.selectTab(newModels[0]) actions.selectTab({
uri: newModels[0].uri,
})
} }
} else { } else {
const model = props.editor?.getModel() const model = props.editor?.getModel()
@ -240,13 +280,23 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
await breakpoint(100) await breakpoint(100)
const queries = values.allTabs.map((model) => { const queries = values.allTabs.map((model) => {
return { return {
query: props.monaco?.editor.getModel(model)?.getValue() || '', query: props.monaco?.editor.getModel(model.uri)?.getValue() || '',
path: model.path.split('/').pop(), path: model.uri.path.split('/').pop(),
view: model.view,
} }
}) })
localStorage.setItem(editorModelsStateKey(props.key), JSON.stringify(queries)) localStorage.setItem(editorModelsStateKey(props.key), JSON.stringify(queries))
}, },
runQuery: ({ queryOverride }) => { runQuery: ({ queryOverride }) => {
if (values.activeQuery === queryOverride || values.activeQuery === values.queryInput) {
dataNodeLogic({
key: values.activeTabKey,
query: {
kind: NodeKind.HogQLQuery,
query: queryOverride || values.queryInput,
},
}).actions.loadData(true)
}
actions.setActiveQuery(queryOverride || values.queryInput) actions.setActiveQuery(queryOverride || values.queryInput)
}, },
saveAsView: async () => { saveAsView: async () => {
@ -261,10 +311,13 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
errors: { errors: {
viewName: (name) => (!name ? 'You must enter a name' : undefined), viewName: (name) => (!name ? 'You must enter a name' : undefined),
}, },
onSubmit: ({ viewName }) => actions.saveAsViewSuccess(viewName), onSubmit: async ({ viewName }) => {
await asyncActions.saveAsViewSubmit(viewName)
},
shouldAwaitSubmit: true,
}) })
}, },
saveAsViewSuccess: async ({ name }) => { saveAsViewSubmit: async ({ name }) => {
const query: HogQLQuery = { const query: HogQLQuery = {
kind: NodeKind.HogQLQuery, kind: NodeKind.HogQLQuery,
query: values.queryInput, query: values.queryInput,
@ -290,11 +343,34 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
breakpoint() breakpoint()
actions.setMetadata(query, response) actions.setMetadata(query, response)
}, },
deleteDataWarehouseSavedQuerySuccess: ({ payload: viewId }) => {
const tabToRemove = values.allTabs.find((tab) => tab.view?.id === viewId)
if (tabToRemove) {
actions.deleteTab(tabToRemove)
}
lemonToast.success('View deleted')
},
createDataWarehouseSavedQuerySuccess: ({ dataWarehouseSavedQueries, payload: view }) => {
const newView = view && dataWarehouseSavedQueries.find((v) => v.name === view.name)
if (newView) {
const newTabs = values.allTabs.map((tab) => ({
...tab,
view: tab.uri.path === values.activeModelUri?.uri.path ? newView : tab.view,
}))
const newTab = newTabs.find((tab) => tab.uri.path === values.activeModelUri?.uri.path)
actions.setTabs(newTabs)
newTab && actions.selectTab(newTab)
actions.updateState()
}
},
updateDataWarehouseSavedQuerySuccess: () => {
lemonToast.success('View updated')
},
})), })),
subscriptions(({ props, actions, values }) => ({ subscriptions(({ props, actions, values }) => ({
activeModelUri: (activeModelUri) => { activeModelUri: (activeModelUri) => {
if (props.monaco) { if (props.monaco) {
const _model = props.monaco.editor.getModel(activeModelUri) const _model = props.monaco.editor.getModel(activeModelUri.uri)
const val = _model?.getValue() const val = _model?.getValue()
actions.setQueryInput(val ?? '') actions.setQueryInput(val ?? '')
actions.runQuery() actions.runQuery()
@ -313,7 +389,7 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
}, },
})), })),
selectors({ selectors({
activeTabKey: [(s) => [s.activeModelUri], (activeModelUri) => `hogQLQueryEditor/${activeModelUri?.path}`], activeTabKey: [(s) => [s.activeModelUri], (activeModelUri) => `hogQLQueryEditor/${activeModelUri?.uri.path}`],
isValidView: [(s) => [s.metadata], (metadata) => !!(metadata && metadata[1]?.isValidView)], isValidView: [(s) => [s.metadata], (metadata) => !!(metadata && metadata[1]?.isValidView)],
hasErrors: [ hasErrors: [
(s) => [s.modelMarkers], (s) => [s.modelMarkers],

View File

@ -1,18 +0,0 @@
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

@ -46,7 +46,7 @@ export const dataWarehouseViewsLogic = kea<dataWarehouseViewsLogicType>([
await api.dataWarehouseSavedQueries.delete(viewId) await api.dataWarehouseSavedQueries.delete(viewId)
return values.dataWarehouseSavedQueries.filter((view) => view.id !== viewId) return values.dataWarehouseSavedQueries.filter((view) => view.id !== viewId)
}, },
updateDataWarehouseSavedQuery: async (view: DatabaseSchemaViewTable) => { updateDataWarehouseSavedQuery: async (view: Partial<DatabaseSchemaViewTable> & { id: string }) => {
const newView = await api.dataWarehouseSavedQueries.update(view.id, view) const newView = await api.dataWarehouseSavedQueries.update(view.id, view)
return values.dataWarehouseSavedQueries.map((savedQuery) => { return values.dataWarehouseSavedQueries.map((savedQuery) => {
if (savedQuery.id === view.id) { if (savedQuery.id === view.id) {

View File

@ -43,7 +43,7 @@ function UpdateSourceConnectionFormContainer(props: UpdateSourceConnectionFormCo
<> <>
<span className="block mb-2">Overwrite your existing configuration here</span> <span className="block mb-2">Overwrite your existing configuration here</span>
<Form logic={dataWarehouseSourceSettingsLogic} formKey="sourceConfig" enableFormOnSubmit> <Form logic={dataWarehouseSourceSettingsLogic} formKey="sourceConfig" enableFormOnSubmit>
<SourceFormComponent {...props} jobInputs={source?.job_inputs} /> <SourceFormComponent {...props} />
<div className="mt-4 flex flex-row justify-end gap-2"> <div className="mt-4 flex flex-row justify-end gap-2">
<LemonButton <LemonButton
loading={sourceLoading && !source} loading={sourceLoading && !source}