From f5cbf8c183c60a97e7f98af059efeb872739a6f8 Mon Sep 17 00:00:00 2001 From: Peter Steffey Date: Tue, 23 Feb 2021 14:01:30 -0500 Subject: [PATCH 1/5] QueryBuilder Improvements - types & safety Updates TypeScript types to be more accurate (and fix some mistakes), adding PropTypes for non-typescript safety. Refactoring updateVizState to be more consistent and easier to understand, updating orderMember handling to be less problematic and use a single source of truth. Convert to a fully uncontrolled component to follow react best practices and prevent unexpected behavior. --- .gitignore | 2 + packages/cubejs-client-core/index.d.ts | 1 + packages/cubejs-client-react/index.d.ts | 96 ++++-- packages/cubejs-client-react/package.json | 1 + .../cubejs-client-react/src/QueryBuilder.jsx | 317 ++++++++---------- 5 files changed, 197 insertions(+), 220 deletions(-) diff --git a/.gitignore b/.gitignore index fa518f0074193..d46c4ec44073e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ yarn-error.log **/coverage lerna-debug.log tsconfig.tsbuildinfo +**/*.swo +**/*.swp diff --git a/packages/cubejs-client-core/index.d.ts b/packages/cubejs-client-core/index.d.ts index ce59daf1e2ad3..35bb133ab713b 100644 --- a/packages/cubejs-client-core/index.d.ts +++ b/packages/cubejs-client-core/index.d.ts @@ -641,6 +641,7 @@ declare module '@cubejs-client/core' { */ tableColumns(pivotConfig?: PivotConfig): TableColumn[]; + totalRow(): ChartPivotRow; query(): Query; rawData(): T[]; annotation(): QueryAnnotations; diff --git a/packages/cubejs-client-react/index.d.ts b/packages/cubejs-client-react/index.d.ts index d9df0d3f6d8d6..91fdc359854ec 100644 --- a/packages/cubejs-client-react/index.d.ts +++ b/packages/cubejs-client-react/index.d.ts @@ -5,16 +5,18 @@ import { ResultSet, Filter, PivotConfig, - MemberType, TCubeMeasure, TCubeDimension, - TCubeMember, + TCubeSegment, + TimeDimension, ProgressResponse, TDryRunResponse, TOrderMember, QueryOrder, TQueryOrderArray, TSourceAxis, + TimeDimensionComparison, + TimeDimensionRanged, } from '@cubejs-client/core'; /** @@ -141,9 +143,8 @@ declare module '@cubejs-client/react' { type ChartType = 'line' | 'bar' | 'table' | 'area' | 'number' | 'pie'; type VizState = { - [key: string]: any; + query?: Query; pivotConfig?: PivotConfig; - shouldApplyHeuristicOrder?: boolean; chartType?: ChartType; }; @@ -155,12 +156,12 @@ declare module '@cubejs-client/react' { /** * Default query */ - query?: Query; - vizState?: VizState; + defaultQuery?: Query; + defaultChartType?: ChartType; + initialVizState?: VizState; /** * @default defaultChartType line */ - defaultChartType?: ChartType; /** * Defaults to `false`. This means that the default heuristics will be applied. For example: when the query is empty and you select a measure that has a default time dimension it will be pushed to the query. * @default disableHeuristics false @@ -173,10 +174,9 @@ declare module '@cubejs-client/react' { */ stateChangeHeuristics?: (state: QueryBuilderState) => QueryBuilderState; /** - * Called by the `QueryBuilder` when the query state has changed. Use it when state is maintained outside of the `QueryBuilder` component. + * Called by the `QueryBuilder` when the viz state has changed. Use it to save state outside of the `QueryBuilder` component. */ - setQuery?: (query: Query) => void; - setVizState?: (vizState: VizState) => void; + onVizStateChanged?: (vizState: VizState) => void; }; type QueryBuilderState = VizState & { @@ -188,14 +188,15 @@ declare module '@cubejs-client/react' { resultSet?: ResultSet | null; error?: Error | null; loadingState?: TLoadingState; + /** * Indicates whether the query is ready to be displayed or not */ isQueryPresent: boolean; - measures: string[]; - dimensions: string[]; - segments: string[]; - timeDimensions: Filter[]; + measures: (TCubeMeasure & { index: number })[]; + dimensions: (TCubeDimension & { index: number })[]; + segments: (TCubeSegment & { index: number })[]; + timeDimensions: (TimeDimensionWithExtraFields & { index: number })[]; /** * An array of available measures to select. They are loaded via the API from Cube.js Backend. @@ -212,18 +213,18 @@ declare module '@cubejs-client/react' { /** * An array of available segments to select. They are loaded via the API from Cube.js Backend. */ - availableSegments: TCubeMember[]; + availableSegments: TCubeSegment[]; - updateMeasures: MemberUpdater; - updateDimensions: MemberUpdater; - updateSegments: MemberUpdater; - updateTimeDimensions: MemberUpdater; - updateFilters: MemberUpdater; + updateMeasures: MeasureUpdater; + updateDimensions: DimensionUpdater; + updateSegments: SegmentUpdater; + updateTimeDimensions: TimeDimensionUpdater; + updateFilters: FilterUpdater; /** * Used for partial of full query update */ updateQuery: (query: Query) => void; - filters: Filter[]; + filters: (FilterWithExtraFields & { index: number })[]; /** * All possible order members for the query */ @@ -235,22 +236,24 @@ declare module '@cubejs-client/react' { /** * See [Pivot Config](@cubejs-client-core#types-pivot-config) */ - pivotConfig: PivotConfig; + pivotConfig?: PivotConfig; /** * Helper method for `pivotConfig` updates */ updatePivotConfig: PivotConfigUpdater; - + /** * Selected chart type */ - chartType: ChartType; - + chartType?: ChartType; + /** * Used for chart type update */ updateChartType: (chartType: ChartType) => void; + validatedQuery: Query; + refresh: () => void; }; /** @@ -477,25 +480,44 @@ declare module '@cubejs-client/react' { * /> * ``` */ - type MemberUpdater = { - add: (member: MemberType) => void; - remove: (member: MemberType) => void; - update: (member: MemberType, updateWith: MemberType) => void; + + type FilterWithExtraFields = Omit & { + dimension: TCubeDimension | TCubeMeasure; + operators: { name: string; title: string }[]; + }; + type TimeDimensionWithExtraFields = Omit & { + dimension: TCubeDimension & { granularities: { name: string; title: string }[] }; + }; + + type MemberUpdater = { + add: (member: T) => void; + remove: (member: { index: number }) => void; + update: (member: { index: number }, updateWith: T) => void; }; + type DimensionUpdater = MemberUpdater; + type MeasureUpdater = MemberUpdater; + type SegmentUpdater = MemberUpdater; + // Only require the fields that are actually used (otherwise fields like `operators` are required just to add/update) + type TimeDimensionUpdater = MemberUpdater< + (Pick | Pick) & { dimension: TCubeDimension } + >; + type FilterUpdater = MemberUpdater< + Pick & { dimension: TCubeDimension | TCubeMeasure } + >; type OrderUpdater = { set: (memberId: string, order: QueryOrder | 'none') => void; - update: (order: TQueryOrderArray) => void; + update: (order: Query['order']) => void; reorder: (sourceIndex: number, destinationIndex: number) => void; }; type PivotConfigUpdater = { - moveItem: ( - sourceIndex: number, - destinationIndex: number, - sourceAxis: TSourceAxis, - destinationAxis: TSourceAxis - ) => void; - update: (pivotConfig: PivotConfig & { limit: number }) => void; + moveItem: (args: { + sourceIndex: number; + destinationIndex: number; + sourceAxis: TSourceAxis; + destinationAxis: TSourceAxis; + }) => void; + update: (pivotConfig: PivotConfig & { limit?: number }) => void; }; } diff --git a/packages/cubejs-client-react/package.json b/packages/cubejs-client-react/package.json index 5e00c558cde95..fba3358be67b6 100644 --- a/packages/cubejs-client-react/package.json +++ b/packages/cubejs-client-react/package.json @@ -12,6 +12,7 @@ "@babel/runtime": "^7.1.2", "@cubejs-client/core": "^0.26.19", "core-js": "^3.6.5", + "prop-types": "^15.7.2", "ramda": "^0.27.0" }, "peerDependencies": { diff --git a/packages/cubejs-client-react/src/QueryBuilder.jsx b/packages/cubejs-client-react/src/QueryBuilder.jsx index e96a71775c6fc..a815bf62b2907 100644 --- a/packages/cubejs-client-react/src/QueryBuilder.jsx +++ b/packages/cubejs-client-react/src/QueryBuilder.jsx @@ -1,6 +1,7 @@ import React from 'react'; -import { prop, uniqBy, indexBy, fromPairs } from 'ramda'; +import { prop, uniqBy, pick } from 'ramda'; import { ResultSet, moveItemInArray, defaultOrder, flattenFilters, getQueryMembers } from '@cubejs-client/core'; +import PropTypes from 'prop-types'; import QueryRenderer from './QueryRenderer.jsx'; import CubeContext from './CubeContext'; @@ -16,25 +17,6 @@ const granularities = [ ]; export default class QueryBuilder extends React.Component { - static getDerivedStateFromProps(props, state) { - const nextState = { - ...state, - ...(props.vizState || {}), - }; - - if (Array.isArray(props.query)) { - throw new Error('Array of queries is not supported.'); - } - - return { - ...nextState, - query: { - ...nextState.query, - ...(props.query || {}), - }, - }; - } - static resolveMember(type, { meta, query }) { if (!meta) { return []; @@ -64,56 +46,25 @@ export default class QueryBuilder extends React.Component { })); } - static getOrderMembers(state) { - const { query, meta } = state; - - if (!meta) { - return []; - } - - const toOrderMember = (member) => ({ - id: member.name, - title: member.title, - }); - - return uniqBy( - prop('id'), - [ - ...QueryBuilder.resolveMember('measures', state).map(toOrderMember), - ...QueryBuilder.resolveMember('dimensions', state).map(toOrderMember), - ...QueryBuilder.resolveMember('timeDimensions', state).map((td) => toOrderMember(td.dimension)), - ].map((member) => ({ - ...member, - order: query.order?.[member.id] || 'none', - })) - ); - } - constructor(props) { super(props); this.state = { - query: props.query, - chartType: 'line', - orderMembers: [], - pivotConfig: null, - validatedQuery: props.query, + chartType: props.defaultChartType, + query: props.defaultQuery, + ...props.initialVizState, missingMembers: [], isFetchingMeta: false, - ...props.vizState, }; this.mutexObj = {}; } - async componentDidMount() { - await this.fetchMeta(); + componentDidMount() { + this.fetchMeta(); } fetchMeta = async () => { - const { query, pivotConfig } = this.state; - let dryRunResponse; - let missingMembers = []; let meta; let metaError = null; @@ -124,21 +75,14 @@ export default class QueryBuilder extends React.Component { metaError = error; } - if (this.isQueryPresent()) { - missingMembers = this.getMissingMembers(query, meta); - - if (missingMembers.length === 0) { - dryRunResponse = this.cubejsApi().dryRun(query); - } - } - this.setState({ meta, metaError, - orderMembers: QueryBuilder.getOrderMembers({ meta, query }), - pivotConfig: ResultSet.getNormalizedPivotConfig(dryRunResponse?.pivotQuery || {}, pivotConfig), - missingMembers, isFetchingMeta: false + }, () => { + // Run update query to force viz state update + // This will catch any new missing members, and also validate the query against the new meta + this.updateQuery({}); }); } @@ -216,7 +160,7 @@ export default class QueryBuilder extends React.Component { meta, metaError, query, - orderMembers = [], + queryError, chartType, pivotConfig, validatedQuery, @@ -239,25 +183,60 @@ export default class QueryBuilder extends React.Component { index: i, })); + const measures = QueryBuilder.resolveMember('measures', this.state); + const dimensions = QueryBuilder.resolveMember('dimensions', this.state); + const timeDimensions = QueryBuilder.resolveMember('timeDimensions', this.state); + const segments = (query.segments || []).map((m, i) => ({ index: i, ...meta.resolveMember(m, 'segments') })); + + const availableMeasures = meta.membersForQuery(query, 'measures'); + const availableDimensions = meta.membersForQuery(query, 'dimensions'); + const availableSegments = meta.membersForQuery(query, 'segments'); + + let orderMembers = uniqBy( + prop('id'), + [ + ...(Array.isArray(query.order) ? query.order : Object.entries(query.order)) + .map(([id, order]) => ({ + id, + order, + title: meta.resolveMember(id, ['measures', 'dimensions']).title + })), + // uniqBy prefers first, so these will only be added if not already in the query + ...[ + ...measures, + ...dimensions + ].map(({ name, title }) => ({ id: name, title, order: 'none' })) + ] + ); + + // Preserve order until the members change or manually re-ordered + // This is needed so that when an order member becomes active, it doesn't jump to the top of the list + const orderMemberOrderKey = JSON.stringify(orderMembers.map(({ id }) => id).sort()); + if (this.orderMemberOrderKey && this.orderMemberOrder && orderMemberOrderKey === this.orderMemberOrderKey) { + orderMembers = this.orderMemberOrder.map(id => orderMembers.find(member => member.id === id)); + } else { + this.orderMemberOrderKey = orderMemberOrderKey; + this.orderMemberOrder = orderMembers.map(({ id }) => id); + } + return { meta, metaError, query, + error: queryError, // Match same name as QueryRenderer prop validatedQuery, isQueryPresent: this.isQueryPresent(), chartType, - measures: QueryBuilder.resolveMember('measures', this.state), - dimensions: QueryBuilder.resolveMember('dimensions', this.state), - timeDimensions: QueryBuilder.resolveMember('timeDimensions', this.state), - segments: ((meta && query.segments) || []).map((m, i) => ({ index: i, ...meta.resolveMember(m, 'segments') })), + measures, + dimensions, + timeDimensions, + segments, filters, orderMembers, - availableMeasures: (meta && meta.membersForQuery(query, 'measures')) || [], - availableDimensions: (meta && meta.membersForQuery(query, 'dimensions')) || [], - availableTimeDimensions: ((meta && meta.membersForQuery(query, 'dimensions')) || []).filter( - (m) => m.type === 'time' - ), - availableSegments: (meta && meta.membersForQuery(query, 'segments')) || [], + availableMeasures, + availableDimensions, + availableTimeDimensions: availableDimensions.filter(m => m.type === 'time'), + availableSegments, updateQuery: (queryUpdate) => this.updateQuery(queryUpdate), updateMeasures: updateMethods('measures'), updateDimensions: updateMethods('dimensions'), @@ -266,12 +245,15 @@ export default class QueryBuilder extends React.Component { updateFilters: updateMethods('filters', toFilter), updateChartType: (newChartType) => this.updateVizState({ chartType: newChartType }), updateOrder: { - set: (memberId, order = 'asc') => { - this.updateVizState({ - orderMembers: orderMembers.map((orderMember) => ({ - ...orderMember, - order: orderMember.id === memberId ? order : orderMember.order, - })), + set: (memberId, newOrder = 'asc') => { + this.updateQuery({ + order: + orderMembers + .map((orderMember) => ({ + ...orderMember, + order: orderMember.id === memberId ? newOrder : orderMember.order, + })) + .reduce((acc, { id, order }) => (order !== 'none' ? ([...acc, [id, order]]) : acc), []) }); }, update: (order) => { @@ -284,8 +266,11 @@ export default class QueryBuilder extends React.Component { return; } - this.updateVizState({ - orderMembers: moveItemInArray(orderMembers, sourceIndex, destinationIndex), + this.orderMemberOrderKey = null; + this.updateQuery({ + order: + moveItemInArray(orderMembers, sourceIndex, destinationIndex) + .reduce((acc, { id, order }) => (order !== 'none' ? ([...acc, [id, order]]) : acc), []) }); }, }, @@ -309,27 +294,22 @@ export default class QueryBuilder extends React.Component { nextPivotConfig[sourceAxis].splice(sourceIndex, 1); nextPivotConfig[destinationAxis].splice(destinationIndex, 0, id); - this.updateVizState({ - pivotConfig: nextPivotConfig, - }); + this.updateVizState({ pivotConfig: nextPivotConfig }); }, update: (config) => { const { limit } = config; - if (limit == null) { - this.updateVizState({ - pivotConfig: { - ...pivotConfig, - ...config, - }, - }); - } else { - this.updateQuery({ limit }); - } + this.updateVizState({ + pivotConfig: { + ...pivotConfig, + ...config, + }, + ...(limit ? { query: { ...query, limit } } : null) + }); }, }, missingMembers, - refresh: this.fetchMeta, + refresh: () => this.fetchMeta(), isFetchingMeta, ...queryRendererProps, }; @@ -337,109 +317,63 @@ export default class QueryBuilder extends React.Component { updateQuery(queryUpdate) { const { query } = this.state; + const newQuery = { + ...query, + ...queryUpdate + }; this.updateVizState({ - query: { - ...query, - ...queryUpdate, - }, + query: newQuery }); } async updateVizState(state) { - const { setQuery, setVizState } = this.props; const { query: stateQuery, pivotConfig: statePivotConfig, meta } = this.state; - let finalState = this.applyStateChangeHeuristics(state); - const query = { ...(finalState.query || stateQuery) }; + // Only accept the 3 objects that are part of VizState + state = pick(['query', 'pivotConfig', 'chartType'], state); - const runSetters = (currentState) => { - if (setVizState) { - const { meta: _, validatedQuery, ...toSet } = currentState; - setVizState(toSet); - } - if (currentState.query && setQuery) { - setQuery(currentState.query); - } - }; + const finalState = this.applyStateChangeHeuristics(state); + if (!finalState.query) finalState.query = stateQuery; if (finalState.shouldApplyHeuristicOrder) { - query.order = defaultOrder(query); + finalState.query.order = defaultOrder(finalState.query); } - const updatedOrderMembers = indexBy( - prop('id'), - QueryBuilder.getOrderMembers({ - ...this.state, - ...finalState, - }) - ); - const currentOrderMemberIds = (finalState.orderMembers || []).map(({ id }) => id); - const currentOrderMembers = (finalState.orderMembers || []).filter(({ id }) => Boolean(updatedOrderMembers[id])); - - Object.entries(updatedOrderMembers).forEach(([id, orderMember]) => { - if (!currentOrderMemberIds.includes(id)) { - currentOrderMembers.push(orderMember); - } - }); - - const nextQuery = { - ...query, - order: fromPairs(currentOrderMembers.map(({ id, order }) => (order !== 'none' ? [id, order] : false)).filter(Boolean)) - }; - finalState.pivotConfig = ResultSet.getNormalizedPivotConfig( - nextQuery, + finalState.query, finalState.pivotConfig !== undefined ? finalState.pivotConfig : statePivotConfig ); - const missingMembers = this.getMissingMembers(nextQuery, meta); + finalState.missingMembers = this.getMissingMembers(finalState.query, meta); - runSetters({ - ...state, - query: nextQuery, - orderMembers: currentOrderMembers, - }); - - this.setState({ - ...finalState, - query: nextQuery, - orderMembers: currentOrderMembers, - missingMembers - }); - - let pivotQuery = {}; - if (QueryRenderer.isQueryPresent(query) && missingMembers.length === 0) { + this.setState(finalState); // Update optimistically so that UI does not stutter + + if (QueryRenderer.isQueryPresent(finalState.query) && finalState.missingMembers.length === 0) { + finalState.queryError = null; try { - const response = await this.cubejsApi().dryRun(query, { + const response = await this.cubejsApi().dryRun(finalState.query, { mutexObj: this.mutexObj, }); - pivotQuery = response.pivotQuery; if (finalState.shouldApplyHeuristicOrder) { - nextQuery.order = (response.queryOrder || []).reduce((memo, current) => ({ ...memo, ...current }), {}); + finalState.query.order = (response.queryOrder || []).reduce((memo, current) => ({ ...memo, ...current }), {}); } - if (QueryRenderer.isQueryPresent(stateQuery)) { - finalState = { - ...finalState, - query: nextQuery, - pivotConfig: ResultSet.getNormalizedPivotConfig(pivotQuery, finalState.pivotConfig), - }; - - this.setState({ - ...finalState, - validatedQuery: this.validatedQuery(finalState), - }); - runSetters({ - ...this.state, - ...finalState, - }); - } + finalState.pivotConfig = ResultSet.getNormalizedPivotConfig(response.pivotQuery, finalState.pivotConfig); + finalState.validatedQuery = this.validatedQuery(finalState); } catch (error) { console.error(error); + finalState.queryError = error; } } + + this.setState(finalState, () => { + const { onVizStateChanged } = this.props; + if (onVizStateChanged) { + onVizStateChanged(pick(['chartType', 'pivotConfig', 'query'], this.state)); + } + }); } validatedQuery(state) { @@ -447,12 +381,12 @@ export default class QueryBuilder extends React.Component { return { ...query, - filters: (query.filters || []).filter((f) => f.operator), + filters: (query.filters || []).filter(f => f.operator), }; } defaultHeuristics(newState) { - const { query, sessionGranularity } = this.state; + const { query, sessionGranularity, meta } = this.state; const defaultGranularity = sessionGranularity || 'day'; if (Array.isArray(query)) { @@ -463,8 +397,6 @@ export default class QueryBuilder extends React.Component { const oldQuery = query; let newQuery = newState.query; - const { meta } = this.state; - if ( (oldQuery.timeDimensions || []).length === 1 && (newQuery.timeDimensions || []).length === 1 @@ -608,9 +540,12 @@ export default class QueryBuilder extends React.Component { } render() { - const { query } = this.state; + const { query, meta } = this.state; const { cubejsApi, render, wrapWithQueryRenderer } = this.props; + // Wait for the meta to load before attempting to render + if (!meta) return null; + if (wrapWithQueryRenderer) { return ( Date: Fri, 26 Feb 2021 12:24:23 -0500 Subject: [PATCH 2/5] Adding meta props to QueryBuilderRenderProps --- packages/cubejs-client-react/index.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-client-react/index.d.ts b/packages/cubejs-client-react/index.d.ts index 91fdc359854ec..e7c97e01ab899 100644 --- a/packages/cubejs-client-react/index.d.ts +++ b/packages/cubejs-client-react/index.d.ts @@ -13,10 +13,10 @@ import { TDryRunResponse, TOrderMember, QueryOrder, - TQueryOrderArray, TSourceAxis, TimeDimensionComparison, TimeDimensionRanged, + Meta, } from '@cubejs-client/core'; /** @@ -189,6 +189,10 @@ declare module '@cubejs-client/react' { error?: Error | null; loadingState?: TLoadingState; + meta: Meta; + metaError?: Error | null; + isFetchingMeta: boolean; + /** * Indicates whether the query is ready to be displayed or not */ From 2b1e8619b8d750294d9030f7daf4d9ccbf83f988 Mon Sep 17 00:00:00 2001 From: Peter Steffey Date: Fri, 26 Feb 2021 21:06:16 -0500 Subject: [PATCH 3/5] Removing meta render block so that meta error is shown --- .../cubejs-client-react/src/QueryBuilder.jsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/cubejs-client-react/src/QueryBuilder.jsx b/packages/cubejs-client-react/src/QueryBuilder.jsx index a815bf62b2907..15a68cffa6d06 100644 --- a/packages/cubejs-client-react/src/QueryBuilder.jsx +++ b/packages/cubejs-client-react/src/QueryBuilder.jsx @@ -170,36 +170,38 @@ export default class QueryBuilder extends React.Component { const flatFilters = uniqBy( prop('member'), - flattenFilters((meta && query.filters) || []).map((filter) => ({ + flattenFilters(query.filters || []).map((filter) => ({ ...filter, member: filter.member || filter.dimension, })) ); - const filters = flatFilters.map((m, i) => ({ - ...m, - dimension: meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']), - operators: meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']), - index: i, - })); + const filters = meta + ? flatFilters.map((m, i) => ({ + ...m, + dimension: meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']), + operators: meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']), + index: i, + })) + : []; const measures = QueryBuilder.resolveMember('measures', this.state); const dimensions = QueryBuilder.resolveMember('dimensions', this.state); const timeDimensions = QueryBuilder.resolveMember('timeDimensions', this.state); - const segments = (query.segments || []).map((m, i) => ({ index: i, ...meta.resolveMember(m, 'segments') })); + const segments = ((meta && query.segments) || []).map((m, i) => ({ index: i, ...meta.resolveMember(m, 'segments') })); - const availableMeasures = meta.membersForQuery(query, 'measures'); - const availableDimensions = meta.membersForQuery(query, 'dimensions'); - const availableSegments = meta.membersForQuery(query, 'segments'); + const availableMeasures = meta ? meta.membersForQuery(query, 'measures') : []; + const availableDimensions = meta ? meta.membersForQuery(query, 'dimensions') : []; + const availableSegments = meta ? meta.membersForQuery(query, 'segments') : []; let orderMembers = uniqBy( prop('id'), [ - ...(Array.isArray(query.order) ? query.order : Object.entries(query.order)) + ...(Array.isArray(query.order) ? query.order : Object.entries(query.order || {})) .map(([id, order]) => ({ id, order, - title: meta.resolveMember(id, ['measures', 'dimensions']).title + title: meta ? meta.resolveMember(id, ['measures', 'dimensions']).title : '' })), // uniqBy prefers first, so these will only be added if not already in the query ...[ @@ -543,9 +545,6 @@ export default class QueryBuilder extends React.Component { const { query, meta } = this.state; const { cubejsApi, render, wrapWithQueryRenderer } = this.props; - // Wait for the meta to load before attempting to render - if (!meta) return null; - if (wrapWithQueryRenderer) { return ( Date: Sat, 27 Feb 2021 15:34:16 -0500 Subject: [PATCH 4/5] Fixing lint error --- packages/cubejs-client-react/src/QueryBuilder.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cubejs-client-react/src/QueryBuilder.jsx b/packages/cubejs-client-react/src/QueryBuilder.jsx index 15a68cffa6d06..3ab7d49c5bbac 100644 --- a/packages/cubejs-client-react/src/QueryBuilder.jsx +++ b/packages/cubejs-client-react/src/QueryBuilder.jsx @@ -542,7 +542,7 @@ export default class QueryBuilder extends React.Component { } render() { - const { query, meta } = this.state; + const { query } = this.state; const { cubejsApi, render, wrapWithQueryRenderer } = this.props; if (wrapWithQueryRenderer) { From a8a6961b18b58bd221ec452f2b4f5f2c2d68f2ad Mon Sep 17 00:00:00 2001 From: Peter Steffey Date: Mon, 1 Mar 2021 08:07:26 -0500 Subject: [PATCH 5/5] Undoing unecessary changes --- .../cubejs-client-react/src/QueryBuilder.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/cubejs-client-react/src/QueryBuilder.jsx b/packages/cubejs-client-react/src/QueryBuilder.jsx index 3ab7d49c5bbac..eeb74a34b6a57 100644 --- a/packages/cubejs-client-react/src/QueryBuilder.jsx +++ b/packages/cubejs-client-react/src/QueryBuilder.jsx @@ -296,7 +296,9 @@ export default class QueryBuilder extends React.Component { nextPivotConfig[sourceAxis].splice(sourceIndex, 1); nextPivotConfig[destinationAxis].splice(destinationIndex, 0, id); - this.updateVizState({ pivotConfig: nextPivotConfig }); + this.updateVizState({ + pivotConfig: nextPivotConfig + }); }, update: (config) => { const { limit } = config; @@ -319,13 +321,12 @@ export default class QueryBuilder extends React.Component { updateQuery(queryUpdate) { const { query } = this.state; - const newQuery = { - ...query, - ...queryUpdate - }; this.updateVizState({ - query: newQuery + query: { + ...query, + ...queryUpdate, + }, }); } @@ -336,7 +337,7 @@ export default class QueryBuilder extends React.Component { state = pick(['query', 'pivotConfig', 'chartType'], state); const finalState = this.applyStateChangeHeuristics(state); - if (!finalState.query) finalState.query = stateQuery; + if (!finalState.query) finalState.query = { ...stateQuery }; if (finalState.shouldApplyHeuristicOrder) { finalState.query.order = defaultOrder(finalState.query); @@ -383,7 +384,7 @@ export default class QueryBuilder extends React.Component { return { ...query, - filters: (query.filters || []).filter(f => f.operator), + filters: (query.filters || []).filter((f) => f.operator), }; }