Skip to content

Commit

Permalink
fix(cubejs-playground): update query builder (#9201)
Browse files Browse the repository at this point in the history
  • Loading branch information
tenphi authored Feb 7, 2025
1 parent 18909fe commit f8e523b
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,8 +13,7 @@ export function QueryBuilder(
props: Omit<QueryBuilderProps, 'apiUrl'> & {
displayPrivateItems?: boolean;
apiUrl: string | null;
disableLimitEnforcing?: boolean;
children?: React.ReactNode;
children?: ReactNode;
}
) {
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,22 @@ const QueryBuilderInternals = memo(function QueryBuilderInternals() {
styles={{ padding: '0 1x' }}
onChange={(tab: string) => setTab(tab as Tab)}
>
<Tab id="results" title="Results" />
<Tab id="generated-sql" title="Generated SQL" />
<Tab id="sql" title="SQL API" />
<Tab id="json" title="REST API" />
<Tab id="graphql" title="GraphQL API" />
<Tab keepMounted id="results" title="Results">
<QueryBuilderResults forceMinHeight={!isChartExpanded} />
</Tab>
<Tab id="generated-sql" title="Generated SQL">
<QueryBuilderGeneratedSQL />
</Tab>
<Tab id="sql" title="SQL API">
<QueryBuilderSQL />
</Tab>
<Tab id="json" title="REST API">
<QueryBuilderRest />
</Tab>
<Tab id="graphql" title="GraphQL API">
<QueryBuilderGraphQL />
</Tab>
</Tabs>
{tab === 'results' && <QueryBuilderResults forceMinHeight={!isChartExpanded} />}
{tab === 'generated-sql' && <QueryBuilderGeneratedSQL />}
{tab === 'json' && <QueryBuilderRest />}
{tab === 'sql' && <QueryBuilderSQL />}
{tab === 'graphql' && <QueryBuilderGraphQL />}
</>
);
}, [tab, isChartExpanded]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Block padding="1x">
<Alert>Compose a query to see an SQL query.</Alert>
</Block>
);
}

if (!query) {
return null;
return (
<Block padding="1x">
<Alert>Unable to generate an SQL query.</Alert>
</Block>
);
}

if (!isQueryEmpty && meta) {
Expand Down
202 changes: 159 additions & 43 deletions packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -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<TabsContextValue, 'setTabContent'> {
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<TabsContextValue | undefined>(undefined);

const TabsElement = tasty({
Expand All @@ -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',
},

Expand All @@ -49,6 +94,7 @@ const TabContainer = tasty({

const TabElement = tasty(Action, {
styles: {
position: 'relative',
preset: {
'': 't3m',
'[data-size="large"]': 't2m',
Expand All @@ -74,6 +120,7 @@ const TabElement = tasty(Action, {
'': '#dark-02',
hovered: '#purple',
active: '#purple-text',
'disabled & !active': '#dark-04',
},
borderBottom: {
'': 'none',
Expand All @@ -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,
},
},
});

Expand Down Expand Up @@ -122,80 +185,133 @@ const TabCloseButton = tasty(Action, {
children: <CloseIcon />,
});

interface TabsProps extends Omit<TabsContextValue, 'setContent'> {
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<ReactNode>(null);
const { label, activeKey, size, type, onChange, onDelete, children, styles, extra } = props;
const [contentMap, setContentMap] = useState<Map<string, TabData>>(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 (
<TabsContext.Provider value={{ activeKey, onChange, onDelete, type, size, setContent }}>
<TabsContext.Provider
value={{ activeKey, onChange, onDelete, type, size, setTabContent, prerender, keepMounted }}
>
<TabsElement
qa="Tabs"
aria-label={label ?? 'Tabs'}
data-size={size ?? 'normal'}
mods={{ card: isCardType }}
mods={mods}
styles={styles}
>
<div data-element="Container">{children}</div>
{extra ? <div data-element="Extra">{extra}</div> : null}
</TabsElement>
{content}
{[...contentMap.entries()].map(([id, { content, prerender, keepMounted }]) =>
prerender || id === activeKey || keepMounted ? (
<div
key={id}
data-qa="TabPanel"
data-qaval={id}
style={{
display: id === activeKey ? 'contents' : 'none',
}}
>
{content}
</div>
) : null
)}
</TabsContext.Provider>
);
}

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<FocusableRefValue>(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 (
<TabContainer>
<TabContainer mods={mods}>
<TabElement
qa={`Tab-${id}` ?? qa}
ref={ref}
qa={qa ?? `Tab-${id}`}
qaVal={qaVal}
isDisabled={isDisabled}
styles={styles}
mods={{
active: isActive,
card: isCardType,
deletable: isDeletable,
}}
mods={mods}
data-size={size}
onPress={onChangeCallback}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function validateQuery(query: Record<string, any>): Query {
sanitizedQuery.segments = query.segments;
}

if (typeof query.limit === 'number') {
if (typeof query.limit === 'number' && query.limit > 0) {
sanitizedQuery.limit = query.limit;
}

Expand Down
Loading

0 comments on commit f8e523b

Please sign in to comment.