Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add order, renewQuery, and reactivity to Vue component #229

Merged
merged 6 commits into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
});