From 9ebd2899892552857e5a18555822ea260f75f64d Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Fri, 5 May 2017 18:03:15 -0700 Subject: [PATCH 1/9] Trying out charting with highcharts and chartjs --- app/components/records-charter.js | 157 ++++++++++++++++++ app/components/records-viewer.js | 28 +++- app/models/query.js | 4 - app/models/result.js | 25 ++- app/routes/query.js | 9 + app/styles/components/records-charter.scss | 5 + app/styles/components/records-viewer.scss | 53 ++++-- app/styles/result.scss | 1 - app/templates/components/query-blurb.hbs | 2 +- app/templates/components/records-charter.hbs | 6 + app/templates/components/records-viewer.hbs | 33 ++-- app/templates/result.hbs | 18 +- bower.json | 17 +- package.json | 1 + .../components/records-charter-test.js | 25 +++ 15 files changed, 328 insertions(+), 56 deletions(-) create mode 100644 app/components/records-charter.js create mode 100644 app/styles/components/records-charter.scss create mode 100644 app/templates/components/records-charter.hbs create mode 100644 tests/integration/components/records-charter-test.js diff --git a/app/components/records-charter.js b/app/components/records-charter.js new file mode 100644 index 00000000..b2ee82d5 --- /dev/null +++ b/app/components/records-charter.js @@ -0,0 +1,157 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Ember from 'ember'; + +export default Ember.Component.extend({ + classNames: ['records-charter'], + model: null, + metadata: null, + columns: null, + rows: null, + graphType: 'bar', + + sampleRow: Ember.computed('rows', 'columns', function() { + let typicalRow = { }; + let rows = this.get('rows'); + let c = 0; + this.get('columns').forEach(column => { + for (let row of rows) { + c++; + let value = row[column]; + if (!Ember.isEmpty(value)) { + typicalRow[column] = value; + break; + } + } + }); + console.log(c); + console.log(typicalRow); + return typicalRow; + }), + + independentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { + let { columns, sampleRow, isDistribution } = this.getProperties('columns', 'sampleRow', 'model.isDistribution'); + if (isDistribution) { + return Ember.A(columns.filter(c => this.isAny(c, 'Quantile', 'Range'))); + } + // Pick all string columns + return Ember.A(columns.filter(c => Ember.typeOf(sampleRow[c]) === 'string')); + }), + + dependentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { + let { columns, sampleRow, isDistribution } = this.getProperties('columns', 'sampleRow', 'model.isDistribution'); + if (isDistribution) { + return Ember.A(columns.filter(c => this.isAny(c, 'Count', 'Value', 'Probability'))); + } + // Pick all number columns + return Ember.A(columns.filter(c => Ember.typeOf(sampleRow[c]) === 'number')); + }), + + options: Ember.computed('dependentColumns', function() { + let numberOfColumns = this.get('dependentColumns.length'); + if (numberOfColumns === 1) { + return { }; + } + // Everything else, 2 axes + return { + scales: { + yAxes: [{ + position: 'left', + id: '0' + }, { + position: 'right', + id: '1' + }] + } + }; + }), + + labels: Ember.computed('independentColumns', 'rows', function() { + // Only one independent column for now + let rows = this.get('rows'); + // [ [field1 values...], [field2 values...], ...] + let valuesList = this.get('independentColumns').map(field => this.getFieldValues(field, rows)); + return this.zip(valuesList); + }), + + datasets: Ember.computed('graphType', 'dependentColumns', 'rows', function() { + let dependentColumns = this.get('dependentColumns'); + let rows = this.get('rows'); + let graphType = this.get('graphType'); + return dependentColumns.map((c, i) => this.dataset(graphType, c, rows, i)); + }), + + data: Ember.computed('labels', 'datasets', function() { + return { + labels: this.get('labels'), + datasets: this.get('datasets') + }; + }), + + dataset(graphType, column, rows, index) { + let values = this.getFieldValues(column, rows); + let dataset = { + label: column, + data: values, + backgroundColor: this.randomColors(values.length) + }; + // Add yAxisID only if we have more than one dataset. More than 2 => Add the first y-axis + if (index === 1) { + dataset.yAxisID = '1'; + } else if (index > 1) { + dataset.yAxisID = '0'; + } + return dataset; + }, + + randomUpto(size) { + return Math.floor(Math.random() * size); + }, + + randomColor() { + let red = this.randomUpto(255); + let green = this.randomUpto(255); + let blue = this.randomUpto(255); + return `rgb(${red},${green},${blue})`; + }, + + randomColors(size) { + let colors = []; + for (let i = 0; i < size; ++i) { + colors.push(this.randomColor()); + } + return colors; + }, + + isType(row, field, type) { + return Ember.isEqual(Ember.typeOf(row[field]), type); + }, + + isAny(field, ...values) { + for (let value of values) { + if (Ember.isEqual(field, value)) { + return true; + } + } + return false; + }, + + zip(arrayOfArrays, delimiter = '/') { + if (Ember.isEmpty(arrayOfArrays)) { + return []; + } + let zipped = arrayOfArrays[0].map((_, i) => arrayOfArrays.map(a => a[i])); + return zipped.map(a => a.reduce((p, c) => `${p}${delimiter}${c}`), ''); + }, + + getFieldValues(field, rows, round = false) { + let values = rows.map(row => row[field]); + if (round) { + values = values.map(v => v.toFixed(4)); + } + return values; + } +}); diff --git a/app/components/records-viewer.js b/app/components/records-viewer.js index 29eff229..0bb4bb39 100644 --- a/app/components/records-viewer.js +++ b/app/components/records-viewer.js @@ -8,10 +8,19 @@ import Ember from 'ember'; export default Ember.Component.extend({ fileSaver: Ember.inject.service(), classNames: ['records-viewer'], + showRawData: false, showTable: false, + showChart: false, + // Copy of the model + model: null, + metadata: null, records: null, fileName: 'results', + enableCharting: Ember.computed('model', function() { + return this.get('model.isChartable'); + }), + columns: Ember.computed('records', function() { return Ember.A(this.extractUniqueColumns(this.get('records'))); }).readOnly(), @@ -98,13 +107,24 @@ export default Ember.Component.extend({ return flattened; }, + flipTo(field) { + this.set('showRawData', false); + this.set('showTable', false); + this.set('showChart', false); + this.set(field, true); + }, + actions: { - showRawData() { - this.set('showTable', false); + rawDataMode() { + this.flipTo('showRawData'); + }, + + tableMode() { + this.flipTo('showTable'); }, - showTable() { - this.set('showTable', true); + chartMode() { + this.flipTo('showChart'); }, downloadAsJSON() { diff --git a/app/models/query.js b/app/models/query.js index 6d5f3125..bc189882 100644 --- a/app/models/query.js +++ b/app/models/query.js @@ -126,10 +126,6 @@ export default DS.Model.extend(Validations, { return max; }).readOnly(), - isRawWithoutFields: Ember.computed('projections.[]', 'aggregation.type', function() { - return this.get('aggregation.type') === AGGREGATIONS.get('RAW') && Ember.isEmpty(this.get('projections')); - }), - summarizeFieldLike(fieldLike) { return Ember.isEmpty(fieldLike) ? '' : fieldLike.getEach('name').reject((n) => Ember.isEmpty(n)).join(', '); } diff --git a/app/models/result.js b/app/models/result.js index 754c08a0..eb21c0b8 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -3,8 +3,9 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -import DS from 'ember-data'; import Ember from 'ember'; +import DS from 'ember-data'; +import { AGGREGATIONS } from 'bullet-ui/models/aggregation'; export default DS.Model.extend({ metadata: DS.attr({ @@ -22,5 +23,25 @@ export default DS.Model.extend({ return new Date(Date.now()); } }), - query: DS.belongsTo('query', { autoSave: true }) + query: DS.belongsTo('query', { autoSave: true }), + querySnapshot: DS.attr(), + + isReallyRaw: Ember.computed('querySnapshot', function() { + return this.get('querySnapshot.type') === AGGREGATIONS.get('RAW') && this.get('querySnapshot.projectionSize') === 0; + }), + + isDistribution: Ember.computed('querySnapshot', function() { + return this.get('querySnapshot.type') === AGGREGATIONS.get('DISTRIBUTION'); + }), + + isTopK: Ember.computed('querySnapshot', function() { + return this.get('querySnapshot.type') === AGGREGATIONS.get('TOP_K'); + }), + + + isGroupBy: Ember.computed('querySnapshot', function() { + return this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; + }), + + isChartable: Ember.computed.or('isDistribution', 'isGroupBy', 'isTopK') }); diff --git a/app/routes/query.js b/app/routes/query.js index 3e0efde1..25b65f76 100644 --- a/app/routes/query.js +++ b/app/routes/query.js @@ -7,6 +7,7 @@ import Ember from 'ember'; export default Ember.Route.extend({ querier: Ember.inject.service(), + queryManager: Ember.inject.service(), resultHandler(data, context) { context.set('pendingRequest', null); @@ -14,6 +15,14 @@ export default Ember.Route.extend({ let result = context.store.createRecord('result', { metadata: data.meta, records: data.records, + querySnapshot: { + type: query.get('aggregation.type'), + groupsSize: query.get('aggregation.groups.length'), + metricsSize: query.get('aggregation.metrics.length'), + projectionsSize: query.get('projections.length'), + fieldsSummary: query.get('fieldsSummary'), + filterSummary: query.get('filterSummary') + }, query: query }); result.save(); diff --git a/app/styles/components/records-charter.scss b/app/styles/components/records-charter.scss new file mode 100644 index 00000000..56334502 --- /dev/null +++ b/app/styles/components/records-charter.scss @@ -0,0 +1,5 @@ + +.records-charter { + width: 75%; + margin: auto; +} diff --git a/app/styles/components/records-viewer.scss b/app/styles/components/records-viewer.scss index 1876388d..e2c67e66 100644 --- a/app/styles/components/records-viewer.scss +++ b/app/styles/components/records-viewer.scss @@ -29,6 +29,9 @@ } .view-controls { + display: flex; + justify-content: flex-end; + width: 180px; .view-control { margin-left: 15px; border: 2px solid $secondary-button-color; @@ -40,26 +43,46 @@ vertical-align: -1px; } background-color: $white; - width: 30px; - height: 30px; - &:hover { + width: 32px; + height: 32px; + + &:not(.active):hover { border-color: $secondary-button-hover-color; - i { + i, span { color: $secondary-button-hover-color; } } &:focus { outline: none; } + &.active { + // From bootstrap-sass + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.25)); + } } - .raw-view, .table-view { - padding-left: 7px; + .table-view > i { + padding-left: 2px; + } + .raw-view { + display: flex; + justify-content: center; + align-items: center; + font-size: $font-size-small; + vertical-align: 0px; + i:first-of-type { + padding-left: 1px; + } + .slash { + align-self: flex-start; + font-size: $font-size-regular; + color: $secondary-button-color; + } } + .download { padding-right: 6px; } - .download-options { left: -50px; top: 40px; @@ -90,19 +113,19 @@ color: white; border: 9px solid transparent; border-bottom-color: $primary-button-color; - top: -18px; + top: -19px; right: 4px; - z-index: -2; + z-index: -3; .inner { position: relative; width: 0; height: 0; color: $white; - left: -7px; - top: -6px; - border: 7px solid transparent; + left: -9px; + top: -8px; + border: 9px solid transparent; border-bottom-color: $white; - z-index: -1; + z-index: -2; } } } @@ -110,6 +133,8 @@ } } .records-display { + @import "pretty-json"; @import "records-table"; + @import "records-charter"; } -} \ No newline at end of file +} diff --git a/app/styles/result.scss b/app/styles/result.scss index 14f4f85a..ebc455bb 100644 --- a/app/styles/result.scss +++ b/app/styles/result.scss @@ -98,7 +98,6 @@ $information-wrapper-padding-right: 35px; } .result-container { - @import "components/pretty-json"; @import "components/records-viewer"; @import "components/no-results-help"; pre { diff --git a/app/templates/components/query-blurb.hbs b/app/templates/components/query-blurb.hbs index eed42928..f16fa076 100644 --- a/app/templates/components/query-blurb.hbs +++ b/app/templates/components/query-blurb.hbs @@ -8,4 +8,4 @@
Fields: {{query.fieldsSummary}} -
\ No newline at end of file + diff --git a/app/templates/components/records-charter.hbs b/app/templates/components/records-charter.hbs new file mode 100644 index 00000000..b369d5aa --- /dev/null +++ b/app/templates/components/records-charter.hbs @@ -0,0 +1,6 @@ +{{!-- + Copyright 2016, Yahoo Inc. + Licensed under the terms of the Apache License, Version 2.0. + See the LICENSE file associated with the project for terms. + --}} + {{ember-chart type=graphType data=data options=options}} diff --git a/app/templates/components/records-viewer.hbs b/app/templates/components/records-viewer.hbs index 11b57a91..b38e66de 100644 --- a/app/templates/components/records-viewer.hbs +++ b/app/templates/components/records-viewer.hbs @@ -17,17 +17,22 @@
- {{#if showTable}} - - {{else}} - {{/if}} + +
- {{#if showTable}} - {{records-table columnNames=columns rawRows=records}} - {{else}} + {{#if showRawData}} {{pretty-json data=records}} + {{else if showTable}} + {{records-table columnNames=columns rawRows=records}} + {{else if showChart}} + {{records-charter model=model metadata=metadata columns=columns rows=records}} {{/if}} -
\ No newline at end of file +
diff --git a/app/templates/result.hbs b/app/templates/result.hbs index ce07d52b..a32f124f 100644 --- a/app/templates/result.hbs +++ b/app/templates/result.hbs @@ -13,7 +13,7 @@

Query Definition

- {{query-blurb query=model.query}} + {{query-blurb query=model.querySnapshot}}
- {{#if model.records}} - {{#if model.query.isRawWithoutFields}} - {{records-viewer records=model.records showTable=false}} - {{else}} - {{records-viewer records=model.records showTable=true}} - {{/if}} + {{#if model.records}} + {{#if model.isReallyRaw}} + {{records-viewer model=model records=model.records metadata=model.metadata showRawData=true}} {{else}} - {{partial "partials/no-results-help"}} + {{records-viewer model=model records=model.records metadata=model.metadata showTable=true}} + {{/if}} + {{else}} + {{partial "partials/no-results-help"}} {{/if}}
- \ No newline at end of file + diff --git a/bower.json b/bower.json index e51b4082..0ce5b1c7 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,11 @@ { - "name": "bullet-ui", - "dependencies": { - "filesaver": "1.3.2", - "interact": "^1.2.8", - "jQuery-QueryBuilder": "2.4.3", - "jQuery-QueryBuilder-Subfield": "yahoo/jQuery-QueryBuilder-Subfield#v1.0.1", - "jQuery-QueryBuilder-Placeholders": "yahoo/jQuery-QueryBuilder-Placeholders#v1.0.0" - } + "name": "bullet-ui", + "dependencies": { + "filesaver": "1.3.2", + "interact": "^1.2.8", + "jQuery-QueryBuilder": "2.4.3", + "jQuery-QueryBuilder-Subfield": "yahoo/jQuery-QueryBuilder-Subfield#v1.0.1", + "jQuery-QueryBuilder-Placeholders": "yahoo/jQuery-QueryBuilder-Placeholders#v1.0.0", + "chartjs": "2.5.0" + } } diff --git a/package.json b/package.json index 9faa6005..c6ce4200 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "ember-cli-app-version": "^2.0.0", "ember-cli-babel": "^5.1.7", "ember-cli-bootstrap-sassy": "0.5.5", + "ember-cli-chart": "^3.2.0", "ember-cli-code-coverage": "0.3.11", "ember-cli-dependency-checker": "^1.3.0", "ember-cli-htmlbars": "^1.1.1", diff --git a/tests/integration/components/records-charter-test.js b/tests/integration/components/records-charter-test.js new file mode 100644 index 00000000..c6f7756c --- /dev/null +++ b/tests/integration/components/records-charter-test.js @@ -0,0 +1,25 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('records-charter', 'Integration | Component | records charter', { + integration: true +}); + +test('it renders', function(assert) { + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{records-charter}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#records-charter}} + template block text + {{/records-charter}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); From d3ec605871303ea5983e8ecab14360f839d47dca Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Mon, 8 May 2017 17:18:17 -0700 Subject: [PATCH 2/9] Adding migrations instance initializer. Adding pivottable dependency. Fixing QB. Styling output data section --- app/components/pivot-table.js | 4 ++ app/components/query-blurb.js | 2 +- app/components/records-charter.js | 14 ++--- app/components/records-viewer.js | 4 -- app/initializers/startup.js | 21 +------- app/instance-initializers/migrations.js | 40 ++++++++++++++ app/models/result.js | 7 +-- app/routes/query.js | 18 +------ app/services/query-manager.js | 54 +++++++++++++++---- app/styles/components/output-data-input.scss | 37 +++++++------ app/styles/components/query-input.scss | 3 +- app/styles/components/records-viewer.scss | 8 ++- .../components/cells/query-name-entry.hbs | 4 +- .../components/output-data-input.hbs | 19 +++---- app/templates/components/pivot-table.hbs | 1 + app/templates/components/query-blurb.hbs | 4 +- app/templates/components/records-viewer.hbs | 10 ++-- app/templates/result.hbs | 2 +- bower.json | 5 +- config/env-settings.json | 3 ++ ember-cli-build.js | 16 ++++-- .../components/pivot-table-test.js | 25 +++++++++ .../instance-initializers/migrations-test.js | 25 +++++++++ 23 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 app/components/pivot-table.js create mode 100644 app/instance-initializers/migrations.js create mode 100644 app/templates/components/pivot-table.hbs create mode 100644 tests/integration/components/pivot-table-test.js create mode 100644 tests/unit/instance-initializers/migrations-test.js diff --git a/app/components/pivot-table.js b/app/components/pivot-table.js new file mode 100644 index 00000000..926b6130 --- /dev/null +++ b/app/components/pivot-table.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/app/components/query-blurb.js b/app/components/query-blurb.js index d0839ccb..63e8d8b1 100644 --- a/app/components/query-blurb.js +++ b/app/components/query-blurb.js @@ -7,5 +7,5 @@ import Ember from 'ember'; export default Ember.Component.extend({ classNames: ['query-blurb'], - query: null + summary: null }); diff --git a/app/components/records-charter.js b/app/components/records-charter.js index b2ee82d5..c8c23d1c 100644 --- a/app/components/records-charter.js +++ b/app/components/records-charter.js @@ -16,10 +16,8 @@ export default Ember.Component.extend({ sampleRow: Ember.computed('rows', 'columns', function() { let typicalRow = { }; let rows = this.get('rows'); - let c = 0; this.get('columns').forEach(column => { for (let row of rows) { - c++; let value = row[column]; if (!Ember.isEmpty(value)) { typicalRow[column] = value; @@ -27,13 +25,12 @@ export default Ember.Component.extend({ } } }); - console.log(c); - console.log(typicalRow); return typicalRow; }), independentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { - let { columns, sampleRow, isDistribution } = this.getProperties('columns', 'sampleRow', 'model.isDistribution'); + let { columns, sampleRow } = this.getProperties('columns', 'sampleRow'); + let isDistribution = this.get('model.isDistribution'); if (isDistribution) { return Ember.A(columns.filter(c => this.isAny(c, 'Quantile', 'Range'))); } @@ -119,11 +116,8 @@ export default Ember.Component.extend({ }, randomColors(size) { - let colors = []; - for (let i = 0; i < size; ++i) { - colors.push(this.randomColor()); - } - return colors; + let color = this.randomColor(); + return new Array(size).fill(color); }, isType(row, field, type) { diff --git a/app/components/records-viewer.js b/app/components/records-viewer.js index 0bb4bb39..3476e3b8 100644 --- a/app/components/records-viewer.js +++ b/app/components/records-viewer.js @@ -17,10 +17,6 @@ export default Ember.Component.extend({ records: null, fileName: 'results', - enableCharting: Ember.computed('model', function() { - return this.get('model.isChartable'); - }), - columns: Ember.computed('records', function() { return Ember.A(this.extractUniqueColumns(this.get('records'))); }).readOnly(), diff --git a/app/initializers/startup.js b/app/initializers/startup.js index 105ac43e..fd9b1cb5 100644 --- a/app/initializers/startup.js +++ b/app/initializers/startup.js @@ -18,7 +18,7 @@ export default { // Merge into default settings, overriding them let settings = { }; Ember.merge(settings, ENV.APP.SETTINGS); - Ember.merge(settings, decodedSettings); + Ember.$.extend(true, settings, decodedSettings); application.register('settings:main', Ember.Object.create(settings), { instantiate: false }); application.inject('service', 'settings', 'settings:main'); @@ -27,24 +27,5 @@ export default { application.inject('model', 'settings', 'settings:main'); application.inject('controller', 'settings', 'settings:main'); application.inject('component', 'settings', 'settings:main'); - - let version = settings.modelVersion; - this.applyMigrations(version); - localStorage.modelVersion = version; - }, - - /** - * Applies any forced migrations for local storage. Currently, only wipes localStorage - * if version is greater than the stored version or if stored version is not present. - * @param {Number} version A numeric version to compare the current stored version against. - * @return {Boolean} Denoting whether local storage was modified. - */ - applyMigrations(version) { - let currentVersion = localStorage.modelVersion; - if (!currentVersion || version > currentVersion) { - localStorage.clear(); - return true; - } - return false; } }; diff --git a/app/instance-initializers/migrations.js b/app/instance-initializers/migrations.js new file mode 100644 index 00000000..98fedfc7 --- /dev/null +++ b/app/instance-initializers/migrations.js @@ -0,0 +1,40 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ + +import Ember from 'ember'; + +export default { + name: 'migrations', + initialize(application) { + let settings = application.lookup('settings:main'); + let migrations = settings.get('migrations'); + let version = settings.get('modelVersion'); + + let currentVersion = localStorage.modelVersion; + if (!currentVersion || version > currentVersion) { + let manager = application.lookup('service:queryManager'); + this.applyMigrations(manager, migrations); + } + localStorage.modelVersion = version; + }, + + /** + * Applies any forced migrations for local storage. + * @param {Object} manager The query manager. + * @param {Object} migrations An object containing migrations to apply. + */ + applyMigrations(manager, migrations) { + let deletions = migrations.deletions; + // Only support clearing everything or results at the moment + if (Ember.isEqual(deletions, 'result')) { + Ember.Logger.log('Deleting all results'); + manager.deleteAllResults(); + } else if (Ember.isEqual(deletions, 'query')) { + Ember.Logger.log('Deleting all queries'); + localStorage.clear(); + } + } +}; diff --git a/app/models/result.js b/app/models/result.js index eb21c0b8..f825f492 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -27,7 +27,7 @@ export default DS.Model.extend({ querySnapshot: DS.attr(), isReallyRaw: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('RAW') && this.get('querySnapshot.projectionSize') === 0; + return this.get('querySnapshot.type') === AGGREGATIONS.get('RAW') && this.get('querySnapshot.projectionsSize') === 0; }), isDistribution: Ember.computed('querySnapshot', function() { @@ -38,10 +38,11 @@ export default DS.Model.extend({ return this.get('querySnapshot.type') === AGGREGATIONS.get('TOP_K'); }), - isGroupBy: Ember.computed('querySnapshot', function() { return this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; }), - isChartable: Ember.computed.or('isDistribution', 'isGroupBy', 'isTopK') + isChartable: Ember.computed.or('isDistribution', 'isGroupBy', 'isTopK'), + + isPivotable: Ember.computed.not('isChartable') }); diff --git a/app/routes/query.js b/app/routes/query.js index 25b65f76..363e24bd 100644 --- a/app/routes/query.js +++ b/app/routes/query.js @@ -11,23 +11,7 @@ export default Ember.Route.extend({ resultHandler(data, context) { context.set('pendingRequest', null); - context.store.findRecord('query', context.paramsFor('query').query_id).then((query) => { - let result = context.store.createRecord('result', { - metadata: data.meta, - records: data.records, - querySnapshot: { - type: query.get('aggregation.type'), - groupsSize: query.get('aggregation.groups.length'), - metricsSize: query.get('aggregation.metrics.length'), - projectionsSize: query.get('projections.length'), - fieldsSummary: query.get('fieldsSummary'), - filterSummary: query.get('filterSummary') - }, - query: query - }); - result.save(); - query.set('lastRun', result.get('created')); - query.save(); + context.get('queryManager').addResult(context.paramsFor('query').query_id, data).then(result => { context.transitionTo('result', result.get('id')); }); }, diff --git a/app/services/query-manager.js b/app/services/query-manager.js index 75dd2ffb..01cb8cd8 100644 --- a/app/services/query-manager.js +++ b/app/services/query-manager.js @@ -69,6 +69,28 @@ export default Ember.Service.extend({ return childModel.save(); }, + addResult(id, data) { + return this.get('store').findRecord('query', id).then((query) => { + let result = this.get('store').createRecord('result', { + metadata: data.meta, + records: data.records, + querySnapshot: { + type: query.get('aggregation.type'), + groupsSize: query.get('aggregation.groups.length'), + metricsSize: query.get('aggregation.metrics.length'), + projectionsSize: query.get('projections.length'), + fieldsSummary: query.get('fieldsSummary'), + filterSummary: query.get('filterSummary') + }, + query: query + }); + result.save(); + query.set('lastRun', result.get('created')); + query.save(); + return result; + }); + }, + setAggregationAttributes(query, fields) { return query.get('aggregation').then(aggregation => { fields.forEach(field => { @@ -190,17 +212,21 @@ export default Ember.Service.extend({ deleteMultiple(name, model, inverseName) { return model.get(name).then(items => { - items.toArray().forEach(item => { + let promises = items.toArray().map(item => { items.removeObject(item); item.set(inverseName, null); item.destroyRecord(); }); + return Ember.RSVP.all(promises); }); }, deleteProjections(query) { return query.get('projections').then((p) => { - p.forEach(i => i.destroyRecord()); + let promises = p.toArray().map(item => { + item.destroyRecord(); + }) + return Ember.RSVP.all(promises); }); }, @@ -214,15 +240,25 @@ export default Ember.Service.extend({ }, deleteResults(query) { - this.deleteMultiple('results', query, 'query'); - query.save(); + this.deleteMultiple('results', query, 'query').then(() => { + query.save(); + }); }, deleteQuery(query) { - this.deleteSingle('filter', query, 'query'); - this.deleteMultiple('projections', query, 'query'); - this.deleteMultiple('results', query, 'query'); - this.deleteAggregation(query); - query.destroyRecord(); + return Ember.RSVP.all([ + this.deleteSingle('filter', query, 'query'), + this.deleteMultiple('projections', query, 'query'), + this.deleteMultiple('results', query, 'query'), + this.deleteAggregation(query) + ]).then(() => { + query.destroyRecord(); + }) + }, + + deleteAllResults() { + this.get('store').findAll('query').then(queries => { + queries.forEach(q => this.deleteResults(q)); + }); } }); diff --git a/app/styles/components/output-data-input.scss b/app/styles/components/output-data-input.scss index 6fc30cc8..265327df 100644 --- a/app/styles/components/output-data-input.scss +++ b/app/styles/components/output-data-input.scss @@ -1,11 +1,14 @@ .output-data-input { - $box-rectangle-color: #F6F6F6; $border-rectangle-color: #DCDEE2; + $subsection-color: #8C8C8C; @import "info-popover"; - padding-top: 5px; - padding-left: 10px; + padding: 15px; + margin-top: 5px; + margin-bottom: 15px; max-width: 800px; + background-color: $box-rectangle-color; + position: relative; .ember-radio-button { display: block; @@ -14,6 +17,9 @@ font-size: $font-size-regular-small; font-family: $font-family-regular; font-weight: $font-weight-regular; + &.checked { + font-weight: $font-weight-strong; + } input { margin-right: 10px; } @@ -29,18 +35,15 @@ .output-options { display: flex; justify-content: space-between; - .ember-radio-button { - font-size: $font-size-regular; - } } .output-container { @import "validated-input"; - margin-left: $output-option-indentation; - margin-bottom: 20px; .help { - float: right; + position: absolute; + bottom: 10px; + right: 10px; font-size: $font-size-regular-small; font-family: $font-family-regular; font-weight: $font-weight-regular; @@ -70,15 +73,17 @@ } - .subsection-header { - margin-top: 5px; - margin-bottom: 10px; - text-transform: uppercase; - font-weight: $font-weight-medium; - } - .narrow-row { margin-top: 5px; } } + + .subsection-header { + margin: 5px 0; + text-transform: uppercase; + font-size: $font-size-small; + font-weight: $font-weight-medium; + color: $subsection-color; + opacity: 0.9; + } } diff --git a/app/styles/components/query-input.scss b/app/styles/components/query-input.scss index b5527e15..2afd5174 100644 --- a/app/styles/components/query-input.scss +++ b/app/styles/components/query-input.scss @@ -8,6 +8,7 @@ $fill-icon-color: $inactive-grey; $error-icon-color: $error-red; $error-icon-hover-color: #FF9693; + $box-rectangle-color: #F6F6F6; $background-box-color: $background-grey; $background-box-error-color: #FFE1E0; $background-box-hover-color: $background-grey-darker; @@ -173,7 +174,7 @@ @import "validated-input"; margin-top: 10px; - margin-left: 0; + margin-left: 5px; input { height: 45px; } diff --git a/app/styles/components/records-viewer.scss b/app/styles/components/records-viewer.scss index e2c67e66..7b564d7f 100644 --- a/app/styles/components/records-viewer.scss +++ b/app/styles/components/records-viewer.scss @@ -5,9 +5,15 @@ */ .records-viewer { $records-header-color: #00C877; - margin-right: $results-right-space; + // Make unselectable + -moz-user-select: -moz-none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + + .records-title { @import "section-heading-with-help"; diff --git a/app/templates/components/cells/query-name-entry.hbs b/app/templates/components/cells/query-name-entry.hbs index 6a474e34..696ca282 100644 --- a/app/templates/components/cells/query-name-entry.hbs +++ b/app/templates/components/cells/query-name-entry.hbs @@ -7,7 +7,7 @@ {{#if row.name}}

{{row.name}}

{{else}} - {{query-blurb query=row}} + {{query-blurb summary=row}} {{/if}}
@@ -20,4 +20,4 @@ {{#tooltip-on-element}}Delete Query and Historical Results{{/tooltip-on-element}} -
\ No newline at end of file + diff --git a/app/templates/components/output-data-input.hbs b/app/templates/components/output-data-input.hbs index 527b047c..63b0e567 100644 --- a/app/templates/components/output-data-input.hbs +++ b/app/templates/components/output-data-input.hbs @@ -4,6 +4,7 @@ See the LICENSE file associated with the project for terms. --}} +
Select your Output Type
{{#radio-button radioId="raw" value=AGGREGATIONS.RAW groupValue=outputDataType changed=(action "addRawAggregation" false) disabled=disabled}} Raw Data @@ -29,7 +30,7 @@
{{#if isRawAggregation}} -
Choose your Fields
+
Select your Fields
{{#radio-button radioId="all" value=RAWS.ALL groupValue=rawType changed=(action "deleteProjections") disabled=disabled}} Show All Fields @@ -58,7 +59,7 @@ {{#if isGroupAggregation}}
-
Choose Group Fields
+
Select Group Fields
{{#each query.aggregation.groups as |group|}} {{validated-field-selection model=group columns=columns subfieldSuffix=subfieldSuffix subfieldSeparator=subfieldSeparator disabled=disabled forceDirty=forceDirty fieldClasses="col-xs-6" nameClasses="col-xs-5" @@ -73,7 +74,7 @@
-
Choose Metric Fields
+
Select Metric Fields
{{#each query.aggregation.metrics as |metric|}} {{#validated-field-selection model=metric columns=columns subfieldSuffix=subfieldSuffix subfieldSeparator=subfieldSeparator disabled=disabled forceDirty=forceDirty fieldClasses="col-xs-5" nameClasses="col-xs-3" disableField=metric.hasNoField @@ -97,7 +98,7 @@ {{#if isCountDistinctAggregation}}
-
Choose your Fields
+
Select your Fields
{{#each query.aggregation.groups as |field|}} {{validated-field-selection model=field columns=columns subfieldSuffix=subfieldSuffix subfieldSeparator=subfieldSeparator @@ -119,7 +120,7 @@ {{#if isDistributionAggregation}}
-
Choose Type
+
Select Distribution
{{#radio-button radioId="quantile" value=DISTRIBUTIONS.QUANTILE groupValue=distributionType changed=(action "modifyDistributionType" DISTRIBUTIONS.QUANTILE) disabled=disabled}} @@ -135,7 +136,7 @@ {{/radio-button}}
-
Choose a Field
+
Select a Field
{{#each query.aggregation.groups as |field|}} {{validated-field-selection model=field columns=columns subfieldSuffix=subfieldSuffix subfieldSeparator=subfieldSeparator @@ -144,7 +145,7 @@ {{/each}}
-
Choose how your Points are created
+
Select how your Points are created
{{#radio-button radioId="number-points" value=DISTRIBUTION_POINTS.NUMBER groupValue=pointType changed=(action "modifyDistributionPointType" DISTRIBUTION_POINTS.NUMBER) disabled=disabled}} @@ -180,7 +181,7 @@ {{#if isTopKAggregation}}
-
Choose Fields
+
Select Fields
{{#each query.aggregation.groups as |field|}} {{validated-field-selection model=field columns=columns subfieldSuffix=subfieldSuffix subfieldSeparator=subfieldSeparator disabled=disabled @@ -192,7 +193,7 @@ Field
-
Choose the maximum number of results
+
Select the maximum number of results
{{validated-input inputClassNames="col-xs-2" model=query.aggregation valuePath="size" fieldName="K" disabled=isListening}}
diff --git a/app/templates/components/pivot-table.hbs b/app/templates/components/pivot-table.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/app/templates/components/pivot-table.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/components/query-blurb.hbs b/app/templates/components/query-blurb.hbs index f16fa076..3d2cfee2 100644 --- a/app/templates/components/query-blurb.hbs +++ b/app/templates/components/query-blurb.hbs @@ -4,8 +4,8 @@ See the LICENSE file associated with the project for terms. --}}
- Filters: {{query.filterSummary}} + Filters: {{summary.filterSummary}}
- Fields: {{query.fieldsSummary}} + Fields: {{summary.fieldsSummary}}
diff --git a/app/templates/components/records-viewer.hbs b/app/templates/components/records-viewer.hbs index b38e66de..8f1efed0 100644 --- a/app/templates/components/records-viewer.hbs +++ b/app/templates/components/records-viewer.hbs @@ -17,12 +17,10 @@
- {{#if enableCharting}} - - {{/if}} + + +
+ {{/if}} +
+ {{#if pivotMode}} + {{pivot-table rows=rows columns=columns}} + {{else}} + {{ember-chart type=chartType data=data options=options}} + {{/if}} +
diff --git a/app/templates/components/records-viewer.hbs b/app/templates/components/records-viewer.hbs index 8f1efed0..902482ef 100644 --- a/app/templates/components/records-viewer.hbs +++ b/app/templates/components/records-viewer.hbs @@ -17,10 +17,12 @@
- + {{#if enableCharting}} + + {{/if}}
diff --git a/app/templates/partials/-results-help.hbs b/app/templates/partials/-results-help.hbs index 33b15c96..f252304c 100644 --- a/app/templates/partials/-results-help.hbs +++ b/app/templates/partials/-results-help.hbs @@ -4,30 +4,46 @@ See the LICENSE file associated with the project for terms. --}}

- Once Bullet has finished running the query, all the matched records are returned to the browser. The results can be displayed either as raw JSON format or formatted in a tabular view. Toggle between the views by clicking Show Raw Data View or Show Table View icons. + Once Bullet has finished running the query, all the matched records are returned to the browser. The results can be displayed either as raw JSON format, formatted in a tabular view, displayed as a chart or pivoted on. Toggle between the views by clicking Show Raw Data or Show as Table or Show as Chart icons.

Saving and Sharing
-

- The output can be saved locally as JSON, CSV, or a flattened CSV (where any map fields and lists are exploded till the base elements) -

-

- It is not possible to natively share query or result URLs from Bullet since all queries and results are stored on the local browser only. -

+

The output can be saved locally as JSON, CSV, or a flattened CSV (where any map fields and lists are exploded till the base elements)

+

It is not possible to natively share query or result URLs from Bullet since all queries and results are stored on the local browser only.

Raw JSON View
-If you selected the "Show All Fields” option under the Raw Data selection on the “Edit Query” page, or clicked on Show Raw Data View toggle at the top right of the page, the data is displayed in a raw JSON format. +

If you selected the Show All Fields option under the Raw Data selection on the Query page, or clicked on Show Raw Data view toggle at the top right of the page, the data is displayed in a raw JSON format.

Table View
+

If you elected to aggregate data or chose to Select Fields under the Raw Data selection on the query page, or clicked on the Show as Table button, the data is displayed in a table format.

+
    +
  • If a table cell is clicked on, a popup window appears with a JSON representation of the cell data. This is particularly useful for long strings, maps, and other complex data types
  • +
  • Columns width can be adjusted by dragging the vertical column divider at the top of the table
  • +
  • Columns can be rearranged by dragging and dropping them in the desired location
  • +
  • Columns can be also be sorted by clicking on their headers
  • +
+ +
Chart View

- If you elected to Aggregate Data or chose “Select Fields” under the Raw Data selection on the “Edit Query” page, or clicked on the Show as Table at the top right of the page, the data is displayed in a table format. If a table cell is clicked on, a popup window appears with a JSON representation of the cell data. This is particularly useful for long strings, maps, and other complex data types. + Unless you performed a Group All query (a Group query with no Group Fields) or a Count Distinct query, you will see a Show as Chart option. This option graphs your data as a chart with a + guess as to what your independent and dependent fields are. There are two sub-options that appear in this mode. You can see the pre-determined chart as described before by default or + by clicking the Chart sub-option. You can also click the Pivot option to to pivot and aggregate your result data in various forms by using various aggregations and dragging/dropping + various fields to slice and dice.

+ +
Simple Chart

- Columns width can be adjusted by dragging the vertical column divider at the top of the table. + For all queries besides Raw Data queries with Select all Fields, Bullet will try to find your independent fields by finding the string fields in your result and your dependent fields by using the numeric fields in this view. + If there are multiple independent fields, Bullet will separate them using a / in the chart. The chart is a Bar chart.

+ +

If you performed a Raw data query and chose to Show all Data, the Chart sub-option is disabled. You are only able to pivot on the data.

+ +
Pivot

- Columns can be rearranged by dragging and dropping them in the desired location. + If you find that the Chart option is not sufficient and you want aggregate your data some more, you can click the Pivot option. This will launch a UI where you can choose various + ways to display the data (Table by default) and choose various aggregation operations (Count by default). You can also drag and drop fields from a unused region to either a horizontal + or a vertical region to pivot on the data as you would in most spreadsheet applications.

-

- Columns can be also be sorted by clicking on their headers -

\ No newline at end of file + +

You can also click the dropdown next to each field to include or exclude various values for the visualization.

diff --git a/config/env-settings.json b/config/env-settings.json index dfaa7d0b..009d121f 100644 --- a/config/env-settings.json +++ b/config/env-settings.json @@ -12,7 +12,7 @@ } ], "bugLink": "https://github.com/yahoo/bullet-ui/issues", - "modelVersion": 1, + "modelVersion": 2, "migrations": { "deletions": "result" }, diff --git a/ember-cli-build.js b/ember-cli-build.js index 1d7548d9..954f0af4 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -39,14 +39,35 @@ module.exports = function(defaults) { app.import('bower_components/c3/c3.js'); app.import('bower_components/c3/c3.css'); app.import('bower_components/d3/d3.js'); - app.import('bower_components/jquery-ui/jquery-ui.js'); + + // Manually importing jquery-ui dependencies to keep clashes with bootstrap a minimum + // Add all the core dependencies just in case + app.import('bower_components/jquery-ui/ui/version.js'); + app.import('bower_components/jquery-ui/ui/widget.js'); + app.import('bower_components/jquery-ui/ui/position.js'); + app.import('bower_components/jquery-ui/ui/data.js'); + app.import('bower_components/jquery-ui/ui/disable-selection.js'); + app.import('bower_components/jquery-ui/ui/escape-selector.js'); + app.import('bower_components/jquery-ui/ui/focusable.js'); + app.import('bower_components/jquery-ui/ui/form-reset-mixin.js'); + app.import('bower_components/jquery-ui/ui/jquery-1-7.js'); + app.import('bower_components/jquery-ui/ui/keycode.js'); + app.import('bower_components/jquery-ui/ui/labels.js'); + app.import('bower_components/jquery-ui/ui/plugin.js'); + app.import('bower_components/jquery-ui/ui/safe-active-element.js'); + app.import('bower_components/jquery-ui/ui/safe-blur.js'); + app.import('bower_components/jquery-ui/ui/scroll-parent.js'); + app.import('bower_components/jquery-ui/ui/tabbable.js'); + app.import('bower_components/jquery-ui/ui/unique-id.js'); + + app.import('bower_components/jquery-ui/ui/widgets/mouse.js'); + app.import('bower_components/jquery-ui/ui/widgets/draggable.js'); + app.import('bower_components/jquery-ui/ui/widgets/droppable.js'); + app.import('bower_components/jquery-ui/ui/widgets/sortable.js'); + app.import('bower_components/pivottable/dist/pivot.js'); app.import('bower_components/pivottable/dist/pivot.css'); app.import('bower_components/pivottable/dist/c3_renderers.js'); - app.import('bower_components/pivottable/dist/export_renderers.js'); - - // Must load after to replace jquery-ui tooltips with bootstrap - app.import('bower_components/bootstrap/dist/js/bootstrap.js'); return app.toTree(); }; From 8e65a66b52519879ad48a4927b52ec63098993cb Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Tue, 9 May 2017 19:45:28 -0700 Subject: [PATCH 4/9] Reducing jquery-ui deps --- app/models/result.js | 8 ++++---- config/env-settings.json | 2 +- ember-cli-build.js | 22 ++++------------------ 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/app/models/result.js b/app/models/result.js index 74b28547..b97667d5 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -42,6 +42,10 @@ export default DS.Model.extend({ return this.get('querySnapshot.type') === AGGREGATIONS.get('GROUP') && this.get('querySnapshot.groupsSize') === 0; }), + isGroupBy: Ember.computed('querySnapshot', function() { + return this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; + }), + isDistribution: Ember.computed('querySnapshot', function() { return this.get('querySnapshot.type') === AGGREGATIONS.get('DISTRIBUTION'); }), @@ -50,9 +54,5 @@ export default DS.Model.extend({ return this.get('querySnapshot.type') === AGGREGATIONS.get('TOP_K'); }), - isGroupBy: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; - }), - isSingleRow: Ember.computed.or('isCountDistinct', 'isGroupAll') }); diff --git a/config/env-settings.json b/config/env-settings.json index 009d121f..75cfc00c 100644 --- a/config/env-settings.json +++ b/config/env-settings.json @@ -14,7 +14,7 @@ "bugLink": "https://github.com/yahoo/bullet-ui/issues", "modelVersion": 2, "migrations": { - "deletions": "result" + "deletions": "" }, "defaultValues": { "aggregationMaxSize": 512, diff --git a/ember-cli-build.js b/ember-cli-build.js index 954f0af4..86b6091b 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -33,38 +33,24 @@ module.exports = function(defaults) { app.import('bower_components/jQuery-QueryBuilder-Subfield/query-builder-subfield.css'); app.import('bower_components/jQuery-QueryBuilder-Placeholders/query-builder-placeholders.js'); + // FileSaver app.import('bower_components/filesaver/FileSaver.js'); // pivottable app.import('bower_components/c3/c3.js'); app.import('bower_components/c3/c3.css'); app.import('bower_components/d3/d3.js'); - // Manually importing jquery-ui dependencies to keep clashes with bootstrap a minimum - // Add all the core dependencies just in case + // Just need the sortable plugin and all of its dependency chain app.import('bower_components/jquery-ui/ui/version.js'); app.import('bower_components/jquery-ui/ui/widget.js'); - app.import('bower_components/jquery-ui/ui/position.js'); app.import('bower_components/jquery-ui/ui/data.js'); - app.import('bower_components/jquery-ui/ui/disable-selection.js'); - app.import('bower_components/jquery-ui/ui/escape-selector.js'); - app.import('bower_components/jquery-ui/ui/focusable.js'); - app.import('bower_components/jquery-ui/ui/form-reset-mixin.js'); - app.import('bower_components/jquery-ui/ui/jquery-1-7.js'); - app.import('bower_components/jquery-ui/ui/keycode.js'); - app.import('bower_components/jquery-ui/ui/labels.js'); + app.import('bower_components/jquery-ui/ui/ie.js'); app.import('bower_components/jquery-ui/ui/plugin.js'); - app.import('bower_components/jquery-ui/ui/safe-active-element.js'); - app.import('bower_components/jquery-ui/ui/safe-blur.js'); app.import('bower_components/jquery-ui/ui/scroll-parent.js'); - app.import('bower_components/jquery-ui/ui/tabbable.js'); - app.import('bower_components/jquery-ui/ui/unique-id.js'); - app.import('bower_components/jquery-ui/ui/widgets/mouse.js'); - app.import('bower_components/jquery-ui/ui/widgets/draggable.js'); - app.import('bower_components/jquery-ui/ui/widgets/droppable.js'); app.import('bower_components/jquery-ui/ui/widgets/sortable.js'); - + // Core pivottable app.import('bower_components/pivottable/dist/pivot.js'); app.import('bower_components/pivottable/dist/pivot.css'); app.import('bower_components/pivottable/dist/c3_renderers.js'); From 4c26f033d0c08a1da3bb6969e683f3ea8d5ffd68 Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Wed, 10 May 2017 00:39:27 -0700 Subject: [PATCH 5/9] Fixing some tests --- app/components/records-charter.js | 16 ++--- app/instance-initializers/migrations.js | 59 +++++++-------- tests/acceptance/query-firing-test.js | 9 ++- .../components/query-blurb-test.js | 2 +- .../components/records-viewer-test.js | 53 +++++++++++++- .../instance-initializers/migrations-test.js | 38 ++++++++-- tests/unit/models/result-test.js | 72 +++++++++++++++++++ tests/unit/routes/query-test.js | 2 +- 8 files changed, 200 insertions(+), 51 deletions(-) diff --git a/app/components/records-charter.js b/app/components/records-charter.js index c280353c..b8c6b86f 100644 --- a/app/components/records-charter.js +++ b/app/components/records-charter.js @@ -40,7 +40,7 @@ export default Ember.Component.extend({ return Ember.A(columns.filter(c => this.isAny(c, 'Quantile', 'Range'))); } // Pick all string columns - return Ember.A(columns.filter(c => Ember.typeOf(sampleRow[c]) === 'string')); + return Ember.A(columns.filter(c => this.isType(sampleRow, c, 'string'))); }), dependentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { @@ -50,7 +50,7 @@ export default Ember.Component.extend({ return Ember.A(columns.filter(c => this.isAny(c, 'Count', 'Value', 'Probability'))); } // Pick all number columns - return Ember.A(columns.filter(c => Ember.typeOf(sampleRow[c]) === 'number')); + return Ember.A(columns.filter(c => this.isType(sampleRow, c, 'number'))); }), options: Ember.computed('dependentColumns', function() { @@ -77,6 +77,7 @@ export default Ember.Component.extend({ let rows = this.get('rows'); // [ [field1 values...], [field2 values...], ...] let valuesList = this.get('independentColumns').map(field => this.getFieldValues(field, rows)); + // valuesList won't be empty because all non-Raw aggregations will have at least one string field return this.zip(valuesList); }), @@ -139,19 +140,12 @@ export default Ember.Component.extend({ }, zip(arrayOfArrays, delimiter = '/') { - if (Ember.isEmpty(arrayOfArrays)) { - return []; - } let zipped = arrayOfArrays[0].map((_, i) => arrayOfArrays.map(a => a[i])); return zipped.map(a => a.reduce((p, c) => `${p}${delimiter}${c}`), ''); }, - getFieldValues(field, rows, round = false) { - let values = rows.map(row => row[field]); - if (round) { - values = values.map(v => v.toFixed(4)); - } - return values; + getFieldValues(field, rows) { + return rows.map(row => row[field]); }, actions: { diff --git a/app/instance-initializers/migrations.js b/app/instance-initializers/migrations.js index 98fedfc7..b03ed26c 100644 --- a/app/instance-initializers/migrations.js +++ b/app/instance-initializers/migrations.js @@ -3,38 +3,39 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ - import Ember from 'ember'; -export default { - name: 'migrations', - initialize(application) { - let settings = application.lookup('settings:main'); - let migrations = settings.get('migrations'); - let version = settings.get('modelVersion'); +export function initialize(application) { + let settings = application.lookup('settings:main'); + let migrations = settings.get('migrations'); + let version = settings.get('modelVersion'); - let currentVersion = localStorage.modelVersion; - if (!currentVersion || version > currentVersion) { - let manager = application.lookup('service:queryManager'); - this.applyMigrations(manager, migrations); - } - localStorage.modelVersion = version; - }, + let currentVersion = window.localStorage.modelVersion; + if (!currentVersion || version > currentVersion) { + let manager = application.lookup('service:queryManager'); + applyMigrations(manager, migrations); + } + window.localStorage.modelVersion = version; +} - /** - * Applies any forced migrations for local storage. - * @param {Object} manager The query manager. - * @param {Object} migrations An object containing migrations to apply. - */ - applyMigrations(manager, migrations) { - let deletions = migrations.deletions; - // Only support clearing everything or results at the moment - if (Ember.isEqual(deletions, 'result')) { - Ember.Logger.log('Deleting all results'); - manager.deleteAllResults(); - } else if (Ember.isEqual(deletions, 'query')) { - Ember.Logger.log('Deleting all queries'); - localStorage.clear(); - } +/** + * Applies any forced migrations for local storage. + * @param {Object} manager The query manager. + * @param {Object} migrations An object containing migrations to apply. + */ +export function applyMigrations(manager, migrations) { + let deletions = migrations.deletions; + // Only support clearing everything or results at the moment + if (Ember.isEqual(deletions, 'result')) { + Ember.Logger.log('Deleting all results'); + manager.deleteAllResults(); + } else if (Ember.isEqual(deletions, 'query')) { + Ember.Logger.log('Deleting all queries'); + window.localStorage.clear(); } +} + +export default { + name: 'migrations', + initialize }; diff --git a/tests/acceptance/query-firing-test.js b/tests/acceptance/query-firing-test.js index e7ccc405..c5f9db3a 100644 --- a/tests/acceptance/query-firing-test.js +++ b/tests/acceptance/query-firing-test.js @@ -3,20 +3,27 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ +import Ember from 'ember'; import { test } from 'qunit'; import moduleForAcceptance from 'bullet-ui/tests/helpers/module-for-acceptance'; import RESULTS from '../fixtures/results'; import COLUMNS from '../fixtures/columns'; import { mockAPI, failAPI } from '../helpers/pretender'; -let server; +let server, logger; moduleForAcceptance('Acceptance | query firing', { + beforeEach() { + logger = Ember.Logger.error; + Ember.Logger.error = function() { }; + }, + afterEach() { // Wipe out localstorage because we are creating here if (server) { server.shutdown(); } + Ember.Logger.error = logger; window.localStorage.clear(); } }); diff --git a/tests/integration/components/query-blurb-test.js b/tests/integration/components/query-blurb-test.js index e1fffc87..3016bcbb 100644 --- a/tests/integration/components/query-blurb-test.js +++ b/tests/integration/components/query-blurb-test.js @@ -37,7 +37,7 @@ test('it fully summarizes a query', function(assert) { query.addProjection('foo', 'f'); query.addProjection('bar', 'b'); this.set('mockedQuery', query); - this.render(hbs`{{query-blurb query=mockedQuery}}`); + this.render(hbs`{{query-blurb summary=mockedQuery}}`); let actualText = this.$().text(); let spaceLess = actualText.replace(/\s/g, ''); assert.equal(spaceLess, 'Filters:AnActualFilterSummaryFields:fb0.1,0.2'); diff --git a/tests/integration/components/records-viewer-test.js b/tests/integration/components/records-viewer-test.js index b9d690f4..e958ed37 100644 --- a/tests/integration/components/records-viewer-test.js +++ b/tests/integration/components/records-viewer-test.js @@ -36,22 +36,28 @@ moduleForComponent('records-viewer', 'Integration | Component | records viewer', test('it renders the data by default in a pre tag', function(assert) { this.set('mockRecords', RESULTS.SINGLE.records); - this.render(hbs`{{records-viewer records=mockRecords}}`); + this.set('rawMode', true); + this.render(hbs`{{records-viewer records=mockRecords showRawData=rawMode}}`); assert.equal(this.$('.pretty-json-container').text().trim(), JSON.stringify(RESULTS.SINGLE.records, null, 4).trim()); }); test('it allows swapping between table and the raw views', function(assert) { - assert.expect(6); + assert.expect(10); + this.set('mockModel', { isSingleRow: true }); this.set('mockRecords', RESULTS.SINGLE.records); - this.render(hbs`{{records-viewer records=mockRecords}}`); + this.render(hbs`{{records-viewer model=mockModel records=mockRecords}}`); this.$('.table-view').click(); return wait().then(() => { + assert.equal(this.$('.chart-view').length, 0); + assert.ok(this.$('.table-view').hasClass('active')); assert.equal(this.$('.pretty-json-container').length, 0); assert.equal(this.$('.records-table').length, 1); assert.equal(this.$('.lt-column').length, 3); assert.equal(this.$('.lt-body .lt-row .lt-cell').length, 3); this.$('.raw-view').click(); return wait().then(() => { + assert.equal(this.$('.chart-view').length, 0); + assert.ok(this.$('.raw-view').hasClass('active')); assert.equal(this.$('.records-table').length, 0); assert.equal(this.$('.pretty-json-container').length, 1); }); @@ -107,3 +113,44 @@ test('it handles missing columns across rows when making CSVs', function(assert) this.render(hbs`{{records-viewer records=mockRecords}}`); this.$('.download-option > a:eq(1)').click(); }); + +test('it enables charting mode if the results have more than one row', function(assert) { + assert.expect(9); + this.set('tableMode', true); + this.set('mockModel', { isSingleRow: false }); + this.set('mockRecords', RESULTS.GROUP.records); + this.render(hbs`{{records-viewer model=mockModel records=mockRecords showTable=tableMode}}`); + assert.equal(this.$('.chart-view').length, 1); + assert.equal(this.$('.pretty-json-container').length, 0); + assert.equal(this.$('.records-table').length, 1); + assert.equal(this.$('.lt-column').length, 4); + assert.equal(this.$('.lt-body .lt-row .lt-cell').length, 12); + this.$('.chart-view').click(); + return wait().then(() => { + assert.equal(this.$('.records-table').length, 0); + assert.equal(this.$('.pretty-json-container').length, 0); + // Defaults to simple chart view + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); + }); +}); + +test('it enables pivot mode if the results are raw', function(assert) { + assert.expect(9); + this.set('tableMode', true); + this.set('mockModel', { isRaw: true, isSingleRow: false }); + this.set('mockRecords', RESULTS.MULTIPLE.records); + this.render(hbs`{{records-viewer model=mockModel records=mockRecords showTable=tableMode}}`); + assert.equal(this.$('.chart-view').length, 1); + assert.equal(this.$('.pretty-json-container').length, 0); + assert.equal(this.$('.records-table').length, 1); + assert.equal(this.$('.lt-column').length, 3); + assert.equal(this.$('.lt-body .lt-row .lt-cell').length, 9); + this.$('.chart-view').click(); + return wait().then(() => { + assert.equal(this.$('.records-table').length, 0); + assert.equal(this.$('.pretty-json-container').length, 0); + assert.equal(this.$('.mode-toggle').length, 0); + assert.equal(this.$('.visual-container .pivot-table-container').length, 1); + }); +}); diff --git a/tests/unit/instance-initializers/migrations-test.js b/tests/unit/instance-initializers/migrations-test.js index eb5ff9db..b4f0748b 100644 --- a/tests/unit/instance-initializers/migrations-test.js +++ b/tests/unit/instance-initializers/migrations-test.js @@ -1,25 +1,53 @@ import Ember from 'ember'; -import { initialize } from 'bullet-ui/instance-initializers/migrations'; +import StartupInitializer from 'bullet-ui/initializers/startup'; +import { initialize, applyMigrations } from 'bullet-ui/instance-initializers/migrations'; import { module, test } from 'qunit'; import destroyApp from '../../helpers/destroy-app'; +let logger; + module('Unit | Instance Initializer | migrations', { beforeEach() { Ember.run(() => { this.application = Ember.Application.create(); + StartupInitializer.initialize(this.application); this.appInstance = this.application.buildInstance(); }); + logger = Ember.Logger.log; + Ember.Logger.log = function() { }; }, + afterEach() { + Ember.Logger.log = logger; Ember.run(this.appInstance, 'destroy'); destroyApp(this.application); } }); -// Replace this with your real tests. -test('it works', function(assert) { +test('it initializes', function(assert) { initialize(this.appInstance); - - // you would normally confirm the results of the initializer here assert.ok(true); }); + +test('it applies the delete results migration', function(assert) { + assert.expect(1); + + let manager = { + deleteAllResults() { + assert.ok(true); + } + }; + let migrations = { deletions: 'result' }; + + applyMigrations(manager, migrations); +}); + +test('it applies the delete queries migration', function(assert) { + assert.expect(1); + + let migrations = { deletions: 'query' }; + window.localStorage.foo = 'bar'; + + applyMigrations(null, migrations); + assert.notOk(window.localStorage.foo); +}); diff --git a/tests/unit/models/result-test.js b/tests/unit/models/result-test.js index 7fc2d2b8..011ec246 100644 --- a/tests/unit/models/result-test.js +++ b/tests/unit/models/result-test.js @@ -5,6 +5,7 @@ */ import Ember from 'ember'; import { moduleForModel, test } from 'ember-qunit'; +import { AGGREGATIONS } from 'bullet-ui/models/aggregation'; moduleForModel('result', 'Unit | Model | result', { needs: ['model:query'] @@ -20,3 +21,74 @@ test('it sets its default values right', function(assert) { assert.ok(parseInt(created.getTime()) >= now); }); +test('it recognizes a raw result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('RAW') }); + assert.ok(model.get('isRaw')); + assert.notOk(model.get('isReallyRaw')); + }); +}); + +test('it recognizes a really raw result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('RAW'), projectionsSize: 0 }); + assert.ok(model.get('isRaw')); + assert.ok(model.get('isReallyRaw')); + assert.notOk(model.get('isSingleRow')); + }); +}); + +test('it recognizes a count distinct result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('COUNT_DISTINCT') }); + assert.ok(model.get('isCountDistinct')); + assert.ok(model.get('isSingleRow')); + }); +}); + +test('it recognizes a group by result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('GROUP'), groupsSize: 2, metricsSize: 2 }); + assert.ok(model.get('isGroupBy')); + assert.notOk(model.get('isGroupAll')); + assert.notOk(model.get('isSingleRow')); + }); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('GROUP'), groupsSize: 1, metricsSize: 1 }); + assert.ok(model.get('isGroupBy')); + assert.notOk(model.get('isGroupAll')); + assert.notOk(model.get('isSingleRow')); + }); +}); + +test('it recognizes a group all result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('GROUP'), groupsSize: 0 }); + assert.notOk(model.get('isGroupBy')); + assert.ok(model.get('isGroupAll')); + assert.ok(model.get('isSingleRow')); + }); +}); + +test('it recognizes a distribution result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('DISTRIBUTION') }); + assert.ok(model.get('isDistribution')); + assert.notOk(model.get('isSingleRow')); + }); +}); + +test('it recognizes a top k result type', function(assert) { + let model = this.subject(); + Ember.run(() => { + model.set('querySnapshot', { type: AGGREGATIONS.get('TOP_K') }); + assert.ok(model.get('isTopK')); + assert.notOk(model.get('isSingleRow')); + }); +}); diff --git a/tests/unit/routes/query-test.js b/tests/unit/routes/query-test.js index ba6942a1..5a913c9f 100644 --- a/tests/unit/routes/query-test.js +++ b/tests/unit/routes/query-test.js @@ -6,7 +6,7 @@ import { moduleFor, test } from 'ember-qunit'; moduleFor('route:query', 'Unit | Route | query', { - needs: ['service:querier'] + needs: ['service:querier', 'service:queryManager'] }); test('it aborts a pending request if one exists', function(assert) { From e583b0842b2191e017ad698ae570596d904f6b7a Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Wed, 10 May 2017 15:32:38 -0700 Subject: [PATCH 6/9] Adding pivot options saving. Adding all but acceptance tests --- app/components/pivot-table.js | 37 ++++++-- app/components/records-charter.js | 9 ++ app/initializers/startup.js | 16 +++- app/models/result.js | 1 + app/templates/components/records-charter.hbs | 2 +- config/env-settings.json | 2 +- tests/fixtures/results.js | 43 +++++++++ tests/helpers/mocked-query.js | 9 +- .../components/pivot-table-test.js | 51 ++++++++--- .../components/records-charter-test.js | 91 ++++++++++++++++--- .../components/records-viewer-test.js | 46 +++++++++- tests/unit/initializers/startup-test.js | 29 ++++++ 12 files changed, 293 insertions(+), 43 deletions(-) diff --git a/app/components/pivot-table.js b/app/components/pivot-table.js index e1577fb3..6392e452 100644 --- a/app/components/pivot-table.js +++ b/app/components/pivot-table.js @@ -3,15 +3,38 @@ import Ember from 'ember'; export default Ember.Component.extend({ rows: null, columns: null, - defaultRenderer: 'Table', + initialOptions: null, + defaultOptions: { + unusedAttrsVertical: true, + menuLimit: 200, + renderers: Ember.$.extend( + Ember.$.pivotUtilities.renderers, + Ember.$.pivotUtilities.c3_renderers + ) + }, + + options: Ember.computed('initialOptions', 'defaultOptions', function() { + let deserialized = this.get('initialOptions'); + let options = this.get('defaultOptions'); + // Attach refresh handler + return Ember.$.extend({ onRefresh: this.refreshHandler(this) }, deserialized, options); + }), didInsertElement() { this._super(...arguments); - this.$('.pivot-table-container').pivotUI(this.get('rows'), { - renderers: Ember.$.extend( - Ember.$.pivotUtilities.renderers, - Ember.$.pivotUtilities.c3_renderers - ) - }); + let { rows, options } = this.getProperties('rows', 'options'); + this.$('.pivot-table-container').pivotUI(rows, options); + }, + + refreshHandler(context) { + return (configuration) => { + let copy = JSON.parse(JSON.stringify(configuration)); + // Deletes functions and defaults: http://nicolas.kruchten.com/pivottable/examples/onrefresh.html + delete copy.aggregators; + delete copy.renderers; + delete copy.rendererOptions; + delete copy.localeStrings; + context.sendAction('onRefresh', copy); + }; } }); diff --git a/app/components/records-charter.js b/app/components/records-charter.js index b8c6b86f..40c44d7b 100644 --- a/app/components/records-charter.js +++ b/app/components/records-charter.js @@ -17,6 +17,9 @@ export default Ember.Component.extend({ canModeSwitch: Ember.computed.not('cannotModeSwitch').readOnly(), notSimpleMode: Ember.computed.not('simpleMode').readOnly(), pivotMode: Ember.computed.or('notSimpleMode', 'cannotModeSwitch').readOnly(), + pivotOptions: Ember.computed('model.pivotOptions', function() { + return JSON.parse(this.get('model.pivotOptions')); + }).readOnly(), sampleRow: Ember.computed('rows', 'columns', function() { let typicalRow = { }; @@ -151,6 +154,12 @@ export default Ember.Component.extend({ actions: { toggleMode() { this.toggleProperty('simpleMode'); + }, + + saveOptions(options) { + let model = this.get('model'); + model.set('pivotOptions', JSON.stringify(options)); + model.save(); } } }); diff --git a/app/initializers/startup.js b/app/initializers/startup.js index fd9b1cb5..478d6ca2 100644 --- a/app/initializers/startup.js +++ b/app/initializers/startup.js @@ -16,9 +16,7 @@ export default { decodedSettings = JSON.parse(decodeURIComponent(metaSettings)); } // Merge into default settings, overriding them - let settings = { }; - Ember.merge(settings, ENV.APP.SETTINGS); - Ember.$.extend(true, settings, decodedSettings); + let settings = this.deepMergeSettings(decodedSettings); application.register('settings:main', Ember.Object.create(settings), { instantiate: false }); application.inject('service', 'settings', 'settings:main'); @@ -27,5 +25,17 @@ export default { application.inject('model', 'settings', 'settings:main'); application.inject('controller', 'settings', 'settings:main'); application.inject('component', 'settings', 'settings:main'); + }, + + deepMergeSettings(overrides) { + let settings = JSON.parse(JSON.stringify(ENV.APP.SETTINGS)); + Ember.$.extend(true, settings, overrides); + + // Handle arrays manually + let helpLinks = []; + Ember.$.merge(helpLinks, ENV.APP.SETTINGS.helpLinks || []); + Ember.$.merge(helpLinks, overrides.helpLinks || []); + settings.helpLinks = helpLinks; + return settings; } }; diff --git a/app/models/result.js b/app/models/result.js index b97667d5..3ac6c229 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -25,6 +25,7 @@ export default DS.Model.extend({ }), query: DS.belongsTo('query', { autoSave: true }), querySnapshot: DS.attr(), + pivotOptions: DS.attr('string'), isRaw: Ember.computed('querySnapshot', function() { return this.get('querySnapshot.type') === AGGREGATIONS.get('RAW'); diff --git a/app/templates/components/records-charter.hbs b/app/templates/components/records-charter.hbs index 9f3ba8e0..69beea17 100644 --- a/app/templates/components/records-charter.hbs +++ b/app/templates/components/records-charter.hbs @@ -11,7 +11,7 @@ {{/if}}
{{#if pivotMode}} - {{pivot-table rows=rows columns=columns}} + {{pivot-table rows=rows columns=columns initialOptions=pivotOptions onRefresh=(action "saveOptions")}} {{else}} {{ember-chart type=chartType data=data options=options}} {{/if}} diff --git a/config/env-settings.json b/config/env-settings.json index 75cfc00c..9d897b13 100644 --- a/config/env-settings.json +++ b/config/env-settings.json @@ -14,7 +14,7 @@ "bugLink": "https://github.com/yahoo/bullet-ui/issues", "modelVersion": 2, "migrations": { - "deletions": "" + "deletions": "results" }, "defaultValues": { "aggregationMaxSize": 512, diff --git a/tests/fixtures/results.js b/tests/fixtures/results.js index afea66b2..8fdd4595 100644 --- a/tests/fixtures/results.js +++ b/tests/fixtures/results.js @@ -130,6 +130,49 @@ const RESULTS = { } ] }, + GROUP_MULTIPLE_METRICS: { + meta: { + wasEstimated: false, + standardDeviations: { + 1: { + upperBound: 3, + lowerBound: 3 + }, + 2: { + upperBound: 3, + lowerBound: 3 + }, + 3: { + upperBound: 3, + lowerBound: 3 + } + }, + uniquesEstimate: 3 + }, + records: [ + { + avg_bar: 42.3424, + sum_foo: 12, + COUNT: 1342, + foo: 'value1', + bar: 'value2' + }, + { + avg_bar: 22.02, + sum_foo: 92, + COUNT: 302, + foo: 'value1', + bar: null + }, + { + avg_bar: 92.3424, + sum_foo: 123192, + COUNT: 234342, + foo: null, + bar: 'value2' + } + ] + }, DISTRIBUTION: { meta: { wasEstimated: false, diff --git a/tests/helpers/mocked-query.js b/tests/helpers/mocked-query.js index 597b46c7..6b0ce146 100644 --- a/tests/helpers/mocked-query.js +++ b/tests/helpers/mocked-query.js @@ -39,8 +39,11 @@ export let MockMetric = Ember.Object.extend({ }); export let MockResult = Ember.Object.extend({ + metadata: null, records: null, - created: null + created: null, + querySnapshot: null, + pivotOptions: null }); export default Ember.Object.extend({ @@ -110,8 +113,8 @@ export default Ember.Object.extend({ this.nestedPropertyAsPromise('aggregation', 'metrics'); }, - addResult(records, created = new Date(Date.now())) { - this.get('_results').pushObject(MockResult.create({ records, created })); + addResult(records, created = new Date(Date.now()), metadata = null, querySnapshot = null, pivotOptions = null) { + this.get('_results').pushObject(MockResult.create({ records, created, metadata, querySnapshot, pivotOptions })); this.topLevelPropertyAsPromise('results'); }, diff --git a/tests/integration/components/pivot-table-test.js b/tests/integration/components/pivot-table-test.js index a54cae3a..003e0b0e 100644 --- a/tests/integration/components/pivot-table-test.js +++ b/tests/integration/components/pivot-table-test.js @@ -1,25 +1,52 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ import { moduleForComponent, test } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; +import RESULTS from '../../fixtures/results'; moduleForComponent('pivot-table', 'Integration | Component | pivot table', { integration: true }); -test('it renders', function(assert) { +test('it renders a pivot table', function(assert) { + assert.expect(1); + this.set('mockRows', RESULTS.GROUP_MULTIPLE_METRICS.records); + this.set('mockColumns', ['foo', 'bar', 'COUNT', 'avg_bar', 'sum_foo']); + this.set('mockOnRefresh', () => { }); - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); + this.render(hbs`{{pivot-table rows=mockRows columns=mockColumns initialOptions=mockOptions + onRefresh=(action mockOnRefresh)}}`); + assert.equal(this.$('.pvtUi select.pvtRenderer').val(), 'Table'); +}); - this.render(hbs`{{pivot-table}}`); +test('it generates options by combining defaults with provided options', function(assert) { + assert.expect(1); + this.set('mockOnRefresh', (config) => { + assert.equal(config.rendererName, 'Heatmap'); + }); + this.set('mockRows', RESULTS.GROUP_MULTIPLE_METRICS.records); + this.set('mockColumns', ['foo', 'bar', 'COUNT', 'avg_bar', 'sum_foo']); + this.set('mockOptions', { rendererName: 'Heatmap' }); - assert.equal(this.$().text().trim(), ''); + this.render(hbs`{{pivot-table rows=mockRows columns=mockColumns initialOptions=mockOptions + onRefresh=(action mockOnRefresh)}}`); +}); - // Template block usage: - this.render(hbs` - {{#pivot-table}} - template block text - {{/pivot-table}} - `); +test('it removes certain options before returning the configuration', function(assert) { + assert.expect(4); + this.set('mockOnRefresh', (config) => { + assert.notOk(config.localeStrings); + assert.notOk(config.aggregators); + assert.notOk(config.renderers); + assert.notOk(config.rendererOptions); + }); + this.set('mockRows', RESULTS.GROUP_MULTIPLE_METRICS.records); + this.set('mockColumns', ['foo', 'bar', 'COUNT', 'avg_bar', 'sum_foo']); + this.set('mockOptions', { rendererOptions: { foo: 'bar' }, localeStrings: { bar: 'foo' } }); - assert.equal(this.$().text().trim(), 'template block text'); + this.render(hbs`{{pivot-table rows=mockRows columns=mockColumns initialOptions=mockOptions + onRefresh=(action mockOnRefresh)}}`); }); diff --git a/tests/integration/components/records-charter-test.js b/tests/integration/components/records-charter-test.js index c6f7756c..f719095f 100644 --- a/tests/integration/components/records-charter-test.js +++ b/tests/integration/components/records-charter-test.js @@ -1,25 +1,92 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Ember from 'ember'; import { moduleForComponent, test } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import RESULTS from '../../fixtures/results'; moduleForComponent('records-charter', 'Integration | Component | records charter', { integration: true }); -test('it renders', function(assert) { +test('it starts off in chart mode and allows you to switch to pivot mode', function(assert) { + assert.expect(5); + this.set('mockModel', Ember.Object.create({ isRaw: false, isDistribution: true, pivotOptions: null, save() { } })); + this.set('mockRows', RESULTS.DISTRIBUTION.records); + this.set('mockColumns', ['Probability', 'Count', 'Range']); + this.render(hbs`{{records-charter rows=mockRows columns=mockColumns model=mockModel}}`); - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); + this.$('.mode-toggle .advanced-view').click(); + return wait().then(() => { + assert.ok(this.$('.mode-toggle .advanced-view').hasClass('selected')); + assert.equal(this.$('.visual-container .pivot-table-container').length, 1); + assert.equal(this.$('.visual-container .pivot-table-container .pvtUi').length, 1); + }); +}); - this.render(hbs`{{records-charter}}`); +test('it charts a single dependent column', function(assert) { + assert.expect(2); + this.set('mockModel', Ember.Object.create({ isRaw: false, pivotOptions: null, save() { } })); + this.set('mockRows', RESULTS.SINGLE.records); + this.set('mockColumns', ['foo', 'timestamp', 'domain']); + this.render(hbs`{{records-charter rows=mockRows columns=mockColumns model=mockModel}}`); + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); +}); - assert.equal(this.$().text().trim(), ''); +test('it charts multiple dependent columns', function(assert) { + assert.expect(2); + this.set('mockModel', Ember.Object.create({ isRaw: false, pivotOptions: null, save() { } })); + this.set('mockRows', RESULTS.GROUP_MULTIPLE_METRICS.records); + this.set('mockColumns', ['foo', 'bar', 'COUNT', 'avg_bar', 'sum_foo']); + this.render(hbs`{{records-charter rows=mockRows columns=mockColumns model=mockModel}}`); + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); +}); + +test('it enables only the pivot mode if the results are raw', function(assert) { + assert.expect(3); + this.set('mockModel', Ember.Object.create({ isRaw: true, pivotOptions: null, save() { } })); + this.set('mockRows', RESULTS.SINGLE.records); + this.set('mockColumns', ['foo', 'timestamp', 'domain']); + this.render(hbs`{{records-charter rows=mockRows columns=mockColumns model=mockModel}}`); + assert.equal(this.$('.mode-toggle').length, 0); + assert.equal(this.$('.visual-container .pivot-table-container').length, 1); + assert.equal(this.$('.visual-container .pivot-table-container .pvtUi').length, 1); +}); - // Template block usage: - this.render(hbs` - {{#records-charter}} - template block text - {{/records-charter}} - `); +test('it saves pivot table configurations', function(assert) { + assert.expect(8); + this.set('mockModel', Ember.Object.create({ + isRaw: false, + isDistribution: true, + pivotOptions: null, + save() { + // Called twice + assert.ok(true); + } + })); + this.set('mockRows', RESULTS.DISTRIBUTION.records); + this.set('mockColumns', ['Probability', 'Count', 'Range']); + this.render(hbs`{{records-charter rows=mockRows columns=mockColumns model=mockModel}}`); - assert.equal(this.$().text().trim(), 'template block text'); + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); + this.$('.mode-toggle .advanced-view').click(); + return wait().then(() => { + assert.ok(this.$('.mode-toggle .advanced-view').hasClass('selected')); + assert.equal(this.$('.visual-container .pivot-table-container .pvtUi').length, 1); + assert.equal(this.$('.pvtUi select.pvtRenderer').val(), 'Table'); + this.$('.pivot-table-container select.pvtRenderer').val('Bar Chart').trigger('change'); + return wait().then(() => { + let options = JSON.parse(this.get('mockModel.pivotOptions')); + assert.equal(options.rendererName, 'Bar Chart'); + }); + }); }); diff --git a/tests/integration/components/records-viewer-test.js b/tests/integration/components/records-viewer-test.js index e958ed37..d47ea35e 100644 --- a/tests/integration/components/records-viewer-test.js +++ b/tests/integration/components/records-viewer-test.js @@ -3,6 +3,7 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ +import Ember from 'ember'; import { moduleForComponent, test } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; import wait from 'ember-test-helpers/wait'; @@ -135,11 +136,17 @@ test('it enables charting mode if the results have more than one row', function( }); }); -test('it enables pivot mode if the results are raw', function(assert) { - assert.expect(9); +test('it allows you to switch to pivot mode', function(assert) { + assert.expect(12); this.set('tableMode', true); - this.set('mockModel', { isRaw: true, isSingleRow: false }); - this.set('mockRecords', RESULTS.MULTIPLE.records); + this.set('mockModel', Ember.Object.create({ + isSingleRow: false, + pivotOptions: null, + save() { + assert.ok(true); + } + })); + this.set('mockRecords', RESULTS.DISTRIBUTION.records); this.render(hbs`{{records-viewer model=mockModel records=mockRecords showTable=tableMode}}`); assert.equal(this.$('.chart-view').length, 1); assert.equal(this.$('.pretty-json-container').length, 0); @@ -147,6 +154,37 @@ test('it enables pivot mode if the results are raw', function(assert) { assert.equal(this.$('.lt-column').length, 3); assert.equal(this.$('.lt-body .lt-row .lt-cell').length, 9); this.$('.chart-view').click(); + return wait().then(() => { + assert.equal(this.$('.records-table').length, 0); + assert.equal(this.$('.pretty-json-container').length, 0); + assert.ok(this.$('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(this.$('.visual-container canvas').length, 1); + this.$('.mode-toggle .advanced-view').click(); + return wait().then(() => { + assert.ok(this.$('.mode-toggle .advanced-view').hasClass('selected')); + assert.equal(this.$('.visual-container .pivot-table-container').length, 1); + }); + }); +}); + +test('it enables only pivot mode if the results are raw', function(assert) { + assert.expect(10); + this.set('tableMode', true); + this.set('mockModel', Ember.Object.create({ + isRaw: true, + pivotOptions: null, + save() { + assert.ok(true); + } + })); + this.set('mockRecords', RESULTS.GROUP.records); + this.render(hbs`{{records-viewer model=mockModel records=mockRecords showTable=tableMode}}`); + assert.equal(this.$('.chart-view').length, 1); + assert.equal(this.$('.pretty-json-container').length, 0); + assert.equal(this.$('.records-table').length, 1); + assert.equal(this.$('.lt-column').length, 4); + assert.equal(this.$('.lt-body .lt-row .lt-cell').length, 12); + this.$('.chart-view').click(); return wait().then(() => { assert.equal(this.$('.records-table').length, 0); assert.equal(this.$('.pretty-json-container').length, 0); diff --git a/tests/unit/initializers/startup-test.js b/tests/unit/initializers/startup-test.js index 194e4a7a..594d1487 100644 --- a/tests/unit/initializers/startup-test.js +++ b/tests/unit/initializers/startup-test.js @@ -6,6 +6,7 @@ import Ember from 'ember'; import StartupInitializer from 'bullet-ui/initializers/startup'; import { module, test } from 'qunit'; +import ENV from 'bullet-ui/config/environment'; let application; @@ -19,6 +20,34 @@ module('Unit | Initializer | startup', { } }); +test('it can deep merge settings', function(assert) { + let overrides = { + modelVersion: 10, + helpLinks: [{ name: 'foo', link: 'http://foo.bar.com' }], + migrations: { deletions: 'query' }, + defaultValues: { + aggregationMaxSize: 200, + sketches: { countDistinctMaxEntries: 10 }, + metadataKeyMapping: { theta: 'foo', foo: 'bar' } + } + }; + let merged = StartupInitializer.deepMergeSettings(overrides); + + let expected = JSON.parse(JSON.stringify(ENV.APP.SETTINGS)); + expected.modelVersion = 10; + expected.helpLinks = [ + { name: 'Tutorials', link: 'https://yahoo.github.io/bullet-docs/ui/usage' }, + { name: 'foo', link: 'http://foo.bar.com' } + ]; + expected.migrations.deletions = 'query'; + expected.defaultValues.aggregationMaxSize = 200; + expected.defaultValues.sketches.countDistinctMaxEntries = 10; + expected.defaultValues.metadataKeyMapping.theta = 'foo'; + expected.defaultValues.metadataKeyMapping.foo = 'bar'; + + assert.deepEqual(merged, expected); +}); + test('it registers the settings factory', function(assert) { assert.ok(application.hasRegistration('settings:main')); }); From f9fe2cb6d70d7a6e8f9c6351e31688978fc5f3d7 Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Wed, 10 May 2017 16:14:08 -0700 Subject: [PATCH 7/9] Adding log suppressions for acceptance tests --- app/components/results-table.js | 7 ---- config/env-settings.json | 2 +- tests/acceptance/navigation-test.js | 2 ++ .../query-default-api-filter-test.js | 2 ++ tests/acceptance/query-default-filter-test.js | 2 ++ tests/acceptance/query-error-test.js | 2 ++ tests/acceptance/query-firing-test.js | 8 ++--- tests/acceptance/query-lifecycle-test.js | 2 ++ .../query-results-lifecycle-test.js | 2 ++ tests/acceptance/query-summarization-test.js | 2 ++ tests/acceptance/query-validation-test.js | 2 ++ tests/acceptance/result-error-test.js | 5 +-- tests/acceptance/result-lifecycle-test.js | 2 ++ tests/acceptance/schema-test.js | 2 ++ tests/helpers/module-for-acceptance.js | 12 +++++++ .../components/results-table-test.js | 33 ++++++++++++++++++- 16 files changed, 70 insertions(+), 17 deletions(-) diff --git a/app/components/results-table.js b/app/components/results-table.js index c30fa673..8cee9dd4 100644 --- a/app/components/results-table.js +++ b/app/components/results-table.js @@ -36,12 +36,5 @@ export default Ember.Component.extend(PaginatedTable, { this._super(...arguments); this.set('table', new Table(this.get('columns'))); this.addPages(1); - }, - - actions: { - resultClick(result) { - this.sendAction('resultClick', result); - } } }); - diff --git a/config/env-settings.json b/config/env-settings.json index 9d897b13..009d121f 100644 --- a/config/env-settings.json +++ b/config/env-settings.json @@ -14,7 +14,7 @@ "bugLink": "https://github.com/yahoo/bullet-ui/issues", "modelVersion": 2, "migrations": { - "deletions": "results" + "deletions": "result" }, "defaultValues": { "aggregationMaxSize": 512, diff --git a/tests/acceptance/navigation-test.js b/tests/acceptance/navigation-test.js index 0a489031..451505fd 100644 --- a/tests/acceptance/navigation-test.js +++ b/tests/acceptance/navigation-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | navigation', { + suppressLogging: true, + beforeEach() { server = mockAPI(RESULTS.MULTIPLE, COLUMNS.BASIC); }, diff --git a/tests/acceptance/query-default-api-filter-test.js b/tests/acceptance/query-default-api-filter-test.js index 98603c48..a3d069ad 100644 --- a/tests/acceptance/query-default-api-filter-test.js +++ b/tests/acceptance/query-default-api-filter-test.js @@ -16,6 +16,8 @@ let url = 'http://foo.bar.com/api/filter'; let hit = 0; moduleForAcceptance('Acceptance | query default api filter', { + suppressLogging: true, + beforeEach() { // Inject into defaultValues in routes, our mock filter values this.application.register('settings:mocked', Ember.Object.create({ defaultFilter: url }), { instantiate: false }); diff --git a/tests/acceptance/query-default-filter-test.js b/tests/acceptance/query-default-filter-test.js index 021ba967..6e3df675 100644 --- a/tests/acceptance/query-default-filter-test.js +++ b/tests/acceptance/query-default-filter-test.js @@ -14,6 +14,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query default filter', { + suppressLogging: true, + beforeEach() { // Inject into defaultValues in routes, our mock filter values this.application.register('settings:mocked', Ember.Object.create({ defaultFilter: FILTERS.AND_LIST }), { instantiate: false }); diff --git a/tests/acceptance/query-error-test.js b/tests/acceptance/query-error-test.js index 3e2580e5..0a509ba7 100644 --- a/tests/acceptance/query-error-test.js +++ b/tests/acceptance/query-error-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query error', { + suppressLogging: true, + beforeEach() { server = mockAPI(RESULTS.MULTIPLE, COLUMNS.BASIC); }, diff --git a/tests/acceptance/query-firing-test.js b/tests/acceptance/query-firing-test.js index c5f9db3a..fc2eb55a 100644 --- a/tests/acceptance/query-firing-test.js +++ b/tests/acceptance/query-firing-test.js @@ -10,20 +10,16 @@ import RESULTS from '../fixtures/results'; import COLUMNS from '../fixtures/columns'; import { mockAPI, failAPI } from '../helpers/pretender'; -let server, logger; +let server; moduleForAcceptance('Acceptance | query firing', { - beforeEach() { - logger = Ember.Logger.error; - Ember.Logger.error = function() { }; - }, + suppressLogging: true, afterEach() { // Wipe out localstorage because we are creating here if (server) { server.shutdown(); } - Ember.Logger.error = logger; window.localStorage.clear(); } }); diff --git a/tests/acceptance/query-lifecycle-test.js b/tests/acceptance/query-lifecycle-test.js index 41cda7df..dfd1e512 100644 --- a/tests/acceptance/query-lifecycle-test.js +++ b/tests/acceptance/query-lifecycle-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query lifecycle', { + suppressLogging: true, + beforeEach() { // Wipe out localstorage because we are creating queries here window.localStorage.clear(); diff --git a/tests/acceptance/query-results-lifecycle-test.js b/tests/acceptance/query-results-lifecycle-test.js index 40f6b165..4a292e6d 100644 --- a/tests/acceptance/query-results-lifecycle-test.js +++ b/tests/acceptance/query-results-lifecycle-test.js @@ -13,6 +13,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query results lifecycle', { + suppressLogging: true, + beforeEach() { // Wipe out localstorage because we are creating queries here window.localStorage.clear(); diff --git a/tests/acceptance/query-summarization-test.js b/tests/acceptance/query-summarization-test.js index 448358c9..425632be 100644 --- a/tests/acceptance/query-summarization-test.js +++ b/tests/acceptance/query-summarization-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query summarization', { + suppressLogging: true, + beforeEach() { // Wipe out localstorage because we are creating queries here window.localStorage.clear(); diff --git a/tests/acceptance/query-validation-test.js b/tests/acceptance/query-validation-test.js index 450f880c..eff4714a 100644 --- a/tests/acceptance/query-validation-test.js +++ b/tests/acceptance/query-validation-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | query validation', { + suppressLogging: true, + beforeEach() { // Wipe out localstorage because we are creating here window.localStorage.clear(); diff --git a/tests/acceptance/result-error-test.js b/tests/acceptance/result-error-test.js index 036d4c65..075ace5c 100644 --- a/tests/acceptance/result-error-test.js +++ b/tests/acceptance/result-error-test.js @@ -6,7 +6,9 @@ import { test } from 'qunit'; import moduleForAcceptance from 'bullet-ui/tests/helpers/module-for-acceptance'; -moduleForAcceptance('Acceptance | result error'); +moduleForAcceptance('Acceptance | result error', { + suppressLogging: true +}); test('visiting a non-existant result', function(assert) { visit('/result/foo'); @@ -15,4 +17,3 @@ test('visiting a non-existant result', function(assert) { assert.equal(currentURL(), '/not-found'); }); }); - diff --git a/tests/acceptance/result-lifecycle-test.js b/tests/acceptance/result-lifecycle-test.js index 6f422181..e60e8a1f 100644 --- a/tests/acceptance/result-lifecycle-test.js +++ b/tests/acceptance/result-lifecycle-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | result lifecycle', { + suppressLogging: true, + beforeEach() { // Wipe out localstorage because we are creating queries here window.localStorage.clear(); diff --git a/tests/acceptance/schema-test.js b/tests/acceptance/schema-test.js index 6083e960..54395d61 100644 --- a/tests/acceptance/schema-test.js +++ b/tests/acceptance/schema-test.js @@ -12,6 +12,8 @@ import { mockAPI } from '../helpers/pretender'; let server; moduleForAcceptance('Acceptance | schema', { + suppressLogging: true, + beforeEach() { server = mockAPI(RESULTS.SINGLE, COLUMNS.BASIC); }, diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js index 930c5759..6f0d3dbc 100644 --- a/tests/helpers/module-for-acceptance.js +++ b/tests/helpers/module-for-acceptance.js @@ -11,16 +11,28 @@ import destroyApp from '../helpers/destroy-app'; const { RSVP: { Promise } } = Ember; export default function(name, options = {}) { + let logger; + const NOOP = () => { }; + const LOGGER = { log: NOOP, warn: NOOP, error: NOOP, assert: NOOP, debug: NOOP, info: NOOP }; + module(name, { beforeEach() { this.application = startApp(); + if (options.suppressLogging) { + logger = Ember.Logger; + Ember.Logger = LOGGER; + } + if (options.beforeEach) { return options.beforeEach.apply(this, arguments); } }, afterEach() { + if (options.suppressLogging) { + Ember.Logger = logger; + } let afterEach = options.afterEach && options.afterEach.apply(this, arguments); return Promise.resolve(afterEach).then(() => destroyApp(this.application)); } diff --git a/tests/integration/components/results-table-test.js b/tests/integration/components/results-table-test.js index bc5d5841..d5feb794 100644 --- a/tests/integration/components/results-table-test.js +++ b/tests/integration/components/results-table-test.js @@ -24,7 +24,7 @@ test('it displays a row with two cells in two columns', function(assert) { assert.equal(this.$('.lt-body .lt-row .lt-cell').eq(1).text().trim(), '3'); }); -test('it sorts a column on click', function(assert) { +test('it sorts by the number of records column on click', function(assert) { assert.expect(2); this.set('mockResults', Ember.A([ Ember.Object.create({ created: new Date(2014, 11, 31), records: Ember.A([1, 2, 3]) }), @@ -39,3 +39,34 @@ test('it sorts a column on click', function(assert) { assert.equal(spaceLess, '01Feb12:00AM031Dec12:00AM3'); }); }); + +test('it sorts by the date column on click', function(assert) { + assert.expect(2); + this.set('mockResults', Ember.A([ + Ember.Object.create({ created: new Date(2015, 1, 1), records: Ember.A() }), + Ember.Object.create({ created: new Date(2014, 11, 31), records: Ember.A([1, 2, 3]) }) + ])); + this.render(hbs`{{results-table results=mockResults}}`); + assert.equal(this.$('.lt-head .lt-column.is-sortable').length, 2); + this.$('.lt-head .lt-column.is-sortable').eq(0).click(); + return wait().then(() => { + let text = this.$('.lt-body .lt-row .lt-cell').text(); + let spaceLess = text.replace(/\s/g, ''); + assert.equal(spaceLess, '31Dec12:00AM301Feb12:00AM0'); + }); +}); + +test('it sends the resultClick action on click', function(assert) { + assert.expect(2); + this.set('mockResultClick', (result) => { + assert.equal(result.get('records.length'), 3); + }); + this.set('mockResults', Ember.A([ + Ember.Object.create({ created: new Date(2015, 1, 1), records: Ember.A() }), + Ember.Object.create({ created: new Date(2014, 11, 31), records: Ember.A([1, 2, 3]) }) + ])); + this.render(hbs`{{results-table results=mockResults resultClick=(action mockResultClick)}}`); + assert.equal(this.$('.lt-head .lt-column.is-sortable').length, 2); + this.$('.lt-body .lt-row .lt-cell .result-date-entry').eq(1).click(); + return wait(); +}); From fca6fb13c0c54f0b7ffbd5ce7a643346528fd2da Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Wed, 10 May 2017 16:39:44 -0700 Subject: [PATCH 8/9] Adding acceptance tests --- tests/acceptance/query-firing-test.js | 1 - tests/acceptance/result-lifecycle-test.js | 108 ++++++++++++++++++++++ tests/helpers/module-for-acceptance.js | 3 +- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/query-firing-test.js b/tests/acceptance/query-firing-test.js index fc2eb55a..08635f96 100644 --- a/tests/acceptance/query-firing-test.js +++ b/tests/acceptance/query-firing-test.js @@ -3,7 +3,6 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -import Ember from 'ember'; import { test } from 'qunit'; import moduleForAcceptance from 'bullet-ui/tests/helpers/module-for-acceptance'; import RESULTS from '../fixtures/results'; diff --git a/tests/acceptance/result-lifecycle-test.js b/tests/acceptance/result-lifecycle-test.js index e60e8a1f..0dd125fc 100644 --- a/tests/acceptance/result-lifecycle-test.js +++ b/tests/acceptance/result-lifecycle-test.js @@ -84,3 +84,111 @@ test('it lets you expand metadata in results', function(assert) { }); }); }); + +test('it lets swap between a row, tabular and advanced chart views when it is a raw query', function(assert) { + assert.expect(12); + + server = mockAPI(RESULTS.MULTIPLE, COLUMNS.BASIC); + + visit('/queries/new'); + click('.submit-button'); + click('.table-view'); + andThen(() => { + assert.equal(find('.records-charter').length, 0); + assert.equal(find('.pretty-json-container').length, 0); + assert.equal(find('.lt-body .lt-row .lt-cell').length, 9); + }); + click('.raw-view'); + andThen(() => { + assert.equal(find('.records-charter').length, 0); + assert.equal(find('.lt-body .lt-row .lt-cell').length, 0); + assert.equal(find('.pretty-json-container').length, 1); + }); + click('.chart-view'); + andThen(() => { + assert.equal(find('.lt-body .lt-row .lt-cell').length, 0); + assert.equal(find('.pretty-json-container').length, 0); + assert.equal(find('.records-charter').length, 1); + assert.equal(find('.pivot-table-container').length, 1); + assert.equal(find('.pvtUi').length, 1); + // Only pivot view + assert.equal(find('.records-chater .mode-toggle').length, 0); + }); +}); + +test('it lets swap between a row, tabular, simple and advanced chart views when it is not a raw query', function(assert) { + assert.expect(15); + + server = mockAPI(RESULTS.DISTRIBUTION, COLUMNS.BASIC); + + visit('/queries/new'); + click('.output-options #distribution'); + click('.output-container .distribution-point-options #points'); + selectChoose('.output-container .field-selection-container .field-selection', 'simple_column'); + fillIn('.output-container .distribution-type-points input', '0,0.2,1'); + click('.submit-button'); + + click('.raw-view'); + andThen(() => { + assert.equal(find('.records-charter').length, 0); + assert.equal(find('.lt-body .lt-row .lt-cell').length, 0); + assert.equal(find('.pretty-json-container').length, 1); + }); + click('.table-view'); + andThen(() => { + assert.equal(find('.records-charter').length, 0); + assert.equal(find('.pretty-json-container').length, 0); + assert.equal(find('.lt-body .lt-row .lt-cell').length, 9); + }); + click('.chart-view'); + andThen(() => { + assert.equal(find('.lt-body .lt-row .lt-cell').length, 0); + assert.equal(find('.pretty-json-container').length, 0); + assert.equal(find('.records-charter').length, 1); + assert.equal(find('.records-charter .mode-toggle').length, 1); + assert.ok(find('.mode-toggle .simple-view').hasClass('selected')); + assert.equal(find('.records-charter canvas').length, 1); + }); + click('.mode-toggle .advanced-view'); + andThen(() => { + assert.ok(find('.mode-toggle .advanced-view').hasClass('selected')); + assert.equal(find('.pivot-table-container').length, 1); + assert.equal(find('.pvtUi').length, 1); + }); +}); + +test('it saves and restores pivot table options', function(assert) { + assert.expect(7); + + server = mockAPI(RESULTS.DISTRIBUTION, COLUMNS.BASIC); + + visit('/queries/new'); + click('.output-options #distribution'); + click('.output-container .distribution-point-options #points'); + selectChoose('.output-container .field-selection-container .field-selection', 'simple_column'); + fillIn('.output-container .distribution-type-points input', '0,0.2,1'); + click('.submit-button'); + + click('.chart-view'); + click('.mode-toggle .advanced-view'); + andThen(() => { + assert.ok(find('.mode-toggle .advanced-view').hasClass('selected')); + assert.equal(find('.pivot-table-container').length, 1); + assert.equal(find('.pvtUi').length, 1); + assert.equal(find('.pvtUi select.pvtRenderer').val(), 'Table'); + find('.pivot-table-container select.pvtRenderer').val('Bar Chart').trigger('change'); + find('.pivot-table-container select.pvtAggregator').val('Sum').trigger('change'); + }); + visit('queries'); + andThen(() => { + assert.equal(find('.queries-table .query-results-entry .length-entry').text().trim(), '1 Results'); + }); + click('.queries-table .query-results-entry'); + click('.query-results-entry-popover .results-table .result-date-entry'); + click('.chart-view'); + click('.mode-toggle .advanced-view'); + andThen(() => { + assert.equal(find('.pvtUi select.pvtRenderer').val(), 'Bar Chart'); + assert.equal(find('.pvtUi select.pvtAggregator').val(), 'Sum'); + }); +}); diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js index 6f0d3dbc..dc537825 100644 --- a/tests/helpers/module-for-acceptance.js +++ b/tests/helpers/module-for-acceptance.js @@ -20,8 +20,7 @@ export default function(name, options = {}) { this.application = startApp(); if (options.suppressLogging) { - logger = Ember.Logger; - Ember.Logger = LOGGER; + [logger, Ember.Logger] = [Ember.Logger, LOGGER]; } if (options.beforeEach) { From 406675e58ce6230a45104a74b171abe8d2d7767d Mon Sep 17 00:00:00 2001 From: Akshai Sarma Date: Wed, 10 May 2017 16:50:36 -0700 Subject: [PATCH 9/9] Using computed properties instead of doing equalities --- app/models/result.js | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/app/models/result.js b/app/models/result.js index 3ac6c229..034c666e 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -27,32 +27,22 @@ export default DS.Model.extend({ querySnapshot: DS.attr(), pivotOptions: DS.attr('string'), - isRaw: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('RAW'); - }), + isRaw: Ember.computed.equal('querySnapshot.type', AGGREGATIONS.get('RAW')), + isCountDistinct: Ember.computed.equal('querySnapshot.type', AGGREGATIONS.get('COUNT_DISTINCT')), + isGroup: Ember.computed.equal('querySnapshot.type', AGGREGATIONS.get('GROUP')), + isDistribution: Ember.computed.equal('querySnapshot.type', AGGREGATIONS.get('DISTRIBUTION')), + isTopK: Ember.computed.equal('querySnapshot.type', AGGREGATIONS.get('TOP_K')), - isReallyRaw: Ember.computed('isRaw', 'querySnapshot', function() { + isReallyRaw: Ember.computed('isRaw', 'querySnapshot.projectionsSize', function() { return this.get('isRaw') && this.get('querySnapshot.projectionsSize') === 0; }), - isCountDistinct: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('COUNT_DISTINCT'); - }), - - isGroupAll: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('GROUP') && this.get('querySnapshot.groupsSize') === 0; - }), - - isGroupBy: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; - }), - - isDistribution: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('DISTRIBUTION'); + isGroupAll: Ember.computed('isGroup', 'querySnapshot.groupsSize', function() { + return this.get('isGroup') && this.get('querySnapshot.groupsSize') === 0; }), - isTopK: Ember.computed('querySnapshot', function() { - return this.get('querySnapshot.type') === AGGREGATIONS.get('TOP_K'); + isGroupBy: Ember.computed('isGroup', 'querySnapshot.metricsSize', 'querySnapshot.groupsSize', function() { + return this.get('isGroup') && this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; }), isSingleRow: Ember.computed.or('isCountDistinct', 'isGroupAll')