Skip to content

Commit f8e523b

Browse files
authored
fix(cubejs-playground): update query builder (#9201)
1 parent 18909fe commit f8e523b

File tree

7 files changed

+198
-66
lines changed

7 files changed

+198
-66
lines changed

packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Alert, Block, Card, PrismCode, Title } from '@cube-dev/ui-kit';
22
import cube, { Query } from '@cubejs-client/core';
3-
import { useEffect, useMemo } from 'react';
3+
import { useEffect, useMemo, ReactNode } from 'react';
44

55
import { QueryBuilderContext } from './context';
66
import { useLocalStorage } from './hooks';
@@ -13,8 +13,7 @@ export function QueryBuilder(
1313
props: Omit<QueryBuilderProps, 'apiUrl'> & {
1414
displayPrivateItems?: boolean;
1515
apiUrl: string | null;
16-
disableLimitEnforcing?: boolean;
17-
children?: React.ReactNode;
16+
children?: ReactNode;
1817
}
1918
) {
2019
const {

packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ORDER_LABEL_BY_TYPE } from './utils/labels';
3333
import { formatNumber } from './utils/formatters';
3434
import { TIMEZONES } from './utils/timezones';
3535

36-
const DEFAULT_LIMIT = 5_000;
36+
const DEFAULT_LIMIT = 0; // no limit
3737

3838
const ALL_TIMEZONES: {
3939
tzCode: string;

packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,22 @@ const QueryBuilderInternals = memo(function QueryBuilderInternals() {
6464
styles={{ padding: '0 1x' }}
6565
onChange={(tab: string) => setTab(tab as Tab)}
6666
>
67-
<Tab id="results" title="Results" />
68-
<Tab id="generated-sql" title="Generated SQL" />
69-
<Tab id="sql" title="SQL API" />
70-
<Tab id="json" title="REST API" />
71-
<Tab id="graphql" title="GraphQL API" />
67+
<Tab keepMounted id="results" title="Results">
68+
<QueryBuilderResults forceMinHeight={!isChartExpanded} />
69+
</Tab>
70+
<Tab id="generated-sql" title="Generated SQL">
71+
<QueryBuilderGeneratedSQL />
72+
</Tab>
73+
<Tab id="sql" title="SQL API">
74+
<QueryBuilderSQL />
75+
</Tab>
76+
<Tab id="json" title="REST API">
77+
<QueryBuilderRest />
78+
</Tab>
79+
<Tab id="graphql" title="GraphQL API">
80+
<QueryBuilderGraphQL />
81+
</Tab>
7282
</Tabs>
73-
{tab === 'results' && <QueryBuilderResults forceMinHeight={!isChartExpanded} />}
74-
{tab === 'generated-sql' && <QueryBuilderGeneratedSQL />}
75-
{tab === 'json' && <QueryBuilderRest />}
76-
{tab === 'sql' && <QueryBuilderSQL />}
77-
{tab === 'graphql' && <QueryBuilderGraphQL />}
7883
</>
7984
);
8085
}, [tab, isChartExpanded]);

packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderSQL.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,20 @@ export function QueryBuilderSQL() {
1616
// todo: fix types of normalizedQueries (e.g. order is always an array)
1717
const [query] = dryRunResponse?.normalizedQueries || [];
1818

19+
if (isQueryEmpty) {
20+
return (
21+
<Block padding="1x">
22+
<Alert>Compose a query to see an SQL query.</Alert>
23+
</Block>
24+
);
25+
}
26+
1927
if (!query) {
20-
return null;
28+
return (
29+
<Block padding="1x">
30+
<Alert>Unable to generate an SQL query.</Alert>
31+
</Block>
32+
);
2133
}
2234

2335
if (!isQueryEmpty && meta) {

packages/cubejs-playground/src/QueryBuilderV2/components/Tabs/Tabs.tsx

Lines changed: 159 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,57 @@
1-
import { ReactNode, createContext, useContext, useCallback, useState, useEffect } from 'react';
1+
import { FocusableRefValue } from '@react-types/shared';
2+
import {
3+
ReactNode,
4+
createContext,
5+
useContext,
6+
useState,
7+
useMemo,
8+
useLayoutEffect,
9+
useRef,
10+
useEffect,
11+
} from 'react';
212
import { Action, tasty, CloseIcon, Styles } from '@cube-dev/ui-kit';
313

14+
import { useEvent } from '../../hooks';
15+
16+
interface TabData {
17+
content: ReactNode;
18+
prerender: boolean;
19+
keepMounted: boolean;
20+
}
21+
422
interface TabsContextValue {
523
type?: 'default' | 'card';
624
size?: 'normal' | 'large';
725
activeKey?: string;
826
extra?: ReactNode;
9-
setContent: (content?: ReactNode) => void;
27+
setTabContent: (id: string, content: TabData | null) => void;
28+
prerender?: boolean;
29+
keepMounted?: boolean;
1030
onChange: (key: string) => void;
1131
onDelete?: (key: string) => void;
1232
}
1333

34+
interface TabsProps extends Omit<TabsContextValue, 'setTabContent'> {
35+
label?: string;
36+
children?: ReactNode;
37+
styles?: Styles;
38+
size?: TabsContextValue['size'];
39+
}
40+
41+
interface TabProps {
42+
id: string;
43+
title: ReactNode;
44+
children?: ReactNode;
45+
isDisabled?: boolean;
46+
qa?: string;
47+
qaVal?: string;
48+
styles?: Styles;
49+
size?: TabsContextValue['size'];
50+
extra?: ReactNode;
51+
prerender?: boolean;
52+
keepMounted?: boolean;
53+
}
54+
1455
const TabsContext = createContext<TabsContextValue | undefined>(undefined);
1556

1657
const TabsElement = tasty({
@@ -23,11 +64,15 @@ const TabsElement = tasty({
2364
shadow: 'inset 0 -1bw 0 #border',
2465
width: '100%',
2566
padding: '0 2x',
67+
scrollbarWidth: 'none',
2668

2769
Container: {
2870
display: 'grid',
2971
gridAutoFlow: 'column',
30-
gap: '0',
72+
gap: {
73+
'': 0,
74+
card: '1bw',
75+
},
3176
placeContent: 'start',
3277
},
3378

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

5095
const TabElement = tasty(Action, {
5196
styles: {
97+
position: 'relative',
5298
preset: {
5399
'': 't3m',
54100
'[data-size="large"]': 't2m',
@@ -74,6 +120,7 @@ const TabElement = tasty(Action, {
74120
'': '#dark-02',
75121
hovered: '#purple',
76122
active: '#purple-text',
123+
'disabled & !active': '#dark-04',
77124
},
78125
borderBottom: {
79126
'': 'none',
@@ -89,11 +136,27 @@ const TabElement = tasty(Action, {
89136
width: 'max 100%',
90137
transition: 'theme, borderBottom',
91138
whiteSpace: 'nowrap',
139+
outline: false,
92140

93141
'@delete-padding': {
94142
'': '1.5x',
95143
deletable: '4.5x',
96144
},
145+
146+
'&::before': {
147+
content: '""',
148+
display: 'block',
149+
position: 'absolute',
150+
inset: '0 0 -1ow 0',
151+
pointerEvents: 'none',
152+
radius: 'top',
153+
shadow: {
154+
'': 'inset 0 0 0 #purple',
155+
focused: 'inset 0 0 0 1ow #purple-03',
156+
},
157+
transition: 'theme',
158+
zIndex: 1,
159+
},
97160
},
98161
});
99162

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

125-
interface TabsProps extends Omit<TabsContextValue, 'setContent'> {
126-
label?: string;
127-
children?: ReactNode;
128-
styles?: Styles;
129-
size?: TabsContextValue['size'];
130-
}
131-
132-
interface TabProps {
133-
id: string;
134-
title: ReactNode;
135-
children?: ReactNode;
136-
isDisabled?: boolean;
137-
qa?: string;
138-
styles?: Styles;
139-
size?: TabsContextValue['size'];
140-
extra?: ReactNode;
141-
}
142-
143188
export function Tabs(props: TabsProps) {
144-
const [content, setContent] = useState<ReactNode>(null);
145-
const { label, activeKey, size, type, onChange, onDelete, children, styles, extra } = props;
189+
const [contentMap, setContentMap] = useState<Map<string, TabData>>(new Map());
190+
const {
191+
label,
192+
activeKey,
193+
size,
194+
type,
195+
onChange,
196+
onDelete,
197+
children,
198+
styles,
199+
extra,
200+
prerender,
201+
keepMounted,
202+
} = props;
146203

147204
const isCardType = type === 'card';
148205

206+
// Update the content map whenever the activeKey changes
207+
const setTabContent = useEvent((id: string, content: TabData | null) => {
208+
setContentMap((prev) => {
209+
const newMap = new Map(prev);
210+
if (content) {
211+
newMap.set(id, content);
212+
} else {
213+
newMap.delete(id);
214+
}
215+
216+
return newMap;
217+
});
218+
});
219+
220+
const mods = useMemo(() => ({ card: isCardType, deletable: !!onDelete }), [isCardType, onDelete]);
221+
149222
return (
150-
<TabsContext.Provider value={{ activeKey, onChange, onDelete, type, size, setContent }}>
223+
<TabsContext.Provider
224+
value={{ activeKey, onChange, onDelete, type, size, setTabContent, prerender, keepMounted }}
225+
>
151226
<TabsElement
152227
qa="Tabs"
153228
aria-label={label ?? 'Tabs'}
154229
data-size={size ?? 'normal'}
155-
mods={{ card: isCardType }}
230+
mods={mods}
156231
styles={styles}
157232
>
158233
<div data-element="Container">{children}</div>
159234
{extra ? <div data-element="Extra">{extra}</div> : null}
160235
</TabsElement>
161-
{content}
236+
{[...contentMap.entries()].map(([id, { content, prerender, keepMounted }]) =>
237+
prerender || id === activeKey || keepMounted ? (
238+
<div
239+
key={id}
240+
data-qa="TabPanel"
241+
data-qaval={id}
242+
style={{
243+
display: id === activeKey ? 'contents' : 'none',
244+
}}
245+
>
246+
{content}
247+
</div>
248+
) : null
249+
)}
162250
</TabsContext.Provider>
163251
);
164252
}
165253

166254
export function Tab(props: TabProps) {
167-
const { title, id, isDisabled, qa, styles, children } = props;
168-
const { activeKey, size, type, onChange, onDelete, setContent } = useContext(TabsContext) || {};
255+
let { title, id, isDisabled, prerender, keepMounted, qa, qaVal, styles, children } = props;
256+
257+
const ref = useRef<FocusableRefValue>(null);
258+
259+
const { activeKey, size, type, onChange, onDelete, setTabContent, ...contextProps } =
260+
useContext(TabsContext) || ({} as TabsContextValue);
261+
262+
prerender = prerender ?? contextProps.prerender;
263+
keepMounted = keepMounted ?? contextProps.keepMounted;
169264

170265
const isActive = id === activeKey;
171266

172-
const onDeleteCallback = useCallback(() => {
267+
const onDeleteCallback = useEvent(() => {
173268
onDelete?.(id);
174-
}, [onDelete, id]);
175-
const onChangeCallback = useCallback(() => {
269+
});
270+
const onChangeCallback = useEvent(() => {
176271
onChange?.(id);
177-
}, [id]);
272+
});
178273

179274
const isCardType = type === 'card';
180-
const isDeletable = onDelete && isCardType;
275+
const isDeletable = !!onDelete;
276+
277+
useLayoutEffect(() => {
278+
if (prerender || isActive) {
279+
setTabContent?.(id, {
280+
content: children,
281+
prerender: prerender ?? false,
282+
keepMounted: keepMounted ?? false,
283+
});
284+
} else if (!keepMounted) {
285+
setTabContent?.(id, null);
286+
}
287+
}, [children, isActive, keepMounted, prerender, setTabContent]);
288+
289+
useLayoutEffect(() => {
290+
return () => {
291+
setTabContent?.(id, null);
292+
};
293+
}, []);
294+
295+
const mods = useMemo(
296+
() => ({ card: isCardType, active: isActive, deletable: isDeletable, disabled: isDisabled }),
297+
[isCardType, isActive, isDeletable, isDisabled]
298+
);
181299

182300
useEffect(() => {
183-
if (isActive) {
184-
setContent?.(children || null);
301+
if (ref.current && isActive) {
302+
ref.current.UNSAFE_getDOMNode()?.scrollIntoView?.();
185303
}
186-
}, [activeKey, children]);
304+
}, [isActive]);
187305

188306
return (
189-
<TabContainer>
307+
<TabContainer mods={mods}>
190308
<TabElement
191-
qa={`Tab-${id}` ?? qa}
309+
ref={ref}
310+
qa={qa ?? `Tab-${id}`}
311+
qaVal={qaVal}
192312
isDisabled={isDisabled}
193313
styles={styles}
194-
mods={{
195-
active: isActive,
196-
card: isCardType,
197-
deletable: isDeletable,
198-
}}
314+
mods={mods}
199315
data-size={size}
200316
onPress={onChangeCallback}
201317
>

packages/cubejs-playground/src/QueryBuilderV2/utils/validate-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function validateQuery(query: Record<string, any>): Query {
118118
sanitizedQuery.segments = query.segments;
119119
}
120120

121-
if (typeof query.limit === 'number') {
121+
if (typeof query.limit === 'number' && query.limit > 0) {
122122
sanitizedQuery.limit = query.limit;
123123
}
124124

0 commit comments

Comments
 (0)