diff --git a/docs/Cube.js-Frontend/@cubejs-client-vue.md b/docs/Cube.js-Frontend/@cubejs-client-vue.md index 367321a45119c..9681ed7f47108 100644 --- a/docs/Cube.js-Frontend/@cubejs-client-vue.md +++ b/docs/Cube.js-Frontend/@cubejs-client-vue.md @@ -15,7 +15,7 @@ into Vue.js app. ### Props -- `query`: analytic query. [Learn more about it's format](query-format). +- `query`: query parameters ([learn more about its format](query-format)). - `cubejsApi`: `CubejsApi` instance to use. ### Slots @@ -24,19 +24,21 @@ into Vue.js app. ##### Slot Props -- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convient interface for data munipulation. +- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convenient interface for data manipulation. #### Empty Slot -This slot functions as a empty/loading state in which if the query is loading or empty you can show -something in the meantime +This slot functions as a empty/loading state in which if the query is loading or empty so you can show +something in the meantime. #### Error Slot +This slot will be rendered if any error happens while the query is loading or rendering. + ##### Slot Props -- `error`: will show the details from error. -- `sqlQuery`: will show tried query +- `error`: the error. +- `sqlQuery`: the attempted query. ### Example ```js @@ -93,11 +95,12 @@ export default { ``` ## QueryBuilder -`` is used to build interactive analytics query builders. It abstracts state management and API calls to Cube.js Backend. It uses scoped slot props technique. +`` is used to build interactive analytics query builders. It abstracts state management and API calls to Cube.js Backend. It uses scoped slot props technique. ### Props -- `query`: default query. +- `query`: query parameters ([learn more about its format](query-format)). This property is reactive - if you change the object here, +the internal query values will be overwritten. This is not two-way. - `cubejsApi`: `CubejsApi` instance to use. Required. - `defaultChartType`: default value of chart type. Default: 'line'. @@ -107,38 +110,39 @@ export default { ##### Slot Props -- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convient interface for data munipulation. +- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convenient interface for data manipulation. #### Empty Slot This slot functions as a empty/loading state in which if the query is loading or empty you can show -something in the meantime +something in the meantime. #### Error Slot +This slot will be rendered if any error happens while the query is loading or rendering. + ##### Slot Props -- `error`: will show the details from error. -- `sqlQuery`: will show tried query +- `error`: the error. +- `sqlQuery`: the attempted query. #### Builder Slot -- `measures`, `dimensions`, `segments`, `timeDimensions`, `filters` - arrays of +- `measures`, `dimensions`, `segments`, `timeDimensions`, `filters` - arrays containing the selected query builder members. - `availableMeasures`, `availableDimensions`, `availableTimeDimensions`, -`availableSegments` - arrays of available to select members. They are loaded via +`availableSegments` - arrays containing available members to select. They are loaded via API from Cube.js Backend. -- `addMeasures`, `addDimensions`, `addSegments`, `addTimeDimensions` - function to control the adding of new members to query builder -- `removeMeasures`, `removeDimensions`, `removeSegments`, `removeTimeDimensions` - function to control the removing of member to query builder -- `setMeasures`, `setDimensions`, `setSegments`, `setTimeDimensions` - function to control the set of members to query builder -- `updateMeasures`, `updateDimensions`, `updateSegments`, `updateTimeDimensions` - function to control the update of member to query builder -- `chartType` - string, containing currently selected chart type. +- `addMeasures`, `addDimensions`, `addSegments`, `addTimeDimensions` - functions to control the adding of new members to query builder. +- `removeMeasures`, `removeDimensions`, `removeSegments`, `removeTimeDimensions` - functions to control the removing of members to query builder. +- `setMeasures`, `setDimensions`, `setSegments`, `setTimeDimensions` - functions to control the setting of members to query builder. +- `updateMeasures`, `updateDimensions`, `updateSegments`, `updateTimeDimensions` - functions to control the updating of members to query builder. +- `chartType` - string containing currently selected chart type. - `updateChartType` - function-setter for chart type. -- `isQueryPresent` - Bool indicating whether is query ready to be displayed or - not. +- `isQueryPresent` - bool indicating whether is query ready to be displayed or not. - `query` - current query, based on selected members. -- `setLimit`, `removeLimit` - functions to control the number of results returned -- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination +- `setLimit`, `removeLimit` - functions to control the number of results returned. +- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination. ### Example [Open in CodeSandbox](https://codesandbox.io/s/3rlxjkv2p) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index 193cd84dafa23..b7ba770350001 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -29,6 +29,8 @@ export default { availableSegments: [], limit: null, offset: null, + renewQuery: false, + order: {} }; data.granularities = [ @@ -41,35 +43,7 @@ export default { return data; }, - async mounted() { - this.meta = await this.cubejsApi.meta(); - const { measures, dimensions, segments, timeDimensions, filters, limit, offset } = this.query; - - this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') })); - this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') })); - this.segments = (segments || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'segments') })); - this.timeDimensions = (timeDimensions || []).map((m, i) => ({ - ...m, - dimension: { ...this.meta.resolveMember(m.dimension, 'dimensions'), granularities: this.granularities }, - index: i - })); - this.filters = (filters || []).map((m, i) => ({ - ...m, - // using 'dimension' is deprecated, 'member' should be specified instead - member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']), - operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']), - index: i - })); - - this.availableMeasures = this.meta.membersForQuery({}, 'measures') || []; - this.availableDimensions = this.meta.membersForQuery({}, 'dimensions') || []; - this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || []) - .filter(m => m.type === 'time'); - this.availableSegments = this.meta.membersForQuery({}, 'segments') || []; - this.limit = (limit || null); - this.offset = (offset || null); - }, render(createElement) { const { chartType, @@ -93,6 +67,8 @@ export default { removeLimit, setOffset, removeOffset, + renewQuery, + order } = this; let builderProps = {}; @@ -119,6 +95,8 @@ export default { removeLimit, setOffset, removeOffset, + renewQuery, + order }; QUERY_ELEMENTS.forEach((e) => { @@ -164,7 +142,7 @@ export default { validatedQuery() { const validatedQuery = {}; let toQuery = member => member.name; - // TODO: implement order, timezone, renewQuery + // TODO: implement timezone let hasElements = false; QUERY_ELEMENTS.forEach((e) => { @@ -208,13 +186,55 @@ export default { if (this.offset) { validatedQuery.offset = this.offset; } - // add order + + if (this.order) { + validatedQuery.order = this.order; + } + + if (this.renewQuery) { + validatedQuery.renewQuery = this.renewQuery; + } } return validatedQuery; }, }, + + async mounted() { + this.meta = await this.cubejsApi.meta(); + + this.copyQueryFromProps(); + }, + methods: { + copyQueryFromProps() { + const { measures, dimensions, segments, timeDimensions, filters, limit, offset, renewQuery, order } = this.query; + + this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') })); + this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') })); + this.segments = (segments || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'segments') })); + this.timeDimensions = (timeDimensions || []).map((m, i) => ({ + ...m, + dimension: { ...this.meta.resolveMember(m.dimension, 'dimensions'), granularities: this.granularities }, + index: i + })); + this.filters = (filters || []).map((m, i) => ({ + ...m, + member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']), + operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']), + index: i + })); + + this.availableMeasures = this.meta.membersForQuery({}, 'measures') || []; + this.availableDimensions = this.meta.membersForQuery({}, 'dimensions') || []; + this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || []) + .filter(m => m.type === 'time'); + this.availableSegments = this.meta.membersForQuery({}, 'segments') || []; + this.limit = (limit || null); + this.offset = (offset || null); + this.renewQuery = (renewQuery || false); + this.order = (order || {}); + }, addMember(element, member) { const name = element.charAt(0).toUpperCase() + element.slice(1); let mem; @@ -364,4 +384,17 @@ export default { this.chartType = chartType; }, }, + + watch: { + query() { + if (!this.meta) { + // this is ok as if meta has not been loaded by the time query prop has changed, + // then the promise for loading meta (found in mounted()) will call + // copyQueryFromProps and will therefore update anyway. + return; + } + + this.copyQueryFromProps(); + } + } }; \ No newline at end of file diff --git a/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js b/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js index a741cca153ffb..a7e95140a5615 100644 --- a/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js +++ b/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js @@ -492,5 +492,107 @@ describe('QueryBuilder.vue', () => { expect(wrapper.vm.offset).toBe(10); }); + + it('sets renewQuery', async () => { + const cube = CubejsApi('token'); + jest.spyOn(cube, 'request') + .mockImplementation(fetchMock(load)) + .mockImplementationOnce(fetchMock(meta)); + + const filter = { + member: 'Orders.status', + operator: 'equals', + values: ['invalid'], + }; + + const wrapper = mount(QueryBuilder, { + propsData: { + cubejsApi: cube, + query: { + filters: [filter], + renewQuery: true + }, + }, + }); + + await flushPromises(); + + expect(wrapper.vm.renewQuery).toBe(true); + }); + + it('sets order', async () => { + const cube = CubejsApi('token'); + jest.spyOn(cube, 'request') + .mockImplementation(fetchMock(load)) + .mockImplementationOnce(fetchMock(meta)); + + const filter = { + member: 'Orders.status', + operator: 'equals', + values: ['invalid'], + }; + + const wrapper = mount(QueryBuilder, { + propsData: { + cubejsApi: cube, + query: { + filters: [filter], + order: { + 'Orders.status': 'desc' + } + }, + }, + }); + + await flushPromises(); + + expect(wrapper.vm.order['Orders.status']).toBe('desc'); + }); + + it('is reactive when filter is changed', async () => { + const cube = CubejsApi('token'); + jest.spyOn(cube, 'request') + .mockImplementation(fetchMock(load)) + .mockImplementationOnce(fetchMock(meta)); + + const filter = { + member: 'Orders.status', + operator: 'equals', + values: ['invalid'], + }; + + const newFilter = { + dimension: 'Orders.number', + operator: 'equals', + values: ['1'], + }; + + const wrapper = mount(QueryBuilder, { + propsData: { + cubejsApi: cube, + query: { + filters: [filter] + }, + }, + }); + + await flushPromises(); + + expect(wrapper.vm.filters.length).toBe(1); + expect(wrapper.vm.filters[0].member.name).toBe('Orders.status'); + expect(wrapper.vm.filters[0].values).toContain('invalid'); + + wrapper.setProps({ + query: { + filters: [newFilter] + } + }); + + await flushPromises(); + + expect(wrapper.vm.filters.length).toBe(1); + expect(wrapper.vm.filters[0].member.name).toBe('Orders.number'); + expect(wrapper.vm.filters[0].values).toContain('1'); + }); }); });