feat(editor-3000): integrate nav3000 sidebar (#26184)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
@ -175,7 +175,7 @@
|
||||
.Sidebar3000 {
|
||||
--sidebar-slider-padding: 0.125rem;
|
||||
--sidebar-horizontal-padding: 0.5rem;
|
||||
--sidebar-row-height: 2rem;
|
||||
--sidebar-row-height: 2.5rem;
|
||||
--sidebar-background: var(--bg-3000);
|
||||
|
||||
position: relative;
|
||||
@ -451,7 +451,8 @@
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -523,6 +524,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.SidebarListItem__button,
|
||||
.SidebarListItem__link,
|
||||
.SidebarListItem__rename {
|
||||
--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 {
|
||||
// Pseudo-elements don't work on inputs, so we use a wrapper div
|
||||
background: var(--bg-light);
|
||||
|
@ -27,7 +27,7 @@ export function Navbar(): JSX.Element {
|
||||
const { isAccountPopoverOpen, systemStatusHealthy } = useValues(navigationLogic)
|
||||
const { closeAccountPopover, toggleAccountPopover } = useActions(navigationLogic)
|
||||
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 { toggleSearchBar } = useActions(commandBarLogic)
|
||||
|
||||
|
@ -19,8 +19,16 @@ const SEARCH_DEBOUNCE_MS = 300
|
||||
|
||||
interface SidebarProps {
|
||||
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 {
|
||||
@ -81,6 +89,11 @@ export function Sidebar({ navbarItem }: SidebarProps): JSX.Element {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{sidebarOverlay && (
|
||||
<SidebarOverlay {...sidebarOverlayProps} isOpen={isShown && sidebarOverlayProps?.isOpen} width={width}>
|
||||
{sidebarOverlay}
|
||||
</SidebarOverlay>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -199,3 +212,24 @@ function SidebarKeyboardShortcut(): JSX.Element {
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -13,7 +13,14 @@ import { InfiniteLoader } from 'react-virtualized/dist/es/InfiniteLoader'
|
||||
import { List, ListProps } from 'react-virtualized/dist/es/List'
|
||||
|
||||
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'
|
||||
|
||||
export function SidebarList({ category }: { category: SidebarCategory }): JSX.Element {
|
||||
@ -122,7 +129,7 @@ export function SidebarList({ category }: { category: SidebarCategory }): JSX.El
|
||||
}
|
||||
|
||||
interface SidebarListItemProps {
|
||||
item: BasicListItem | ExtendedListItem | TentativeListItem
|
||||
item: BasicListItem | ExtendedListItem | TentativeListItem | ButtonListItem
|
||||
validateName?: SidebarCategory['validateName']
|
||||
active?: boolean
|
||||
style: React.CSSProperties
|
||||
@ -132,6 +139,10 @@ function isItemTentative(item: SidebarListItemProps['item']): item is TentativeL
|
||||
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 {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
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
|
||||
|
||||
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)) {
|
||||
throw new Error('Tentative items should not be rendered in read mode')
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import { LemonMenuOverlay } from 'lib/lemon-ui/LemonMenu/LemonMenu'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { isNotNil } from 'lib/utils'
|
||||
import React from 'react'
|
||||
import { editorSidebarLogic } from 'scenes/data-warehouse/editor/editorSidebarLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
@ -103,9 +104,6 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
|
||||
reducers({
|
||||
isSidebarShown: [
|
||||
true,
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
{
|
||||
hideSidebar: () => false,
|
||||
showSidebar: () => true,
|
||||
@ -514,9 +512,10 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
|
||||
featureFlags[FEATURE_FLAGS.SQL_EDITOR]
|
||||
? {
|
||||
identifier: Scene.SQLEditor,
|
||||
label: 'SQL editor',
|
||||
label: 'Data warehouse',
|
||||
icon: <IconServer />,
|
||||
to: isUsingSidebar ? undefined : urls.sqlEditor(),
|
||||
to: urls.sqlEditor(),
|
||||
logic: editorSidebarLogic,
|
||||
}
|
||||
: null,
|
||||
featureFlags[FEATURE_FLAGS.DATA_MODELING] && hasOnboardedAnyProduct
|
||||
@ -598,6 +597,9 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
|
||||
activeNavbarItemId: [
|
||||
(s) => [s.activeNavbarItemIdRaw, featureFlagLogic.selectors.featureFlags],
|
||||
(activeNavbarItemIdRaw, featureFlags): string | null => {
|
||||
if (featureFlags[FEATURE_FLAGS.SQL_EDITOR] && activeNavbarItemIdRaw === Scene.SQLEditor) {
|
||||
return Scene.SQLEditor
|
||||
}
|
||||
if (!featureFlags[FEATURE_FLAGS.POSTHOG_3000_NAV]) {
|
||||
return null
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ export interface BasicListItem {
|
||||
* URL within the app. In specific cases this can be null - such items are italicized.
|
||||
*/
|
||||
url: string | null
|
||||
onClick?: () => void
|
||||
/** An optional marker to highlight item state. */
|
||||
marker?: {
|
||||
/** 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
|
||||
ref?: BasicListItem['ref']
|
||||
}
|
||||
|
||||
export interface ButtonListItem extends BasicListItem {
|
||||
key: '__button__'
|
||||
onClick: () => void
|
||||
}
|
||||
|
@ -2206,7 +2206,7 @@ const api = {
|
||||
},
|
||||
async update(
|
||||
viewId: DataWarehouseSavedQuery['id'],
|
||||
data: Pick<DataWarehouseSavedQuery, 'name' | 'query'>
|
||||
data: Partial<DataWarehouseSavedQuery>
|
||||
): Promise<DataWarehouseSavedQuery> {
|
||||
return await new ApiRequest().dataWarehouseSavedQuery(viewId).update({ data })
|
||||
},
|
||||
|
@ -12,6 +12,7 @@ export type LemonFormDialogProps = LemonDialogFormPropsType &
|
||||
Omit<LemonDialogProps, 'primaryButton' | 'secondaryButton' | 'tertiaryButton'> & {
|
||||
initialValues: Record<string, any>
|
||||
onSubmit: (values: Record<string, any>) => void | Promise<void>
|
||||
shouldAwaitSubmit?: boolean
|
||||
}
|
||||
|
||||
export type LemonDialogProps = Pick<
|
||||
@ -26,6 +27,7 @@ export type LemonDialogProps = Pick<
|
||||
onClose?: () => void
|
||||
onAfterClose?: () => void
|
||||
closeOnNavigate?: boolean
|
||||
shouldAwaitSubmit?: boolean
|
||||
}
|
||||
|
||||
export function LemonDialog({
|
||||
@ -37,12 +39,14 @@ export function LemonDialog({
|
||||
content,
|
||||
initialFormValues,
|
||||
closeOnNavigate = true,
|
||||
shouldAwaitSubmit = false,
|
||||
footer,
|
||||
...props
|
||||
}: LemonDialogProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
const { currentLocation } = useValues(router)
|
||||
const lastLocation = useRef(currentLocation.pathname)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
primaryButton =
|
||||
primaryButton ||
|
||||
@ -63,8 +67,20 @@ export function LemonDialog({
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
{...button}
|
||||
onClick={(e) => {
|
||||
loading={button === primaryButton && shouldAwaitSubmit ? isLoading : undefined}
|
||||
// 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)
|
||||
}}
|
||||
/>
|
||||
@ -117,7 +133,8 @@ export const LemonFormDialog = ({
|
||||
type: 'primary',
|
||||
children: '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,
|
||||
}
|
||||
|
||||
|
@ -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 { Sidebar } from '~/layout/navigation-3000/components/Sidebar'
|
||||
import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic'
|
||||
|
||||
import { editorSceneLogic } from './editorSceneLogic'
|
||||
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 { activeNavbarItem } = useValues(navigation3000Logic)
|
||||
const { sidebarOverlayOpen } = useValues(editorSceneLogic)
|
||||
|
||||
const editorSizingLogicProps = {
|
||||
editorSceneRef: ref,
|
||||
@ -28,9 +37,41 @@ export function EditorScene(): JSX.Element {
|
||||
return (
|
||||
<BindLogic logic={editorSizingLogic} props={editorSizingLogicProps}>
|
||||
<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 />
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
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'
|
||||
|
||||
@ -16,6 +15,7 @@ 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
|
||||
@ -25,7 +25,6 @@ export function QueryPane(props: QueryPaneProps): JSX.Element {
|
||||
ref={queryPaneResizerProps.containerRef}
|
||||
>
|
||||
<div className="flex-1">
|
||||
{props.promptError ? <LemonBanner type="warning">{props.promptError}</LemonBanner> : null}
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<CodeEditor
|
||||
@ -54,5 +53,6 @@ export function QueryPane(props: QueryPaneProps): JSX.Element {
|
||||
</div>
|
||||
<Resizer {...queryPaneResizerProps} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,41 +1,42 @@
|
||||
import { IconPlus, IconX } from '@posthog/icons'
|
||||
import { LemonButton } from '@posthog/lemon-ui'
|
||||
import clsx from 'clsx'
|
||||
import { Uri } from 'monaco-editor'
|
||||
|
||||
import { QueryTab } from './multitabEditorLogic'
|
||||
|
||||
interface QueryTabsProps {
|
||||
models: Uri[]
|
||||
onClick: (model: Uri) => void
|
||||
onClear: (model: Uri) => void
|
||||
models: QueryTab[]
|
||||
onClick: (model: QueryTab) => void
|
||||
onClear: (model: QueryTab) => void
|
||||
onAdd: () => void
|
||||
activeModelUri: Uri | null
|
||||
activeModelUri: QueryTab | 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}
|
||||
<div className="flex flex-row overflow-scroll hide-scrollbar h-10">
|
||||
{models.map((model: QueryTab) => (
|
||||
<QueryTabComponent
|
||||
key={model.uri.path}
|
||||
model={model}
|
||||
onClear={models.length > 1 ? onClear : undefined}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
interface QueryTabProps {
|
||||
model: Uri
|
||||
onClick: (model: Uri) => void
|
||||
onClear?: (model: Uri) => void
|
||||
model: QueryTab
|
||||
onClick: (model: QueryTab) => void
|
||||
onClear?: (model: QueryTab) => void
|
||||
active: boolean
|
||||
}
|
||||
|
||||
function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Element {
|
||||
function QueryTabComponent({ model, active, onClear, onClick }: QueryTabProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick?.(model)}
|
||||
@ -45,7 +46,7 @@ function QueryTab({ model, active, onClear, onClick }: QueryTabProps): JSX.Eleme
|
||||
onClear ? 'pl-3 pr-2' : 'px-3'
|
||||
)}
|
||||
>
|
||||
Untitled
|
||||
{model.view?.name ?? 'Untitled'}
|
||||
{onClear && (
|
||||
<LemonButton
|
||||
onClick={(e) => {
|
||||
|
@ -22,8 +22,17 @@ export function QueryWindow(): JSX.Element {
|
||||
monaco,
|
||||
editor,
|
||||
})
|
||||
const { allTabs, activeModelUri, queryInput, activeQuery, activeTabKey, hasErrors, error, isValidView } =
|
||||
useValues(logic)
|
||||
const {
|
||||
allTabs,
|
||||
activeModelUri,
|
||||
queryInput,
|
||||
activeQuery,
|
||||
activeTabKey,
|
||||
hasErrors,
|
||||
error,
|
||||
isValidView,
|
||||
editingView,
|
||||
} = useValues(logic)
|
||||
const { selectTab, deleteTab, createTab, setQueryInput, runQuery, saveAsView } = useActions(logic)
|
||||
|
||||
return (
|
||||
@ -35,6 +44,11 @@ export function QueryWindow(): JSX.Element {
|
||||
onAdd={createTab}
|
||||
activeModelUri={activeModelUri}
|
||||
/>
|
||||
{editingView && (
|
||||
<div className="h-7 bg-warning-highlight p-1">
|
||||
<span> Editing view "{editingView.name}"</span>
|
||||
</div>
|
||||
)}
|
||||
<QueryPane
|
||||
queryInput={queryInput}
|
||||
promptError={null}
|
||||
|
@ -1,14 +1,19 @@
|
||||
import 'react-data-grid/lib/styles.css'
|
||||
|
||||
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 DataGrid from 'react-data-grid'
|
||||
|
||||
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
|
||||
import { themeLogic } from '~/layout/navigation-3000/themeLogic'
|
||||
import { dataNodeLogic } from '~/queries/nodes/DataNode/dataNodeLogic'
|
||||
import { NodeKind } from '~/queries/schema'
|
||||
|
||||
import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic'
|
||||
import { multitabEditorLogic } from './multitabEditorLogic'
|
||||
|
||||
enum ResultsTab {
|
||||
Results = 'results',
|
||||
Visualization = 'visualization',
|
||||
@ -29,6 +34,13 @@ export function ResultPane({
|
||||
logicKey,
|
||||
query,
|
||||
}: ResultPaneProps): JSX.Element {
|
||||
const codeEditorKey = `hogQLQueryEditor/${router.values.location.pathname}`
|
||||
|
||||
const { editingView, queryInput } = useValues(
|
||||
multitabEditorLogic({
|
||||
key: codeEditorKey,
|
||||
})
|
||||
)
|
||||
const { isDarkModeOn } = useValues(themeLogic)
|
||||
const { response, responseLoading } = useValues(
|
||||
dataNodeLogic({
|
||||
@ -40,6 +52,8 @@ export function ResultPane({
|
||||
doNotLoad: !query,
|
||||
})
|
||||
)
|
||||
const { dataWarehouseSavedQueriesLoading } = useValues(dataWarehouseViewsLogic)
|
||||
const { updateDataWarehouseSavedQuery } = useActions(dataWarehouseViewsLogic)
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return (
|
||||
@ -78,11 +92,32 @@ export function ResultPane({
|
||||
]}
|
||||
/>
|
||||
<div className="flex gap-1">
|
||||
{editingView ? (
|
||||
<>
|
||||
<LemonButton
|
||||
loading={dataWarehouseSavedQueriesLoading}
|
||||
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 type="primary" onClick={() => onQueryInputChange()}>
|
||||
Run
|
||||
)}
|
||||
<LemonButton loading={responseLoading} type="primary" onClick={() => onQueryInputChange()}>
|
||||
<span className="mr-1">Run</span>
|
||||
<KeyboardShortcut command enter />
|
||||
</LemonButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 []
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
203
frontend/src/scenes/data-warehouse/editor/editorSidebarLogic.ts
Normal 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)
|
||||
},
|
||||
}),
|
||||
])
|
@ -1,6 +1,6 @@
|
||||
import { Monaco } from '@monaco-editor/react'
|
||||
import { LemonDialog, LemonInput } from '@posthog/lemon-ui'
|
||||
import { actions, kea, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
|
||||
import { LemonDialog, LemonInput, lemonToast } from '@posthog/lemon-ui'
|
||||
import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
|
||||
import { subscriptions } from 'kea-subscriptions'
|
||||
import { LemonField } from 'lib/lemon-ui/LemonField'
|
||||
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 { performQuery } from '~/queries/query'
|
||||
import { HogLanguage, HogQLMetadata, HogQLMetadataResponse, HogQLNotice, HogQLQuery, NodeKind } from '~/queries/schema'
|
||||
import { DataWarehouseSavedQuery } from '~/types'
|
||||
|
||||
import { dataWarehouseViewsLogic } from '../saved_queries/dataWarehouseViewsLogic'
|
||||
import type { multitabEditorLogicType } from './multitabEditorLogicType'
|
||||
@ -22,29 +23,41 @@ export interface MultitabEditorLogicProps {
|
||||
export const editorModelsStateKey = (key: string | number): string => `${key}/editorModelQueries`
|
||||
export const activemodelStateKey = (key: string | number): string => `${key}/activeModelUri`
|
||||
|
||||
export interface QueryTab {
|
||||
uri: Uri
|
||||
view?: DataWarehouseSavedQuery
|
||||
}
|
||||
|
||||
export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
path(['data-warehouse', 'editor', 'multitabEditorLogic']),
|
||||
props({} as MultitabEditorLogicProps),
|
||||
key((props) => props.key),
|
||||
connect({
|
||||
actions: [
|
||||
dataWarehouseViewsLogic,
|
||||
['deleteDataWarehouseSavedQuerySuccess', 'createDataWarehouseSavedQuerySuccess'],
|
||||
],
|
||||
}),
|
||||
actions({
|
||||
setQueryInput: (queryInput: string) => ({ queryInput }),
|
||||
updateState: true,
|
||||
runQuery: (queryOverride?: string) => ({ queryOverride }),
|
||||
setActiveQuery: (query: string) => ({ query }),
|
||||
setTabs: (tabs: Uri[]) => ({ tabs }),
|
||||
addTab: (tab: Uri) => ({ tab }),
|
||||
createTab: () => null,
|
||||
deleteTab: (tab: Uri) => ({ tab }),
|
||||
removeTab: (tab: Uri) => ({ tab }),
|
||||
selectTab: (tab: Uri) => ({ tab }),
|
||||
setTabs: (tabs: QueryTab[]) => ({ tabs }),
|
||||
addTab: (tab: QueryTab) => ({ tab }),
|
||||
createTab: (query?: string, view?: DataWarehouseSavedQuery) => ({ query, view }),
|
||||
deleteTab: (tab: QueryTab) => ({ tab }),
|
||||
removeTab: (tab: QueryTab) => ({ tab }),
|
||||
selectTab: (tab: QueryTab) => ({ tab }),
|
||||
setLocalState: (key: string, value: any) => ({ key, value }),
|
||||
initialize: true,
|
||||
saveAsView: true,
|
||||
saveAsViewSuccess: (name: string) => ({ name }),
|
||||
saveAsViewSubmit: (name: string) => ({ name }),
|
||||
reloadMetadata: true,
|
||||
setMetadata: (query: string, metadata: HogQLMetadataResponse) => ({ query, metadata }),
|
||||
}),
|
||||
propsChanged(({ actions }, oldProps) => {
|
||||
if (!oldProps.monaco && !oldProps.editor) {
|
||||
propsChanged(({ actions, props }, oldProps) => {
|
||||
if (!oldProps.monaco && !oldProps.editor && props.monaco && props.editor) {
|
||||
actions.initialize()
|
||||
}
|
||||
}),
|
||||
@ -62,20 +75,26 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
},
|
||||
],
|
||||
activeModelUri: [
|
||||
null as Uri | null,
|
||||
null as QueryTab | null,
|
||||
{
|
||||
selectTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
editingView: [
|
||||
null as DataWarehouseSavedQuery | null,
|
||||
{
|
||||
selectTab: (_, { tab }) => tab.view ?? null,
|
||||
},
|
||||
],
|
||||
allTabs: [
|
||||
[] as Uri[],
|
||||
[] as QueryTab[],
|
||||
{
|
||||
addTab: (state, { tab }) => {
|
||||
const newTabs = [...state, tab]
|
||||
return newTabs
|
||||
},
|
||||
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
|
||||
},
|
||||
setTabs: (_, { tabs }) => tabs,
|
||||
@ -130,25 +149,32 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
},
|
||||
],
|
||||
})),
|
||||
listeners(({ values, props, actions }) => ({
|
||||
createTab: () => {
|
||||
listeners(({ values, props, actions, asyncActions }) => ({
|
||||
createTab: ({ query = '', view }) => {
|
||||
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)) {
|
||||
currentModelCount++
|
||||
}
|
||||
|
||||
if (props.monaco) {
|
||||
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)
|
||||
actions.addTab(uri)
|
||||
actions.selectTab(uri)
|
||||
actions.addTab({
|
||||
uri,
|
||||
view,
|
||||
})
|
||||
actions.selectTab({
|
||||
uri,
|
||||
view,
|
||||
})
|
||||
|
||||
const queries = values.allTabs.map((tab) => {
|
||||
return {
|
||||
query: props.monaco?.editor.getModel(tab)?.getValue() || '',
|
||||
path: tab.path.split('/').pop(),
|
||||
query: props.monaco?.editor.getModel(tab.uri)?.getValue() || '',
|
||||
path: tab.uri.path.split('/').pop(),
|
||||
view: uri.path === tab.uri.path ? view : tab.view,
|
||||
}
|
||||
})
|
||||
actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries))
|
||||
@ -156,18 +182,20 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
},
|
||||
selectTab: ({ tab }) => {
|
||||
if (props.monaco) {
|
||||
const model = props.monaco.editor.getModel(tab)
|
||||
const model = props.monaco.editor.getModel(tab.uri)
|
||||
props.editor?.setModel(model)
|
||||
}
|
||||
|
||||
const path = tab.path.split('/').pop()
|
||||
const path = tab.uri.path.split('/').pop()
|
||||
path && actions.setLocalState(activemodelStateKey(props.key), path)
|
||||
},
|
||||
deleteTab: ({ tab: tabToRemove }) => {
|
||||
if (props.monaco) {
|
||||
const model = props.monaco.editor.getModel(tabToRemove)
|
||||
if (tabToRemove == values.activeModelUri) {
|
||||
const indexOfModel = values.allTabs.findIndex((tab) => tab.toString() === tabToRemove.toString())
|
||||
const model = props.monaco.editor.getModel(tabToRemove.uri)
|
||||
if (tabToRemove.uri.toString() === values.activeModelUri?.uri.toString()) {
|
||||
const indexOfModel = values.allTabs.findIndex(
|
||||
(tab) => tab.uri.toString() === tabToRemove.uri.toString()
|
||||
)
|
||||
const nextModel =
|
||||
values.allTabs[indexOfModel + 1] || values.allTabs[indexOfModel - 1] || values.allTabs[0] // there will always be one
|
||||
actions.selectTab(nextModel)
|
||||
@ -176,8 +204,9 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
actions.removeTab(tabToRemove)
|
||||
const queries = values.allTabs.map((tab) => {
|
||||
return {
|
||||
query: props.monaco?.editor.getModel(tab)?.getValue() || '',
|
||||
path: tab.path.split('/').pop(),
|
||||
query: props.monaco?.editor.getModel(tab.uri)?.getValue() || '',
|
||||
path: tab.uri.path.split('/').pop(),
|
||||
view: tab.view,
|
||||
}
|
||||
})
|
||||
actions.setLocalState(editorModelsStateKey(props.key), JSON.stringify(queries))
|
||||
@ -197,14 +226,17 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
})
|
||||
|
||||
const models = JSON.parse(allModelQueries || '[]')
|
||||
const newModels: Uri[] = []
|
||||
const newModels: QueryTab[] = []
|
||||
|
||||
models.forEach((model: Record<string, any>) => {
|
||||
if (props.monaco) {
|
||||
const uri = props.monaco.Uri.parse(model.path)
|
||||
const newModel = props.monaco.editor.createModel(model.query, 'hogQL', uri)
|
||||
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.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) {
|
||||
actions.selectTab(newModels[0])
|
||||
actions.selectTab({
|
||||
uri: newModels[0].uri,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const model = props.editor?.getModel()
|
||||
@ -240,13 +280,23 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
await breakpoint(100)
|
||||
const queries = values.allTabs.map((model) => {
|
||||
return {
|
||||
query: props.monaco?.editor.getModel(model)?.getValue() || '',
|
||||
path: model.path.split('/').pop(),
|
||||
query: props.monaco?.editor.getModel(model.uri)?.getValue() || '',
|
||||
path: model.uri.path.split('/').pop(),
|
||||
view: model.view,
|
||||
}
|
||||
})
|
||||
localStorage.setItem(editorModelsStateKey(props.key), JSON.stringify(queries))
|
||||
},
|
||||
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)
|
||||
},
|
||||
saveAsView: async () => {
|
||||
@ -261,10 +311,13 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
errors: {
|
||||
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 = {
|
||||
kind: NodeKind.HogQLQuery,
|
||||
query: values.queryInput,
|
||||
@ -290,11 +343,34 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
breakpoint()
|
||||
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 }) => ({
|
||||
activeModelUri: (activeModelUri) => {
|
||||
if (props.monaco) {
|
||||
const _model = props.monaco.editor.getModel(activeModelUri)
|
||||
const _model = props.monaco.editor.getModel(activeModelUri.uri)
|
||||
const val = _model?.getValue()
|
||||
actions.setQueryInput(val ?? '')
|
||||
actions.runQuery()
|
||||
@ -313,7 +389,7 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
|
||||
},
|
||||
})),
|
||||
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)],
|
||||
hasErrors: [
|
||||
(s) => [s.modelMarkers],
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
@ -46,7 +46,7 @@ export const dataWarehouseViewsLogic = kea<dataWarehouseViewsLogicType>([
|
||||
await api.dataWarehouseSavedQueries.delete(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)
|
||||
return values.dataWarehouseSavedQueries.map((savedQuery) => {
|
||||
if (savedQuery.id === view.id) {
|
||||
|
@ -43,7 +43,7 @@ function UpdateSourceConnectionFormContainer(props: UpdateSourceConnectionFormCo
|
||||
<>
|
||||
<span className="block mb-2">Overwrite your existing configuration here</span>
|
||||
<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">
|
||||
<LemonButton
|
||||
loading={sourceLoading && !source}
|
||||
|