From 95d8801b72bcc8397562139ec3d05fe348561984 Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Wed, 9 Oct 2019 16:43:55 +0100 Subject: [PATCH 1/6] feat(vue): made vue QueryBuilder reactive --- packages/cubejs-vue/src/QueryBuilder.js | 74 ++++++++++++++----------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index 193cd84dafa23..638ac38f1944d 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -28,7 +28,7 @@ export default { availableTimeDimensions: [], availableSegments: [], limit: null, - offset: null, + offset: null }; data.granularities = [ @@ -41,35 +41,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, @@ -92,7 +64,7 @@ export default { setLimit, removeLimit, setOffset, - removeOffset, + removeOffset } = this; let builderProps = {}; @@ -118,7 +90,7 @@ export default { setLimit, removeLimit, setOffset, - removeOffset, + removeOffset }; QUERY_ELEMENTS.forEach((e) => { @@ -208,13 +180,40 @@ export default { if (this.offset) { validatedQuery.offset = this.offset; } - // add order } return validatedQuery; }, }, methods: { + async setQueryValues() { + this.meta = await this.cubejsApi.meta(); + + 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, ['dimensions', 'measures']), + operators: this.meta.filterOperatorsForMember(m.member, ['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); + }, addMember(element, member) { const name = element.charAt(0).toUpperCase() + element.slice(1); let mem; @@ -364,4 +363,13 @@ export default { this.chartType = chartType; }, }, + + watch: { + query: { + handler: async function () { + await this.setQueryValues(); + }, + immediate: true + } + } }; \ No newline at end of file From 1d490ddace2360910bf09710f2baf180e3c82c96 Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Wed, 9 Oct 2019 16:47:54 +0100 Subject: [PATCH 2/6] feat(vue): Added renewQuery support to QueryBuilder --- packages/cubejs-vue/src/QueryBuilder.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index 638ac38f1944d..9054ea190eaa1 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -28,7 +28,8 @@ export default { availableTimeDimensions: [], availableSegments: [], limit: null, - offset: null + offset: null, + renewQuery: false }; data.granularities = [ @@ -64,7 +65,8 @@ export default { setLimit, removeLimit, setOffset, - removeOffset + removeOffset, + renewQuery } = this; let builderProps = {}; @@ -90,7 +92,8 @@ export default { setLimit, removeLimit, setOffset, - removeOffset + removeOffset, + renewQuery }; QUERY_ELEMENTS.forEach((e) => { @@ -136,7 +139,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) => { @@ -180,6 +183,10 @@ export default { if (this.offset) { validatedQuery.offset = this.offset; } + + if (this.renewQuery) { + validatedQuery.renewQuery = this.renewQuery; + } } return validatedQuery; @@ -213,6 +220,7 @@ export default { this.availableSegments = this.meta.membersForQuery({}, 'segments') || []; this.limit = (limit || null); this.offset = (offset || null); + this.renewQuery = (renewQuery || false); }, addMember(element, member) { const name = element.charAt(0).toUpperCase() + element.slice(1); From 7e9999678b0e3686c298543ba367049d2c6b9dc1 Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Wed, 9 Oct 2019 16:48:51 +0100 Subject: [PATCH 3/6] feat(vue): Added order support to QueryBuilder --- packages/cubejs-vue/src/QueryBuilder.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index 9054ea190eaa1..31f3298a17f0a 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -29,7 +29,8 @@ export default { availableSegments: [], limit: null, offset: null, - renewQuery: false + renewQuery: false, + order: {} }; data.granularities = [ @@ -66,7 +67,8 @@ export default { removeLimit, setOffset, removeOffset, - renewQuery + renewQuery, + order } = this; let builderProps = {}; @@ -93,7 +95,8 @@ export default { removeLimit, setOffset, removeOffset, - renewQuery + renewQuery, + order }; QUERY_ELEMENTS.forEach((e) => { @@ -184,6 +187,10 @@ export default { validatedQuery.offset = this.offset; } + if (this.order) { + validatedQuery.order = this.order; + } + if (this.renewQuery) { validatedQuery.renewQuery = this.renewQuery; } @@ -221,6 +228,7 @@ export default { 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); From cc3634f26475f93dddf4a8053782b7bb19db8a4d Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Wed, 9 Oct 2019 18:52:57 +0100 Subject: [PATCH 4/6] feat(vue): Added tests for new functionality --- packages/cubejs-vue/src/QueryBuilder.js | 8 +- .../tests/unit/QueryBuilder.spec.js | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index 31f3298a17f0a..bad3dc2924020 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -215,8 +215,8 @@ export default { })); this.filters = (filters || []).map((m, i) => ({ ...m, - member: this.meta.resolveMember(m.member, ['dimensions', 'measures']), - operators: this.meta.filterOperatorsForMember(m.member, ['dimensions', 'measures']), + member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']), + operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']), index: i })); @@ -382,8 +382,8 @@ export default { watch: { query: { - handler: async function () { - await this.setQueryValues(); + handler: function () { + return this.setQueryValues(); }, immediate: true } diff --git a/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js b/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js index a741cca153ffb..1b2601ba152b7 100644 --- a/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js +++ b/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js @@ -492,5 +492,111 @@ 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'); + + wrapper.vm.$nextTick(() => { + + }); + }); }); }); From 8b28c6917a5ad96bcf634727cd6bcf756a4afad2 Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Thu, 10 Oct 2019 11:16:09 +0100 Subject: [PATCH 5/6] feat(vue): refactored + fixed test for reactivity --- packages/cubejs-vue/src/QueryBuilder.js | 25 +++++++++++++------ .../tests/unit/QueryBuilder.spec.js | 4 --- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/cubejs-vue/src/QueryBuilder.js b/packages/cubejs-vue/src/QueryBuilder.js index bad3dc2924020..b7ba770350001 100644 --- a/packages/cubejs-vue/src/QueryBuilder.js +++ b/packages/cubejs-vue/src/QueryBuilder.js @@ -199,10 +199,15 @@ export default { return validatedQuery; }, }, - methods: { - async setQueryValues() { - this.meta = await this.cubejsApi.meta(); + 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') })); @@ -381,11 +386,15 @@ export default { }, watch: { - query: { - handler: function () { - return this.setQueryValues(); - }, - immediate: true + 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 1b2601ba152b7..a7e95140a5615 100644 --- a/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js +++ b/packages/cubejs-vue/tests/unit/QueryBuilder.spec.js @@ -593,10 +593,6 @@ describe('QueryBuilder.vue', () => { 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'); - - wrapper.vm.$nextTick(() => { - - }); }); }); }); From 8ba8845ae5f6958b861c19943546abf95d867526 Mon Sep 17 00:00:00 2001 From: Tom Broad Date: Thu, 10 Oct 2019 11:38:56 +0100 Subject: [PATCH 6/6] feat(vue): Updated documentation --- docs/Cube.js-Frontend/@cubejs-client-vue.md | 50 +++++++++++---------- 1 file changed, 27 insertions(+), 23 deletions(-) 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)