From 7d77b4ad6ce76e0e6d06f23f0cbb74b265b7c299 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Wed, 18 Sep 2024 13:40:19 +0200 Subject: [PATCH] feat(cubejs-playground): add support for ungrouped and offset options (#8719) --- .../src/QueryBuilderV2/QueryBuilder.tsx | 14 +- .../src/QueryBuilderV2/QueryBuilderChart.tsx | 19 +- .../src/QueryBuilderV2/QueryBuilderExtras.tsx | 343 +++++++++++------- .../QueryBuilderV2/QueryBuilderFilters.tsx | 77 +--- .../QueryBuilderGeneratedSQL.tsx | 14 +- .../QueryBuilderV2/QueryBuilderInternals.tsx | 19 +- .../QueryBuilderV2/QueryBuilderResults.tsx | 10 +- .../QueryBuilderV2/QueryBuilderToolBar.tsx | 4 +- .../components/Accordion/Accordion.tsx | 10 +- .../Accordion/AccordionItemTitle.tsx | 20 +- .../Accordion/AccordionProvider.tsx | 14 +- .../components/Accordion/types.ts | 9 +- .../AccordionCard/AccordionCard.tsx | 4 +- .../components/EditQueryDialogForm.tsx | 76 +--- .../components/OutdatedLabel.tsx | 24 ++ .../components/SidePanelCubeItem.tsx | 65 +--- .../src/QueryBuilderV2/hooks/deep-memo.ts | 5 +- .../src/QueryBuilderV2/hooks/query-builder.ts | 28 +- .../src/QueryBuilderV2/types.ts | 16 +- .../QueryBuilderV2/utils/get-query-hash.tsx | 10 +- .../utils/graphql-converters.ts | 68 +--- .../integration/playground-explore.spec.js | 2 + 22 files changed, 375 insertions(+), 476 deletions(-) create mode 100644 packages/cubejs-playground/src/QueryBuilderV2/components/OutdatedLabel.tsx diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx index 8aa530624a330..6e08734c75a1d 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilder.tsx @@ -40,17 +40,13 @@ export function QueryBuilder(props: Omit & { apiUrl function queryValidator(query: Query) { const queryCopy = JSON.parse(JSON.stringify(query)); - if (typeof queryCopy.limit !== 'number' || queryCopy.limit < 1 || queryCopy.limit > 50_000) { - queryCopy.limit = 5_000; + // add the last stored timezone if the query is empty + if (JSON.stringify(queryCopy) === '{}' && storedTimezones[0]) { + queryCopy.timezone = storedTimezones[0]; } - /** - * @TODO: Add support for offset - */ - delete queryCopy.offset; - - if (!queryCopy.timezone && storedTimezones[0]) { - queryCopy.timezone = storedTimezones[0]; + if (typeof queryCopy.limit !== 'number' || queryCopy.limit < 1 || queryCopy.limit > 50_000) { + queryCopy.limit = 5_000; } return queryCopy; diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderChart.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderChart.tsx index 52af9f7d9ee68..fbc28fc920404 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderChart.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderChart.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Badge, Button, Dialog, DialogTrigger, @@ -26,6 +25,7 @@ import { useQueryBuilderContext } from './context'; import { PivotAxes, PivotOptions } from './Pivot'; import { ArrowIcon } from './icons/ArrowIcon'; import { AccordionCard } from './components/AccordionCard'; +import { OutdatedLabel } from './components/OutdatedLabel'; import { QueryBuilderChartResults } from './QueryBuilderChartResults'; const CHART_HEIGHT = 400; @@ -40,16 +40,11 @@ const ALLOWED_CHART_TYPES = ['table', 'line', 'bar', 'area']; export function QueryBuilderChart(props: QueryBuilderChartProps) { const [isVizardLoaded, setIsVizardLoaded] = useState(false); - const [isExpanded, setIsExpanded] = useLocalStorage( - 'QueryBuilder:Chart:expanded', - false - ); + const [isExpanded, setIsExpanded] = useLocalStorage('QueryBuilder:Chart:expanded', false); const { maxHeight = CHART_HEIGHT, onToggle } = props; let { query, isLoading, - isQueryTouched, - executedQuery, chartType, setChartType, pivotConfig, @@ -57,9 +52,9 @@ export function QueryBuilderChart(props: QueryBuilderChartProps) { resultSet, apiToken, apiUrl, + isResultOutdated, VizardComponent, } = useQueryBuilderContext(); - const isOutdated = executedQuery && isQueryTouched; const containerRef = useRef(null); if (!ALLOWED_CHART_TYPES.includes(chartType || '')) { @@ -146,8 +141,8 @@ export function QueryBuilderChart(props: QueryBuilderChartProps) { isExpanded ? ( ) : undefined - ) : isOutdated ? ( - OUTDATED + ) : isResultOutdated ? ( + ) : undefined } extra={ @@ -226,9 +221,7 @@ export function QueryBuilderChart(props: QueryBuilderChartProps) { }} > <> - {isLoading ? ( - - ) : undefined} + {isLoading ? : undefined} {chart} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx index 655215a57b03f..bfb57f39f2c94 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderExtras.tsx @@ -1,37 +1,38 @@ import { DragOutlined } from '@ant-design/icons'; import { Button, + Checkbox, ComboBox, Content, Dialog, DialogTrigger, + DownIcon, Flow, Grid, + Link, + NumberInput, Radio, Select, Space, Tag, tasty, Text, + Title, } from '@cube-dev/ui-kit'; import { forwardRef, Key, useEffect, useMemo, useState } from 'react'; -import { - DragDropContext, - Draggable, - Droppable, - OnDragEndResponder, -} from 'react-beautiful-dnd'; +import { DragDropContext, Draggable, Droppable, OnDragEndResponder } from 'react-beautiful-dnd'; import { TCubeMemberType } from '@cubejs-client/core'; import { useStoredTimezones, useEvent } from './hooks'; import { MemberLabel } from './components/MemberLabel'; import { useQueryBuilderContext } from './context'; -import { ArrowIcon } from './icons/ArrowIcon'; import { ORDER_LABEL_BY_TYPE } from './utils/labels'; import { formatNumber } from './utils/formatters'; import { TIMEZONES } from './utils/timezones'; -const allTimeZones: { +const DEFAULT_LIMIT = 5_000; + +const ALL_TIMEZONES: { tzCode: string; label: string; name: string; @@ -45,15 +46,15 @@ const allTimeZones: { }, ...TIMEZONES, ]; -const availableTimeZones = allTimeZones.map((tz) => tz.tzCode); +const AVAILABLE_TIMEZONES = ALL_TIMEZONES.map((tz) => tz.tzCode); -const limitOptions = [ +const LIMIT_OPTIONS: { key: number; label: string }[] = [ { key: 100, label: '100' }, { key: 1000, label: '1,000' }, - { key: 5000, label: '5,000' }, - { key: 50000, label: '50,000 (MAX)' }, + { key: 5000, label: '5,000 (Default)' }, + { key: 50000, label: '50,000 (Max)' }, ]; -const limitOptionValues = limitOptions.map((option) => option.key); +const LIMIT_OPTION_VALUES = LIMIT_OPTIONS.map((option) => option.key) as number[]; function timezoneByName(name: string) { return { @@ -171,10 +172,7 @@ type OrderListItemProps = { onSortChange: (name: string, sorting: SortDirection) => void; }; -export const OrderListItem = forwardRef(function OrderListItem( - props: OrderListItemProps, - ref -) { +export const OrderListItem = forwardRef(function OrderListItem(props: OrderListItemProps, ref) { const { name, memberType, @@ -187,12 +185,7 @@ export const OrderListItem = forwardRef(function OrderListItem( const label = props.label ?? name; return ( - + @@ -207,19 +200,11 @@ export const OrderListItem = forwardRef(function OrderListItem( {ORDER_LABEL_BY_TYPE[cubeMemberKind ?? 'string'][0]} - + {ORDER_LABEL_BY_TYPE[cubeMemberKind ?? 'string'][1]} - + None @@ -233,9 +218,7 @@ export function QueryBuilderExtras() { const fields = [...(query?.dimensions ?? []), ...(query?.measures ?? [])]; const storedTimezones = useStoredTimezones(query.timezone); const timeDimensions = - query?.timeDimensions - ?.filter((time) => time.granularity) - .map((time) => time.dimension) ?? []; + query?.timeDimensions?.filter((time) => time.granularity).map((time) => time.dimension) ?? []; timeDimensions.forEach((name) => { if (name && !fields.includes(name)) { @@ -328,6 +311,186 @@ export function QueryBuilderExtras() { setAllFields(newOrder); }); + const optionsPopover = useMemo(() => { + // ungrouped + const isSelected = + query.ungrouped || + query.timezone || + query.offset || + (query.limit && query.limit !== DEFAULT_LIMIT); + const selectedCount = + (query.ungrouped ? 1 : 0) + + (query.timezone ? 1 : 0) + + (query.limit && query.limit !== DEFAULT_LIMIT ? 1 : 0) + + (query.offset ? 1 : 0); + + // timezone + const timezone = query?.timezone || ''; + const optionsWithStored = [...ALL_TIMEZONES]; + + [...storedTimezones].reverse().forEach((name) => { + if (!AVAILABLE_TIMEZONES.includes(name)) { + optionsWithStored.unshift(timezoneByName(name)); + } else { + const option = optionsWithStored.find((tz) => tz.tzCode === name); + + if (option) { + optionsWithStored.splice(optionsWithStored.indexOf(option), 1); + optionsWithStored.unshift(option); + } + } + }); + + const options = optionsWithStored.map((tz) => tz.tzCode).includes(timezone) + ? optionsWithStored + : [timezoneByName(timezone), ...optionsWithStored]; + + // limit + const limit = query.limit || DEFAULT_LIMIT; + const limitOptions = LIMIT_OPTION_VALUES.includes(limit) + ? LIMIT_OPTIONS + : [{ key: limit, label: formatNumber(limit) }, ...LIMIT_OPTIONS].sort( + (a, b) => a.key - b.key + ); + + return ( + + + {(close) => ( + + + + + Query + + { + updateQuery({ ungrouped: ungrouped || undefined }); + close(); + }} + > + Ungrouped + + + { + updateQuery({ timezone: undefined }); + close(); + }} + > + Reset + + ) : null + } + selectedKey={timezone} + onSelectionChange={(val: Key) => { + const timezone = val as string; + + updateQuery(() => ({ + timezone: timezone === '' ? undefined : timezone, + })); + + close(); + }} + > + {options.map((tz) => { + const name = tz.tzCode; + const zone = tz.utc; + + return ( + + + + {name || 'UTC (Default)'} + + {zone ? ( + + GMT{zone} + + ) : undefined} + + + ); + })} + + + { + updateQuery({ offset: undefined }); + close(); + }} + > + Reset + + ) : null + } + minValue={0} + value={query?.offset ?? 0} + onChange={(val) => { + updateQuery({ offset: val }); + }} + /> + + + )} + + ); + }, [query.ungrouped, query.timezone, query.offset, storedTimezones.join('::'), query.limit]); + const orderSelector = useMemo(() => { if (!allFields.length) { return; @@ -339,7 +502,7 @@ export function QueryBuilderExtras() { qa="OrderButton" type={sortedFields.length ? 'primary' : 'secondary'} size="small" - rightIcon={} + rightIcon={} > {sortedFields.length ? ( <> @@ -353,17 +516,11 @@ export function QueryBuilderExtras() { )} - + {(provided) => ( - + {allFields.map((name, index) => { const memberType = getMemberType(name); @@ -398,100 +555,10 @@ export function QueryBuilderExtras() { ); }, [JSON.stringify(order.map), JSON.stringify(allFields), showOrder]); - const limitSelector = useMemo(() => { - const limit = query.limit || 5_000; - const options = limitOptionValues.includes(limit) - ? limitOptions - : [ - { key: query?.limit, label: formatNumber(limit) }, - ...limitOptions, - ].sort((a, b) => (a.key as number) - (b.key as number)); - - return ( - - ); - }, [query.limit]); - - const timezoneSelector = useMemo(() => { - const timezone = query?.timezone || ''; - const optionsWithStored = [...allTimeZones]; - - [...storedTimezones].reverse().forEach((name) => { - if (!availableTimeZones.includes(name)) { - optionsWithStored.unshift(timezoneByName(name)); - } else { - const option = optionsWithStored.find((tz) => tz.tzCode === name); - - if (option) { - optionsWithStored.splice(optionsWithStored.indexOf(option), 1); - optionsWithStored.unshift(option); - } - } - }); - - const options = optionsWithStored.map((tz) => tz.tzCode).includes(timezone) - ? optionsWithStored - : [timezoneByName(timezone), ...optionsWithStored]; - - return ( - { - const timezone = val as string; - - updateQuery(() => ({ - timezone: timezone === '' ? undefined : timezone, - })); - }} - > - {options.map((tz) => { - const name = tz.tzCode; - const zone = tz.utc; - - return ( - - - - {name || 'UTC (Default)'} - - {zone ? ( - - GMT{zone} - - ) : undefined} - - - ); - })} - - ); - }, [query?.timezone, storedTimezones.join('::')]); - return ( - - {timezoneSelector} - - {orderSelector} - {limitSelector} - + + {orderSelector} + {optionsPopover} ); } diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderFilters.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderFilters.tsx index 2d6f488715de5..cdea3f1b14934 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderFilters.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderFilters.tsx @@ -1,14 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { - Block, - Button, - Divider, - Flow, - Menu, - MenuTrigger, - Space, - tasty, -} from '@cube-dev/ui-kit'; +import { Block, Button, Divider, Flow, Menu, MenuTrigger, Space, tasty } from '@cube-dev/ui-kit'; import { PlusOutlined } from '@ant-design/icons'; import { TCubeDimension, TCubeMeasure } from '@cubejs-client/core'; @@ -33,11 +24,7 @@ const BadgeContainer = tasty(Space, { }, }); -export function QueryBuilderFilters({ - onToggle, -}: { - onToggle?: (isExpanded: boolean) => void; -}) { +export function QueryBuilderFilters({ onToggle }: { onToggle?: (isExpanded: boolean) => void }) { const [listMode] = useListMode(); const filtersRef = useRef(null); const { @@ -52,9 +39,7 @@ export function QueryBuilderFilters({ const isCompact = Object.keys(queryStats).length === 1 && - ((selectedCube && - selectedCube === queryStats[selectedCube?.name]?.instance) || - !selectedCube); + ((selectedCube && selectedCube === queryStats[selectedCube?.name]?.instance) || !selectedCube); const timeDimensions = query.timeDimensions || []; const filters = query.filters || []; const segments = query.segments || []; @@ -82,8 +67,7 @@ export function QueryBuilderFilters({ return member.type === 'time' && !dateRanges.list.includes(member.name); }) || []; - const isFiltered = - filters.length > 0 || segments.length > 0 || dateRanges.list.length > 0; + const isFiltered = filters.length > 0 || segments.length > 0 || dateRanges.list.length > 0; const [isExpanded, setIsExpanded] = useState(isFiltered); @@ -131,24 +115,21 @@ export function QueryBuilderFilters({ useEffect(() => { ( - filtersRef?.current?.querySelector('button[data-is-invalid]') as - | HTMLButtonElement - | undefined + filtersRef?.current?.querySelector('button[data-is-invalid]') as HTMLButtonElement | undefined )?.click(); }, [dateRanges.list.length]); useEffect(() => { - const invalidTime = filtersRef?.current?.querySelector( - 'button[data-is-invalid]' - ) as HTMLButtonElement | undefined; + const invalidTime = filtersRef?.current?.querySelector('button[data-is-invalid]') as + | HTMLButtonElement + | undefined; if (invalidTime) { return; } const buttons = filtersRef?.current?.querySelectorAll('button'); - const lastButton = - buttons && buttons.length > 0 ? buttons[buttons.length - 1] : undefined; + const lastButton = buttons && buttons.length > 0 ? buttons[buttons.length - 1] : undefined; (lastButton as HTMLButtonElement | undefined)?.scrollIntoView({ behavior: 'smooth', @@ -215,9 +196,7 @@ export function QueryBuilderFilters({ return null; } - const member = - members.measures[filter.member] || - members.dimensions[filter.member]; + const member = members.measures[filter.member] || members.dimensions[filter.member]; return ( Filter - addFilter(name as string)} - > + addFilter(name as string)}> {availableMeasuresAndDimensions.map((dimension) => { return ( - + {getTypeIcon(dimension.type)} @@ -300,16 +271,10 @@ export function QueryBuilderFilters({ > Date Range - addDateRange(name as string)} - > + addDateRange(name as string)}> {availableTimeDimensions.map((dimension) => { return ( - + {getTypeIcon('time')} {dimension.name.split('.')[1]} @@ -330,19 +295,11 @@ export function QueryBuilderFilters({ addSegment(name as string)}> {availableSegments.map((segment) => { - return ( - - {segment.name.split('.')[1]} - - ); + return {segment.name.split('.')[1]}; })} - {!selectedCube && ( - - Select a cube or a view to add filters - - )} + {!selectedCube && Select a cube or a view to add filters} ) : null} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderGeneratedSQL.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderGeneratedSQL.tsx index 14d9a1ff4cb78..4aca4cf6e5735 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderGeneratedSQL.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderGeneratedSQL.tsx @@ -16,14 +16,8 @@ const EditSQLQueryButton = tasty(Button, { }); export function QueryBuilderGeneratedSQL() { - let { - query, - queryHash, - cubeApi, - isQueryEmpty, - verificationError, - openSqlRunner, - } = useQueryBuilderContext(); + let { query, queryHash, cubeApi, isQueryEmpty, verificationError, openSqlRunner } = + useQueryBuilderContext(); return useDeepMemo(() => { if (!isQueryEmpty) { @@ -61,9 +55,7 @@ export function QueryBuilderGeneratedSQL() { Copy {openSqlRunner ? ( - openSqlRunner?.(value)} - /> + openSqlRunner?.(value)} /> ) : undefined} } diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx index 0944ef3dad7f2..c46f5602b5bf0 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderInternals.tsx @@ -1,7 +1,7 @@ import { Block, Flow, tasty } from '@cube-dev/ui-kit'; import { memo, useEffect, useMemo, useRef, useState } from 'react'; -import { QUERY_BUILDER_COLOR_TOKENS } from './color-tokens'; +import { QUERY_BUILDER_COLOR_TOKENS } from './color-tokens'; import { useAutoSize, useEvent, useListMode, useLocalStorage } from './hooks'; import { useQueryBuilderContext } from './context'; import { Panel } from './components/Panel'; @@ -73,9 +73,7 @@ const QueryBuilderInternals = memo(function QueryBuilderInternals() { - {tab === 'results' && ( - - )} + {tab === 'results' && } {tab === 'generated-sql' && } {tab === 'json' && } {tab === 'sql' && } @@ -99,22 +97,13 @@ const QueryBuilderInternals = memo(function QueryBuilderInternals() { return ( {useMemo( - () => - listMode === 'bi' ? ( - - ) : ( - - ), + () => (listMode === 'bi' ? : ), [listMode] )} - + {useMemo( () => ( <> diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderResults.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderResults.tsx index d7ad647e42d62..daa971ec638d5 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderResults.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderResults.tsx @@ -55,6 +55,7 @@ import { import { formatCurrency, formatNumber } from './utils/formatters'; import { useDeepMemo, useIntervalEffect } from './hooks'; +import { OutdatedLabel } from './components/OutdatedLabel'; import { CopyButton } from './components/CopyButton'; import { Panel } from './components/Panel'; import { ListMemberButton } from './components/ListMemberButton'; @@ -661,7 +662,7 @@ interface MemberItem { export function QueryBuilderResults({ forceMinHeight }: { forceMinHeight?: boolean }) { const { isLoading, - isQueryTouched, + isResultOutdated, query, members, measures: measuresUpdater, @@ -703,7 +704,6 @@ export function QueryBuilderResults({ forceMinHeight }: { forceMinHeight?: boole const dimensions = query?.dimensions || []; const timeDimensions = query?.timeDimensions?.filter((member) => !!member.granularity) || []; const totalColumns = measures.length + dimensions.length + grouping.getAll().length; - const isOutdated = executedQuery && isQueryTouched; const isColumnsSelected = !!totalColumns; // scroll table to the top when page is changed @@ -1190,11 +1190,7 @@ export function QueryBuilderResults({ forceMinHeight }: { forceMinHeight?: boole - {isLoading ? ( - - ) : isOutdated ? ( - OUTDATED - ) : undefined} + {isLoading ? : isResultOutdated ? : undefined} {executedQuery && !isLoading && isColumnsSelected && queryRelated && ( diff --git a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderToolBar.tsx b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderToolBar.tsx index 17c3071c7ce64..af28cde9b394e 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderToolBar.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderToolBar.tsx @@ -34,7 +34,7 @@ export function QueryBuilderToolBar() { isLoading, error, resultSet, - isQueryTouched, + isResultOutdated, isQueryEmpty, isApiBlocked, stopQuery, @@ -81,7 +81,7 @@ export function QueryBuilderToolBar() { isDisabled={isQueryEmpty || !!verificationError || isVerifying || isApiBlocked} isLoading={isLoading} icon={ - !isQueryEmpty && (isLoading || !isQueryTouched) ? ( + !isQueryEmpty && (isLoading || !isResultOutdated) ? ( ) : ( diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/Accordion.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/Accordion.tsx index b99bb7560e1ca..ee663bac3c5c3 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/Accordion.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/Accordion.tsx @@ -9,15 +9,7 @@ const StyledAccordion = tasty({ }); export function Accordion(props: AccordionProps) { - const { - children, - qa, - isLazy, - size, - isSeparated, - titleStyles, - contentStyles, - } = props; + const { children, qa, isLazy, size, isSeparated, titleStyles, contentStyles } = props; return ( {children}; } -function shouldShowExtra( - showExtra: AccordionItemProps['showExtra'], - isHovered: boolean -) { +function shouldShowExtra(showExtra: AccordionItemProps['showExtra'], isHovered: boolean) { if (typeof showExtra === 'boolean') { return showExtra; } diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/AccordionProvider.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/AccordionProvider.tsx index cb6a15efaff6a..66edbb0689819 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/AccordionProvider.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/Accordion/AccordionProvider.tsx @@ -5,15 +5,7 @@ import { AccordionContextType, AccordionProviderProps } from './types'; const AccordionContext = createContext(null); export function AccordionProvider(props: AccordionProviderProps) { - const { - children, - qa, - isLazy, - size, - isSeparated, - titleStyles, - contentStyles, - } = props; + const { children, qa, isLazy, size, isSeparated, titleStyles, contentStyles } = props; return ( - | ReactElement[]; + children: ReactElement | ReactElement[]; qa?: string; isLazy?: boolean; size?: 'small' | 'normal'; @@ -19,10 +17,7 @@ export type AccordionContextType = Pick< 'size' | 'isSeparated' | 'isLazy' | 'titleStyles' | 'contentStyles' | 'qa' >; export type AccordionProviderProps = PropsWithChildren< - Pick< - AccordionProps, - 'size' | 'isSeparated' | 'isLazy' | 'titleStyles' | 'contentStyles' | 'qa' - > + Pick >; export type AccordionItemProps = { title: string | number; diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/AccordionCard/AccordionCard.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/AccordionCard/AccordionCard.tsx index 0c8f5478445f7..ab55d5975ccc9 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/AccordionCard/AccordionCard.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/AccordionCard/AccordionCard.tsx @@ -28,9 +28,7 @@ export function AccordionCard(props: AccordionCardProps) { contentStyles={CONTENT_STYLES} > - - {typeof children === 'function' ? children() : children} - + {typeof children === 'function' ? children() : children} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/EditQueryDialogForm.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/EditQueryDialogForm.tsx index 277c22eb35548..35f1a2228c6e7 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/EditQueryDialogForm.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/EditQueryDialogForm.tsx @@ -1,11 +1,4 @@ -import { - DialogForm, - LoadingIcon, - Radio, - Space, - TextArea, - useForm, -} from '@cube-dev/ui-kit'; +import { DialogForm, LoadingIcon, Radio, Space, TextArea, useForm } from '@cube-dev/ui-kit'; import { Meta, Query, validateQuery } from '@cubejs-client/core'; import { ValidationRule } from '@cube-dev/ui-kit/types/shared'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -49,11 +42,7 @@ function getGraphQLValidator(apiUrl: string, apiToken: string | null) { ]; } -function getJSONValidator( - apiUrl: string, - apiToken: string | null, - meta?: Meta | null -) { +function getJSONValidator(apiUrl: string, apiToken: string | null, meta?: Meta | null) { return [ { async validator(rule: ValidationRule, query: string) { @@ -105,20 +94,10 @@ async function pause(ms: number) { export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { const [form] = useForm(); - const { - onSubmit, - onDismiss, - defaultType = 'json', - query, - apiVersion, - } = props; + const { onSubmit, onDismiss, defaultType = 'json', query, apiVersion } = props; const [type, setType] = useState(defaultType); - const isGraphQLSupported = apiVersion - ? useServerCoreVersionGte('0.35.23', apiVersion) - : true; - const isGraphQLSupportedV1 = apiVersion - ? useServerCoreVersionGte('0.35.27', apiVersion) - : true; + const isGraphQLSupported = apiVersion ? useServerCoreVersionGte('0.35.23', apiVersion) : true; + const isGraphQLSupportedV1 = apiVersion ? useServerCoreVersionGte('0.35.27', apiVersion) : true; const [isBlocked, setIsBlocked] = useState(false); let { apiUrl, apiToken, meta } = useQueryBuilderContext(); @@ -130,9 +109,7 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { async function parseAndPrepareQuery(query: string, type: QueryType) { if (type === 'graphql') { return validateQuery( - JSON.parse( - await convertGraphQLToJsonQuery({ query, apiUrl, apiToken }) - ) || {} + JSON.parse(await convertGraphQLToJsonQuery({ query, apiUrl, apiToken })) || {} ); } @@ -154,6 +131,7 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { try { const query = validateQuery(JSON.parse(jsonQuery) || {}); const graphQLQuery = convertJsonQueryToGraphQL({ meta, query }); + const ungrouped = query.ungrouped; return convertGraphQLToJsonQuery({ apiUrl, @@ -161,7 +139,9 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { query: graphQLQuery, }).then( (jsonQuery) => { - form.setFieldValue('jsonQuery', jsonQuery); + const query = JSON.parse(jsonQuery); + + form.setFieldValue('jsonQuery', JSON.stringify({ ...query, ungrouped }, null, 2)); }, () => { throw ''; @@ -201,32 +181,24 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { type === 'json' ? JSON.stringify(query || {}, null, 2) : meta && query - ? convertJsonQueryToGraphQL({ meta, query }) - : ''; + ? convertJsonQueryToGraphQL({ meta, query }) + : ''; const onTypeChange = useCallback((type) => { setType(type); - const originalQuery = form.getFieldValue( - type === 'json' ? 'graphqlQuery' : 'jsonQuery' - ); + const originalQuery = form.getFieldValue(type === 'json' ? 'graphqlQuery' : 'jsonQuery'); setIsBlocked(true); - void parseAndPrepareQuery( - originalQuery, - type === 'json' ? 'graphql' : 'json' - ) + void parseAndPrepareQuery(originalQuery, type === 'json' ? 'graphql' : 'json') .then((query) => { const value = type === 'json' ? JSON.stringify(query || {}, null, 2) : query - ? convertJsonQueryToGraphQL({ meta, query }) - : ''; + ? convertJsonQueryToGraphQL({ meta, query }) + : ''; - form.setFieldValue( - type === 'json' ? 'jsonQuery' : 'graphqlQuery', - value - ); + form.setFieldValue(type === 'json' ? 'jsonQuery' : 'graphqlQuery', value); }) .finally(() => { setIsBlocked(false); @@ -234,10 +206,7 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { }, []); useEffect(() => { - form.setFieldValue( - type === 'json' ? 'jsonQuery' : 'graphqlQuery', - defaultQueryValue - ); + form.setFieldValue(type === 'json' ? 'jsonQuery' : 'graphqlQuery', defaultQueryValue); }, [JSON.stringify(query)]); useEffect(() => { @@ -248,17 +217,12 @@ export function EditQueryDialogForm(props: PasteQueryDialogFormProps) { await (type === 'json' ? onJsonBlur() : onGraphqlBlur()); const query = - type === 'json' - ? form.getFieldValue('jsonQuery') - : form.getFieldValue('graphqlQuery'); + type === 'json' ? form.getFieldValue('jsonQuery') : form.getFieldValue('graphqlQuery'); await parseAndPrepareQuery(query, type).then((query) => onSubmit(query)); }, []); - const graphqlRules = useMemo( - () => [getGraphQLValidator(apiUrl, apiToken)], - [apiUrl, apiToken] - ); + const graphqlRules = useMemo(() => [getGraphQLValidator(apiUrl, apiToken)], [apiUrl, apiToken]); const jsonRules = useMemo(() => [JSON_VALIDATOR, QUERY_VALIDATOR], []); return ( diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/OutdatedLabel.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/OutdatedLabel.tsx new file mode 100644 index 0000000000000..b77d895d23894 --- /dev/null +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/OutdatedLabel.tsx @@ -0,0 +1,24 @@ +import { Badge, TooltipProvider } from '@cube-dev/ui-kit'; + +import { useQueryBuilderContext } from '../context'; + +export function OutdatedLabel() { + let { isApiTokenChanged, isDataModelChanged, isQueryTouched, isResultOutdated } = + useQueryBuilderContext(); + + let title = ( + <> + {isApiTokenChanged &&
Security context has changed
} + {isDataModelChanged &&
Data model has been updated
} + {isQueryTouched &&
Query has changed
} + + ); + + return ( + + + OUTDATED + + + ); +} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx index 5fa3c4e54dd03..e0988049a9699 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx @@ -166,19 +166,11 @@ export function SidePanelCubeItem({ ); const segments = (filterString ? shownSegments : cube?.segments || []) .map((s) => s.name) - .filter( - (s) => - (mode === 'all' && isOpen) || - showAllMembers || - query?.segments?.includes(s) - ); + .filter((s) => (mode === 'all' && isOpen) || showAllMembers || query?.segments?.includes(s)); if (!filterString) { query?.dimensions?.forEach((dimension) => { - if ( - !dimensions?.includes(dimension) && - dimension.startsWith(`${name}.`) - ) { + if (!dimensions?.includes(dimension) && dimension.startsWith(`${name}.`)) { dimensions.push(dimension); } }); @@ -216,8 +208,7 @@ export function SidePanelCubeItem({ dateRanges.remove(name); } - const showMembers = - (isOpen || mode === 'query' || isUsed || !!filterString) && !isNonJoinable; + const showMembers = (isOpen || mode === 'query' || isUsed || !!filterString) && !isNonJoinable; const dimensionsSection = useMemo(() => { return showMembers && dimensions.length ? ( @@ -242,9 +233,7 @@ export function SidePanelCubeItem({ if (granularity) { return ( query?.timeDimensions?.some( - (td) => - td.dimension === item.name && - td.granularity === granularity + (td) => td.dimension === item.name && td.granularity === granularity ) || false ); } @@ -252,9 +241,8 @@ export function SidePanelCubeItem({ return query?.dimensions?.includes(item.name) || false; }} isFiltered={ - query?.timeDimensions?.some( - (td) => td.dimension === item.name && td.dateRange - ) || false + query?.timeDimensions?.some((td) => td.dimension === item.name && td.dateRange) || + false } onDimensionToggle={(dimension) => { dimensionsUpdater?.toggle(dimension); @@ -265,9 +253,7 @@ export function SidePanelCubeItem({ onMemberToggle?.(name); }} onToggleDataRange={ - !dateRanges.list.includes(item.name) - ? addDateRange - : removeDateRange + !dateRanges.list.includes(item.name) ? addDateRange : removeDateRange } /> ); @@ -282,9 +268,8 @@ export function SidePanelCubeItem({ filterString={filterString} isSelected={query?.dimensions?.includes(name) || false} isFiltered={ - query?.filters?.some( - (filter) => 'member' in filter && filter.member === name - ) || false + query?.filters?.some((filter) => 'member' in filter && filter.member === name) || + false } onAddFilter={addFilter} onRemoveFilter={removeFilter} @@ -329,9 +314,8 @@ export function SidePanelCubeItem({ filterString={filterString} isSelected={query?.measures?.includes(name) || false} isFiltered={ - query?.filters?.some( - (filter) => 'member' in filter && filter.member === name - ) || false + query?.filters?.some((filter) => 'member' in filter && filter.member === name) || + false } onAddFilter={addFilter} onRemoveFilter={removeFilter} @@ -383,8 +367,7 @@ export function SidePanelCubeItem({ const hasOverflow = useHasOverflow(textRef); - const noVisibleMembers = - !dimensions.length && !measures.length && !segments.length; + const noVisibleMembers = !dimensions.length && !measures.length && !segments.length; useEffect(() => { setShowAllMembers(false); @@ -417,11 +400,7 @@ export function SidePanelCubeItem({ type="neutral" size="small" icon={ - !showAllMembers ? ( - - ) : ( - - ) + !showAllMembers ? : } placeContent="start" onPress={() => setShowAllMembers(!showAllMembers)} @@ -435,9 +414,7 @@ export function SidePanelCubeItem({ return null; } else if (isOpen || mode === 'query') { return ( - - No members{mode === 'query' ? ' selected' : ''} - + No members{mode === 'query' ? ' selected' : ''} ); } } else { @@ -451,9 +428,7 @@ export function SidePanelCubeItem({ qaVal={name} icon={ isMissing ? ( - + ) : type === 'cube' ? ( ) : ( @@ -483,15 +458,9 @@ export function SidePanelCubeItem({ onPress={() => !isMissing && !isNonJoinable && onToggle?.(!isOpen)} > - {filterString ? ( - - ) : ( - name - )} + {filterString ? : name} - {description ? ( - - ) : undefined} + {description ? : undefined} {isPrivate ? : undefined} ); diff --git a/packages/cubejs-playground/src/QueryBuilderV2/hooks/deep-memo.ts b/packages/cubejs-playground/src/QueryBuilderV2/hooks/deep-memo.ts index dc95953d68c8a..a0596c4f7f4ef 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/hooks/deep-memo.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/hooks/deep-memo.ts @@ -2,10 +2,7 @@ import { DependencyList, useMemo } from 'react'; import { useDeepDependencies } from './deep-dependencies'; -export function useDeepMemo( - callback: () => T, - dependencies: DependencyList -) { +export function useDeepMemo(callback: () => T, dependencies: DependencyList) { const memoizedDependencies = useDeepDependencies(dependencies); return useMemo(callback, memoizedDependencies); diff --git a/packages/cubejs-playground/src/QueryBuilderV2/hooks/query-builder.ts b/packages/cubejs-playground/src/QueryBuilderV2/hooks/query-builder.ts index b4eceffa8dc00..07791a3aad3a3 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/hooks/query-builder.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/hooks/query-builder.ts @@ -29,7 +29,7 @@ import { prepareQuery, useIsFirstRender, } from '../utils'; -import { CubeStats } from '../types'; +import { CubeStats, QueryOptions } from '../types'; import { useEvent } from './event'; @@ -117,6 +117,10 @@ export function useQueryBuilder(props: QueryBuilderProps) { const [query, setQueryInstance] = useState(defaultQuery || {}); const [executedQuery, setExecutedQuery] = useState(null); + // Invalidation markers + const [isDataModelChanged, setIsDataModelChanged] = useState(false); + const [isApiTokenChanged, setIsApiTokenChanged] = useState(false); + // Calculate hash to invalidate query const queryHash = getQueryHash(query); @@ -203,6 +207,8 @@ export function useQueryBuilder(props: QueryBuilderProps) { return; } + setIsApiTokenChanged(false); + setIsDataModelChanged(false); setIsLoading(false); setExecutedQuery(queryCopy); setResultSet(resultSet); @@ -352,7 +358,7 @@ export function useQueryBuilder(props: QueryBuilderProps) { return originalQuery; } - query = { ...copiedQuery, ...newQuery }; + query = queryValidation({ ...copiedQuery, ...newQuery }); } else { query = queryValidation({ ...copiedQuery, @@ -854,6 +860,19 @@ export function useQueryBuilder(props: QueryBuilderProps) { onQueryChange?.({ query, chartType, pivotConfig }); }, [queryHash, chartType, pivotConfig]); + // Update invalidation markers + useEffect(() => { + if (executedQuery) { + setIsApiTokenChanged(true); + } + }, [cubeApi]); + + useEffect(() => { + if (executedQuery) { + setIsDataModelChanged(true); + } + }, [schemaVersion]); + // After time dimensions updated... useEffect(() => { let updateDateRanges = false; @@ -1103,6 +1122,11 @@ export function useQueryBuilder(props: QueryBuilderProps) { isMemberJoined: isMemberUsed, isCubeUsed, isQueryEmpty, + isApiTokenChanged, + isDataModelChanged, + isResultOutdated: + executedQuery && + (queryHash !== getQueryHash(executedQuery) || isApiTokenChanged || isDataModelChanged), queryHash, cubeApi, hasPrivateMembers, diff --git a/packages/cubejs-playground/src/QueryBuilderV2/types.ts b/packages/cubejs-playground/src/QueryBuilderV2/types.ts index ba5bcf2ced925..f39fa667b4e9a 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/types.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/types.ts @@ -1,10 +1,4 @@ -import { - ChartType, - Cube, - PivotConfig, - PreAggregationType, - Query, -} from '@cubejs-client/core'; +import { ChartType, Cube, PivotConfig, PreAggregationType, Query } from '@cubejs-client/core'; import { VizState } from '@cubejs-client/react'; import { FC, ReactNode } from 'react'; @@ -48,9 +42,7 @@ export interface QueryBuilderProps extends QueryBuilderSharedProps { defaultChartType?: ChartType; defaultPivotConfig?: PivotConfig; tracking?: QueryBuilderTracking; - onQueryChange?: - | ((data: { query: Query; chartType?: ChartType }) => void) - | undefined; + onQueryChange?: ((data: { query: Query; chartType?: ChartType }) => void) | undefined; } export type CubeStats = { @@ -74,3 +66,7 @@ export interface RequestStatusProps { extDbType: string; preAggregationType?: PreAggregationType; } + +export interface QueryOptions { + ungrouped?: boolean; +} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/get-query-hash.tsx b/packages/cubejs-playground/src/QueryBuilderV2/utils/get-query-hash.tsx index a7d411435353f..953b77eeadf29 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/utils/get-query-hash.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/get-query-hash.tsx @@ -33,5 +33,13 @@ export function getQueryHash(query: Query) { ); } - return JSON.stringify(queryCopy); + const orderedQuery = Object.keys(queryCopy) + .sort() + .reduce((acc, key) => { + acc[key as keyof Query] = queryCopy[key]; + + return acc; + }, {} as Query); + + return JSON.stringify(orderedQuery); } diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/graphql-converters.ts b/packages/cubejs-playground/src/QueryBuilderV2/utils/graphql-converters.ts index c7cb49d0175ca..c294adea7b6e7 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/utils/graphql-converters.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/graphql-converters.ts @@ -49,13 +49,7 @@ function metaToTypes(meta: Meta) { return types; } -export function convertJsonQueryToGraphQL({ - meta, - query, -}: { - meta?: Meta | null; - query: Query; -}) { +export function convertJsonQueryToGraphQL({ meta, query }: { meta?: Meta | null; query: Query }) { const types = meta ? metaToTypes(meta) : null; if (!types) { @@ -150,9 +144,7 @@ export class CubeGraphQLConverter { } public convert() { - return t.print( - baseCubeQuery(this.getCubeArgs(), this.getFieldsSelections()) - ); + return t.print(baseCubeQuery(this.getCubeArgs(), this.getFieldsSelections())); } private resolveFilter( @@ -231,10 +223,7 @@ export class CubeGraphQLConverter { ); } - return this.objectFieldFilter( - item.filters, - unCapitalize(item.cubeName) - ); + return this.objectFieldFilter(item.filters, unCapitalize(item.cubeName)); } else { return this.objectField( this.booleanFilter( @@ -289,10 +278,7 @@ export class CubeGraphQLConverter { const filters = Array.isArray(filter) ? filter : [filter]; const value = (f: any): t.ValueNode => { - const kind = - this.types[f.member || f.dimension] === 'number' - ? t.Kind.FLOAT - : t.Kind.STRING; + const kind = this.types[f.member || f.dimension] === 'number' ? t.Kind.FLOAT : t.Kind.STRING; if (['set', 'notSet'].includes(f.operator)) { return { @@ -332,13 +318,8 @@ export class CubeGraphQLConverter { ); } - if ( - singleValueOperators.includes(f.operator) && - (f.values || []).length > 1 - ) { - throw new Error( - `Filter operator "${f.operator}" must have a single value` - ); + if (singleValueOperators.includes(f.operator) && (f.values || []).length > 1) { + throw new Error(`Filter operator "${f.operator}" must have a single value`); } return { @@ -363,9 +344,7 @@ export class CubeGraphQLConverter { value: f.operator === 'equals' && (f.values || []).length <= 1 ? f.operator in OPERATORS_MAP - : OPERATORS_MAP[ - f.operator as keyof typeof OPERATORS_MAP - ] || f.operator, + : OPERATORS_MAP[f.operator as keyof typeof OPERATORS_MAP] || f.operator, }, value: value(f), }, @@ -393,10 +372,7 @@ export class CubeGraphQLConverter { return fields; } - private objectField( - fields: t.ObjectFieldNode | t.ObjectFieldNode[], - fieldName: string - ) { + private objectField(fields: t.ObjectFieldNode | t.ObjectFieldNode[], fieldName: string) { return { kind: t.Kind.OBJECT_FIELD, name: { @@ -411,10 +387,7 @@ export class CubeGraphQLConverter { } // OR: [{ orders: { status: { equals: "active"} }}] - private booleanFilter( - kind: FilterKind, - values: t.ObjectValueNode[] - ): t.ObjectFieldNode { + private booleanFilter(kind: FilterKind, values: t.ObjectValueNode[]): t.ObjectFieldNode { return { kind: t.Kind.OBJECT_FIELD, name: { @@ -502,7 +475,7 @@ export class CubeGraphQLConverter { private getCubeArgs() { const cubeArgsKeys: [ string, - typeof t.Kind.STRING | typeof t.Kind.INT | typeof t.Kind.OBJECT + typeof t.Kind.STRING | typeof t.Kind.INT | typeof t.Kind.OBJECT, ][] = [ ['timezone', t.Kind.STRING], ['limit', t.Kind.INT], @@ -584,18 +557,13 @@ export class CubeGraphQLConverter { initCube(cubeName); // eslint-disable-next-line - const currentField = this.cubes[cubeName].fields.find( - ({ name }) => name === field - ); + const currentField = this.cubes[cubeName].fields.find(({ name }) => name === field); this.cubes[cubeName].fields.push({ name: field, ...(gqlGranularity ? { - granularities: [ - ...(currentField?.granularities || []), - gqlGranularity, - ], + granularities: [...(currentField?.granularities || []), gqlGranularity], } : null), }); @@ -607,9 +575,7 @@ export class CubeGraphQLConverter { const [name, field] = td.dimension.split('.'); const cubeFieldName = `${name}.${field}`; if (td.granularity) { - map[cubeFieldName] = (map[cubeFieldName] || []).concat([ - td.granularity, - ]); + map[cubeFieldName] = (map[cubeFieldName] || []).concat([td.granularity]); } }); @@ -618,9 +584,7 @@ export class CubeGraphQLConverter { const cubeName = unCapitalize(name); initCube(cubeName); - const existingField = this.cubes[cubeName].fields.find( - (f) => f.name === field - ); + const existingField = this.cubes[cubeName].fields.find((f) => f.name === field); if (existingField) { existingField.granularities = uniqArray([ @@ -650,9 +614,7 @@ export class CubeGraphQLConverter { ); } - const exists = this.cubes[gqlCubeName].fields.find( - ({ name }) => name === member - ); + const exists = this.cubes[gqlCubeName].fields.find(({ name }) => name === member); if (!exists) { throw new Error( diff --git a/packages/cubejs-testing/cypress/integration/playground-explore.spec.js b/packages/cubejs-testing/cypress/integration/playground-explore.spec.js index 747f9d8f2973d..7673f11a155d5 100644 --- a/packages/cubejs-testing/cypress/integration/playground-explore.spec.js +++ b/packages/cubejs-testing/cypress/integration/playground-explore.spec.js @@ -74,6 +74,8 @@ context("Playground: Explore Page", () => { cy.setQuery(ordersCountQuery); cy.wait(["@context"]); cy.getByTestId("live-preview-btn").should("exist"); + // avoid crashing here + cy.wait(10000); }); it("does now show the Live Preview button when livePreview is disabled", () => {