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 {
|
.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);
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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 })
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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 { 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],
|
||||||
|
@ -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)
|
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) {
|
||||||
|
@ -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}
|
||||||
|