Skip to content

Commit

Permalink
feat(vue): Add order, renewQuery, and reactivity to Vue component (#229
Browse files Browse the repository at this point in the history
…). Thanks to @TCBroad

* feat(vue): made vue QueryBuilder reactive

* feat(vue): Added renewQuery support to QueryBuilder

* feat(vue): Added order support to QueryBuilder

* feat(vue): Added tests for new functionality

* feat(vue): refactored + fixed test for reactivity

* feat(vue): Updated documentation
  • Loading branch information
TCBroad authored and paveltiunov committed Oct 11, 2019
1 parent 466a849 commit 9293f13
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 53 deletions.
50 changes: 27 additions & 23 deletions docs/Cube.js-Frontend/@cubejs-client-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -93,11 +95,12 @@ export default {
```

## QueryBuilder
`<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.
`<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.

### 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'.

Expand All @@ -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)
Expand Down
93 changes: 63 additions & 30 deletions packages/cubejs-vue/src/QueryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export default {
availableSegments: [],
limit: null,
offset: null,
renewQuery: false,
order: {}
};

data.granularities = [
Expand All @@ -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,
Expand All @@ -93,6 +67,8 @@ export default {
removeLimit,
setOffset,
removeOffset,
renewQuery,
order
} = this;

let builderProps = {};
Expand All @@ -119,6 +95,8 @@ export default {
removeLimit,
setOffset,
removeOffset,
renewQuery,
order
};

QUERY_ELEMENTS.forEach((e) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
};
102 changes: 102 additions & 0 deletions packages/cubejs-vue/tests/unit/QueryBuilder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});

0 comments on commit 9293f13

Please sign in to comment.