Skip to content

Commit f45468b

Browse files
richipargopaveltiunov
authored andcommitted
feat: add basic vue support (#65)
1 parent 7f5df92 commit f45468b

File tree

11 files changed

+12628
-30
lines changed

11 files changed

+12628
-30
lines changed

packages/cubejs-vue/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.DS_Store
2+
node_modules
3+
/dist
4+
5+
# local env files
6+
.env.local
7+
.env.*.local
8+
9+
# Log files
10+
npm-debug.log*
11+
yarn-debug.log*
12+
yarn-error.log*
13+
14+
# Editor directories and files
15+
.idea
16+
.vscode
17+
*.suo
18+
*.ntvs*
19+
*.njsproj
20+
*.sln
21+
*.sw*

packages/cubejs-vue/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# cubejs-vue
2+
3+
## Project setup
4+
```
5+
yarn install
6+
```
7+
8+
### Compiles and hot-reloads for development
9+
```
10+
yarn run serve
11+
```
12+
13+
### Compiles and minifies for production
14+
```
15+
yarn run build
16+
```
17+
18+
### Run your tests
19+
```
20+
yarn run test
21+
```
22+
23+
### Lints and fixes files
24+
```
25+
yarn run lint
26+
```
27+
28+
### Customize configuration
29+
See [Configuration Reference](https://cli.vuejs.org/config/).

packages/cubejs-vue/babel.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
presets: [
3+
'@vue/app'
4+
]
5+
}

packages/cubejs-vue/package.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"name": "@cubejs-client/vue",
3+
"version": "0.1.0",
4+
"description": "Vue.js components for cube.js",
5+
"author": "Ricardo Tapia",
6+
"license": "MIT",
7+
"private": true,
8+
"scripts": {
9+
"serve": "vue-cli-service serve",
10+
"build": "vue-cli-service build",
11+
"lint": "vue-cli-service lint"
12+
},
13+
"dependencies": {
14+
"core-js": "^2.6.5",
15+
"ramda": "^0.26.1",
16+
"vue": "^2.6.6"
17+
},
18+
"devDependencies": {
19+
"@vue/cli-plugin-babel": "^3.5.0",
20+
"@vue/cli-plugin-eslint": "^3.5.0",
21+
"@vue/cli-service": "^3.5.0",
22+
"babel-eslint": "^10.0.1",
23+
"eslint": "^5.8.0",
24+
"eslint-plugin-vue": "^5.0.0",
25+
"vue-template-compiler": "^2.5.21"
26+
},
27+
"eslintConfig": {
28+
"root": true,
29+
"env": {
30+
"node": true
31+
},
32+
"extends": [
33+
"plugin:vue/essential",
34+
"eslint:recommended"
35+
],
36+
"rules": {},
37+
"parserOptions": {
38+
"parser": "babel-eslint"
39+
}
40+
},
41+
"postcss": {
42+
"plugins": {
43+
"autoprefixer": {}
44+
}
45+
},
46+
"main": "dist/cubejs-vue.js",
47+
"module": "dist/cubejs-vue.esm.js",
48+
"files": [
49+
"dist",
50+
"src"
51+
],
52+
"browserslist": [
53+
"> 1%",
54+
"last 2 versions",
55+
"not ie <= 8"
56+
]
57+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Vue from 'vue';
2+
import QueryRenderer from './QueryRenderer';
3+
4+
export default Vue.component('QueryBuilder', {
5+
components: {
6+
QueryRenderer,
7+
},
8+
props: {
9+
query: {
10+
type: Object,
11+
},
12+
cubejsApi: {
13+
type: Object,
14+
required: true,
15+
},
16+
},
17+
async mounted() {
18+
this.meta = await this.cubejsApi.meta();
19+
},
20+
render(createElement) {
21+
const { cubejsApi } = this;
22+
23+
return createElement(QueryRenderer, {
24+
props: {
25+
query: this.validatedQuery(),
26+
cubejsApi,
27+
},
28+
// TODO: check passable props
29+
}, this.$scopedSlots.default(this.prepareRenderProps()));
30+
},
31+
methods: {
32+
isQueryPresent() {
33+
const { query } = this;
34+
35+
return query.measures && query.measures.length ||
36+
query.dimensions && query.dimensions.length ||
37+
query.timeDimensions && query.timeDimensions.length;
38+
},
39+
validatedQuery() {
40+
const { query } = this;
41+
42+
return {
43+
...query,
44+
filters: (query.filters || []).filter(f => f.operator),
45+
};
46+
},
47+
prepareRenderProps(queryRendererProps) {
48+
const getName = member => member.name;
49+
const toTimeDimension = member => ({
50+
dimension: member.dimension.name,
51+
granularity: member.granularity,
52+
dateRange: member.dateRange,
53+
});
54+
const toFilter = member => ({
55+
dimension: member.dimension.name,
56+
operator: member.operator,
57+
values: member.values,
58+
});
59+
60+
const updateMethods = (memberType, toQuery = getName) => ({
61+
add(member) {
62+
this.query = {
63+
...this.query,
64+
[memberType]: (this.query[memberType] || []).concat(toQuery(member)),
65+
};
66+
},
67+
remove(member) {
68+
const members = (this.query[memberType] || []).concat([]);
69+
members.splice(member.index, 1);
70+
71+
// TODO: check return state
72+
this.query = {
73+
...this.query,
74+
[memberType]: members,
75+
};
76+
},
77+
update(member, updateWith) {
78+
const members = (this.query[memberType] || []).concat([]);
79+
members.splice(member.index, 1, toQuery(updateWith));
80+
81+
// TODO: check return state
82+
this.query = {
83+
...this.query,
84+
[memberType]: members,
85+
};
86+
},
87+
});
88+
89+
const granularities = [
90+
{ name: 'hour', title: 'Hour' },
91+
{ name: 'day', title: 'Day' },
92+
{ name: 'week', title: 'Week' },
93+
{ name: 'month', title: 'Month' },
94+
{ name: 'year', title: 'Year' },
95+
];
96+
97+
return {
98+
meta: this.meta,
99+
query: this.query,
100+
validatedQuery: this.validatedQuery(),
101+
isQueryPresent: this.isQueryPresent(),
102+
chartType: this.chartType,
103+
measures: (this.meta && this.query.measures || [])
104+
.map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') })),
105+
dimensions: (this.meta && this.query.dimensions || [])
106+
.map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') })),
107+
segments: (this.meta && this.query.segments || [])
108+
.map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'segments') })),
109+
timeDimensions: (this.meta && this.query.timeDimensions || [])
110+
.map((m, i) => ({
111+
...m,
112+
dimension: { ...this.meta.resolveMember(m.dimension, 'dimensions'), granularities },
113+
index: i
114+
})),
115+
filters: (this.meta && this.query.filters || [])
116+
.map((m, i) => ({
117+
...m,
118+
dimension: this.meta.resolveMember(m.dimension, ['dimensions', 'measures']),
119+
operators: this.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']),
120+
index: i
121+
})),
122+
availableMeasures: this.meta && this.meta.membersForQuery(this.query, 'measures') || [],
123+
availableDimensions: this.meta && this.meta.membersForQuery(this.query, 'dimensions') || [],
124+
availableTimeDimensions: (
125+
this.meta && this.meta.membersForQuery(this.query, 'dimensions') || []
126+
).filter(m => m.type === 'time'),
127+
availableSegments: this.meta && this.meta.membersForQuery(this.query, 'segments') || [],
128+
129+
updateMeasures: updateMethods('measures'),
130+
updateDimensions: updateMethods('dimensions'),
131+
updateSegments: updateMethods('segments'),
132+
updateTimeDimensions: updateMethods('timeDimensions', toTimeDimension),
133+
updateFilters: updateMethods('filters', toFilter),
134+
updateChartType: (chartType) => { this.chartType = chartType; },
135+
...queryRendererProps,
136+
};
137+
},
138+
},
139+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Vue from 'vue';
2+
import { toPairs, fromPairs } from 'ramda';
3+
4+
export default Vue.component('QueryRenderer', {
5+
props: {
6+
query: {
7+
type: Object,
8+
default: () => ({}),
9+
},
10+
queries: {
11+
type: Object,
12+
},
13+
cubejsApi: {
14+
type: Object,
15+
required: true,
16+
},
17+
},
18+
data() {
19+
return {
20+
mutexObj: {},
21+
error: undefined,
22+
resultSet: undefined,
23+
loadingState: false,
24+
sqlQuery: undefined,
25+
};
26+
},
27+
async mounted() {
28+
const { query, queries } = this;
29+
30+
if (query) {
31+
await this.load(query);
32+
}
33+
34+
if (queries) {
35+
await this.loadQueries(queries);
36+
}
37+
},
38+
// TODO: handle update
39+
render(createElement) {
40+
const { resultSet, error, loading, sqlQuery } = this;
41+
42+
if (this.$slots.default) {
43+
return createElement(
44+
'div',
45+
this.$scopedSlots.default({
46+
resultSet: this.queries ? (resultSet || {}) : resultSet,
47+
error,
48+
loadingState: { isLoading: loading, },
49+
sqlQuery,
50+
}),
51+
);
52+
} else {
53+
return null;
54+
}
55+
},
56+
methods: {
57+
async load(query) {
58+
try {
59+
this.loading = true;
60+
61+
if (query && Object.keys(query).length) {
62+
if (this.loadSql === 'only') {
63+
this.sqlQuery = await this.cubejsApi.sql(query, { mutexObj: this.mutexObj, mutexKey: 'sql' });
64+
} else if (this.loadSql) {
65+
this.sqlQuery = await this.cubejsApi.sql(query, { mutexObj: this.mutexObj, mutexKey: 'sql' });
66+
this.resultSet = await this.cubejsApi.load(query, { mutexObj: this.mutexObj, mutexKey: 'query' });
67+
} else {
68+
this.resultSet = await this.cubejsApi.load(query, { mutexObj: this.mutexObj, mutexKey: 'query' });
69+
}
70+
}
71+
72+
this.loading = false;
73+
} catch (exc) {
74+
this.error = exc;
75+
this.resultSet = undefined;
76+
this.loading = false;
77+
}
78+
},
79+
async loadQueries(queries) {
80+
try {
81+
this.loading = true;
82+
83+
const resultPromises = Promise.all(toPairs(queries).map(
84+
([name, query]) =>
85+
this.cubejsApi.load(query, { mutexObj: this.mutexObj, mutexKey: name }).then(r => [name, r])
86+
));
87+
88+
this.resultSet = fromPairs(await resultPromises);
89+
this.loading = false;
90+
} catch (exc) {
91+
this.error = exc;
92+
this.loading = false;
93+
}
94+
},
95+
},
96+
});

packages/cubejs-vue/src/QueryRendererWithTotals.vue

Whitespace-only changes.

packages/cubejs-vue/src/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import QueryRenderer from './QueryRenderer';
2+
// import QueryRendererWithTotals from './QueryRendererWithTotals.vue';
3+
import QueryBuilder from './QueryBuilder';
4+
5+
export { QueryRenderer, QueryBuilder };
6+
7+
export default {};

0 commit comments

Comments
 (0)