diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx index 3eb3bfe0a7cf0..65417c039765d 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx @@ -1,6 +1,6 @@ import { Alert, Block, Card, PrismCode, Title } from '@cube-dev/ui-kit'; import cube, { Query } from '@cubejs-client/core'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, ReactNode } from 'react'; import { QueryBuilderContext } from './context'; import { useLocalStorage } from './hooks'; @@ -13,8 +13,7 @@ export function QueryBuilder( props: Omit & { displayPrivateItems?: boolean; apiUrl: string | null; - disableLimitEnforcing?: boolean; - children?: React.ReactNode; + children?: ReactNode; } ) { const { diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx index 815b15d85a5e2..d43f53e34d9c5 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx @@ -33,7 +33,7 @@ import { ORDER_LABEL_BY_TYPE } from './utils/labels'; import { formatNumber } from './utils/formatters'; import { TIMEZONES } from './utils/timezones'; -const DEFAULT_LIMIT = 5_000; +const DEFAULT_LIMIT = 0; // no limit const ALL_TIMEZONES: { tzCode: string; diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx index 9966caa44d5d0..670e2cb53cc49 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx @@ -64,17 +64,22 @@ const QueryBuilderInternals = memo(function QueryBuilderInternals() { styles={{ padding: '0 1x' }} onChange={(tab: string) => setTab(tab as Tab)} > - - - - - + + + + + + + + + + + + + + + - {tab === 'results' && } - {tab === 'generated-sql' && } - {tab === 'json' && } - {tab === 'sql' && } - {tab === 'graphql' && } ); }, [tab, isChartExpanded]); diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderSQL.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderSQL.tsx index 2cf56e7bda439..f212da0f540b2 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderSQL.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderSQL.tsx @@ -16,8 +16,20 @@ export function QueryBuilderSQL() { // todo: fix types of normalizedQueries (e.g. order is always an array) const [query] = dryRunResponse?.normalizedQueries || []; + if (isQueryEmpty) { + return ( + + Compose a query to see an SQL query. + + ); + } + if (!query) { - return null; + return ( + + Unable to generate an SQL query. + + ); } if (!isQueryEmpty && meta) { diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx index f214b658f9704..dfaa71189b0d4 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx @@ -1,16 +1,57 @@ -import { ReactNode, createContext, useContext, useCallback, useState, useEffect } from 'react'; +import { FocusableRefValue } from '@react-types/shared'; +import { + ReactNode, + createContext, + useContext, + useState, + useMemo, + useLayoutEffect, + useRef, + useEffect, +} from 'react'; import { Action, tasty, CloseIcon, Styles } from '@cube-dev/ui-kit'; +import { useEvent } from '../../hooks'; + +interface TabData { + content: ReactNode; + prerender: boolean; + keepMounted: boolean; +} + interface TabsContextValue { type?: 'default' | 'card'; size?: 'normal' | 'large'; activeKey?: string; extra?: ReactNode; - setContent: (content?: ReactNode) => void; + setTabContent: (id: string, content: TabData | null) => void; + prerender?: boolean; + keepMounted?: boolean; onChange: (key: string) => void; onDelete?: (key: string) => void; } +interface TabsProps extends Omit { + label?: string; + children?: ReactNode; + styles?: Styles; + size?: TabsContextValue['size']; +} + +interface TabProps { + id: string; + title: ReactNode; + children?: ReactNode; + isDisabled?: boolean; + qa?: string; + qaVal?: string; + styles?: Styles; + size?: TabsContextValue['size']; + extra?: ReactNode; + prerender?: boolean; + keepMounted?: boolean; +} + const TabsContext = createContext(undefined); const TabsElement = tasty({ @@ -23,11 +64,15 @@ const TabsElement = tasty({ shadow: 'inset 0 -1bw 0 #border', width: '100%', padding: '0 2x', + scrollbarWidth: 'none', Container: { display: 'grid', gridAutoFlow: 'column', - gap: '0', + gap: { + '': 0, + card: '1bw', + }, placeContent: 'start', }, @@ -49,6 +94,7 @@ const TabContainer = tasty({ const TabElement = tasty(Action, { styles: { + position: 'relative', preset: { '': 't3m', '[data-size="large"]': 't2m', @@ -74,6 +120,7 @@ const TabElement = tasty(Action, { '': '#dark-02', hovered: '#purple', active: '#purple-text', + 'disabled & !active': '#dark-04', }, borderBottom: { '': 'none', @@ -89,11 +136,27 @@ const TabElement = tasty(Action, { width: 'max 100%', transition: 'theme, borderBottom', whiteSpace: 'nowrap', + outline: false, '@delete-padding': { '': '1.5x', deletable: '4.5x', }, + + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + inset: '0 0 -1ow 0', + pointerEvents: 'none', + radius: 'top', + shadow: { + '': 'inset 0 0 0 #purple', + focused: 'inset 0 0 0 1ow #purple-03', + }, + transition: 'theme', + zIndex: 1, + }, }, }); @@ -122,80 +185,133 @@ const TabCloseButton = tasty(Action, { children: , }); -interface TabsProps extends Omit { - label?: string; - children?: ReactNode; - styles?: Styles; - size?: TabsContextValue['size']; -} - -interface TabProps { - id: string; - title: ReactNode; - children?: ReactNode; - isDisabled?: boolean; - qa?: string; - styles?: Styles; - size?: TabsContextValue['size']; - extra?: ReactNode; -} - export function Tabs(props: TabsProps) { - const [content, setContent] = useState(null); - const { label, activeKey, size, type, onChange, onDelete, children, styles, extra } = props; + const [contentMap, setContentMap] = useState>(new Map()); + const { + label, + activeKey, + size, + type, + onChange, + onDelete, + children, + styles, + extra, + prerender, + keepMounted, + } = props; const isCardType = type === 'card'; + // Update the content map whenever the activeKey changes + const setTabContent = useEvent((id: string, content: TabData | null) => { + setContentMap((prev) => { + const newMap = new Map(prev); + if (content) { + newMap.set(id, content); + } else { + newMap.delete(id); + } + + return newMap; + }); + }); + + const mods = useMemo(() => ({ card: isCardType, deletable: !!onDelete }), [isCardType, onDelete]); + return ( - +
{children}
{extra ?
{extra}
: null}
- {content} + {[...contentMap.entries()].map(([id, { content, prerender, keepMounted }]) => + prerender || id === activeKey || keepMounted ? ( +
+ {content} +
+ ) : null + )}
); } export function Tab(props: TabProps) { - const { title, id, isDisabled, qa, styles, children } = props; - const { activeKey, size, type, onChange, onDelete, setContent } = useContext(TabsContext) || {}; + let { title, id, isDisabled, prerender, keepMounted, qa, qaVal, styles, children } = props; + + const ref = useRef(null); + + const { activeKey, size, type, onChange, onDelete, setTabContent, ...contextProps } = + useContext(TabsContext) || ({} as TabsContextValue); + + prerender = prerender ?? contextProps.prerender; + keepMounted = keepMounted ?? contextProps.keepMounted; const isActive = id === activeKey; - const onDeleteCallback = useCallback(() => { + const onDeleteCallback = useEvent(() => { onDelete?.(id); - }, [onDelete, id]); - const onChangeCallback = useCallback(() => { + }); + const onChangeCallback = useEvent(() => { onChange?.(id); - }, [id]); + }); const isCardType = type === 'card'; - const isDeletable = onDelete && isCardType; + const isDeletable = !!onDelete; + + useLayoutEffect(() => { + if (prerender || isActive) { + setTabContent?.(id, { + content: children, + prerender: prerender ?? false, + keepMounted: keepMounted ?? false, + }); + } else if (!keepMounted) { + setTabContent?.(id, null); + } + }, [children, isActive, keepMounted, prerender, setTabContent]); + + useLayoutEffect(() => { + return () => { + setTabContent?.(id, null); + }; + }, []); + + const mods = useMemo( + () => ({ card: isCardType, active: isActive, deletable: isDeletable, disabled: isDisabled }), + [isCardType, isActive, isDeletable, isDisabled] + ); useEffect(() => { - if (isActive) { - setContent?.(children || null); + if (ref.current && isActive) { + ref.current.UNSAFE_getDOMNode()?.scrollIntoView?.(); } - }, [activeKey, children]); + }, [isActive]); return ( - + diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/validate-query.ts b/packages/cubejs-playground/src/QueryBuilderV2/utils/validate-query.ts index 111cd86ce58e5..513f9bcd2ae3d 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/utils/validate-query.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/validate-query.ts @@ -118,7 +118,7 @@ export function validateQuery(query: Record): Query { sanitizedQuery.segments = query.segments; } - if (typeof query.limit === 'number') { + if (typeof query.limit === 'number' && query.limit > 0) { sanitizedQuery.limit = query.limit; } diff --git a/packages/cubejs-playground/src/QueryBuilderV2/values.ts b/packages/cubejs-playground/src/QueryBuilderV2/values.ts index f05d461652e58..30dae45574472 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/values.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/values.ts @@ -92,12 +92,12 @@ export const OPERATORS_BY_TYPE = { export const OPERATORS: Operator[] = [...UNARY_OPERATORS, ...BINARY_OPERATORS]; export const PREDEFINED_GRANULARITIES: TimeDimensionGranularity[] = [ - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'quarter', 'year', + 'quarter', + 'month', + 'week', + 'day', + 'hour', + 'minute', + 'second', ];