diff --git a/app/components/output-data-input.js b/app/components/output-data-input.js index d4b021b9..af136cea 100644 --- a/app/components/output-data-input.js +++ b/app/components/output-data-input.js @@ -41,26 +41,26 @@ export default Ember.Component.extend({ }), // Helper equalities for template - isRawAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('RAW')), - isGroupAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('GROUP')), - isCountDistinctAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('COUNT_DISTINCT')), - isDistributionAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('DISTRIBUTION')), - isTopKAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('TOP_K')), + isRawAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('RAW')).readOnly(), + isGroupAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('GROUP')).readOnly(), + isCountDistinctAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('COUNT_DISTINCT')).readOnly(), + isDistributionAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('DISTRIBUTION')).readOnly(), + isTopKAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('TOP_K')).readOnly(), - isSelectType: Ember.computed.equal('rawType', RAWS.get('SELECT')), - showRawSelections: Ember.computed.and('isRawAggregation', 'isSelectType'), + isSelectType: Ember.computed.equal('rawType', RAWS.get('SELECT')).readOnly(), + showRawSelections: Ember.computed.and('isRawAggregation', 'isSelectType').readOnly(), - isNumberOfPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('NUMBER')), - isPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('POINTS')), - isGeneratedPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('GENERATED')), + isNumberOfPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('NUMBER')).readOnly(), + isPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('POINTS')).readOnly(), + isGeneratedPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('GENERATED')).readOnly(), canDeleteProjections: Ember.computed('query.projections.[]', function() { return this.get('query.projections.length') > 1; - }), + }).readOnly(), canDeleteField: Ember.computed('query.aggregation.groups.[]', function() { return this.get('query.aggregation.groups.length') > 1; - }), + }).readOnly(), findOrDefault(valuePath, defaultValue) { let value = this.get(valuePath); diff --git a/app/components/pivot-table.js b/app/components/pivot-table.js new file mode 100644 index 00000000..6392e452 --- /dev/null +++ b/app/components/pivot-table.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + rows: null, + columns: null, + 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); + 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/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 new file mode 100644 index 00000000..40c44d7b --- /dev/null +++ b/app/components/records-charter.js @@ -0,0 +1,165 @@ +/* + * 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, + columns: null, + rows: null, + chartType: 'bar', + simpleMode: true, + + cannotModeSwitch: Ember.computed.alias('model.isRaw').readOnly(), + 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 = { }; + let rows = this.get('rows'); + this.get('columns').forEach(column => { + for (let row of rows) { + let value = row[column]; + if (!Ember.isEmpty(value)) { + typicalRow[column] = value; + break; + } + } + }); + return typicalRow; + }), + + independentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { + 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'))); + } + // Pick all string columns + return Ember.A(columns.filter(c => this.isType(sampleRow, c, 'string'))); + }), + + dependentColumns: Ember.computed('model', 'sampleRow', 'columns', function() { + let { columns, sampleRow } = this.getProperties('columns', 'sampleRow'); + let isDistribution = this.get('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 => this.isType(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)); + // valuesList won't be empty because all non-Raw aggregations will have at least one string field + return this.zip(valuesList); + }), + + datasets: Ember.computed('dependentColumns', 'rows', function() { + let dependentColumns = this.get('dependentColumns'); + let rows = this.get('rows'); + return dependentColumns.map((c, i) => this.dataset(c, rows, i)); + }), + + data: Ember.computed('labels', 'datasets', function() { + return { + labels: this.get('labels'), + datasets: this.get('datasets') + }; + }), + + dataset(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 color = this.randomColor(); + return new Array(size).fill(color); + }, + + 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 = '/') { + 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) { + return rows.map(row => row[field]); + }, + + actions: { + toggleMode() { + this.toggleProperty('simpleMode'); + }, + + saveOptions(options) { + let model = this.get('model'); + model.set('pivotOptions', JSON.stringify(options)); + model.save(); + } + } +}); diff --git a/app/components/records-viewer.js b/app/components/records-viewer.js index 29eff229..3fe6f89f 100644 --- a/app/components/records-viewer.js +++ b/app/components/records-viewer.js @@ -8,10 +8,17 @@ 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.not('model.isSingleRow').readOnly(), + columns: Ember.computed('records', function() { return Ember.A(this.extractUniqueColumns(this.get('records'))); }).readOnly(), @@ -98,13 +105,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/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/app/initializers/startup.js b/app/initializers/startup.js index 105ac43e..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.merge(settings, decodedSettings); + let settings = this.deepMergeSettings(decodedSettings); application.register('settings:main', Ember.Object.create(settings), { instantiate: false }); application.inject('service', 'settings', 'settings:main'); @@ -27,24 +25,17 @@ 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; + 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/instance-initializers/migrations.js b/app/instance-initializers/migrations.js new file mode 100644 index 00000000..b03ed26c --- /dev/null +++ b/app/instance-initializers/migrations.js @@ -0,0 +1,41 @@ +/* + * 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 function initialize(application) { + let settings = application.lookup('settings:main'); + let migrations = settings.get('migrations'); + let version = settings.get('modelVersion'); + + 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. + */ +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/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..034c666e 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,27 @@ 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(), + pivotOptions: DS.attr('string'), + + 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.projectionsSize', function() { + return this.get('isRaw') && this.get('querySnapshot.projectionsSize') === 0; + }), + + isGroupAll: Ember.computed('isGroup', 'querySnapshot.groupsSize', function() { + return this.get('isGroup') && this.get('querySnapshot.groupsSize') === 0; + }), + + 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') }); diff --git a/app/routes/query.js b/app/routes/query.js index 3e0efde1..363e24bd 100644 --- a/app/routes/query.js +++ b/app/routes/query.js @@ -7,18 +7,11 @@ import Ember from 'ember'; export default Ember.Route.extend({ querier: Ember.inject.service(), + queryManager: Ember.inject.service(), 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, - 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..46519436 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/pivot-table.scss b/app/styles/components/pivot-table.scss new file mode 100644 index 00000000..54c57b97 --- /dev/null +++ b/app/styles/components/pivot-table.scss @@ -0,0 +1,15 @@ +/* + * 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. + */ +.pivot-table-container { + width: 100%; + overflow: auto; + font-size: $font-size-regular-small; + font-family: $font-family-regular; + + .pvtUi { + margin: auto; + } +} diff --git a/app/styles/components/query-builder.scss b/app/styles/components/query-builder.scss index 6e5291e6..37eff04d 100644 --- a/app/styles/components/query-builder.scss +++ b/app/styles/components/query-builder.scss @@ -6,9 +6,9 @@ // Overrides to the QueryBuilder styling .builder { - $logical-button-active-color: #6E54A2; - $logical-button-inactive-hover-color: #E9E9E9; - $logical-button-inactive-color: #D3D3D3; + $logical-button-active-color: $special-button-active-color; + $logical-button-inactive-hover-color: $special-button-inactive-hover-color; + $logical-button-inactive-color: $special-button-inactive-color; $rule-background-color: $background-box-color; $rule-background-hover-color: $background-box-hover-color; $rule-background-error-color: $background-box-error-color; @@ -197,7 +197,6 @@ select { appearance: none; -ms-appearance: none; - -o-appearance: none; -moz-appearance: none; -webkit-appearance: none; &::-ms-expand { 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-charter.scss b/app/styles/components/records-charter.scss new file mode 100644 index 00000000..fe42448e --- /dev/null +++ b/app/styles/components/records-charter.scss @@ -0,0 +1,50 @@ +/* + * 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. + */ +.records-charter { + $mode-active-color: $special-button-active-color; + $mode-inactive-hover-color: $special-button-inactive-hover-color; + $mode-inactive-color: $special-button-inactive-color; + + margin: auto; + + .mode-toggle { + display: flex; + justify-content: center; + margin-top: 20px; + margin-bottom: 40px; + + .mode { + width: 70px; + padding: 0; + background-color: $mode-inactive-color; + color: $white; + height: 28px; + font-weight: $font-weight-regular; + font-family: $font-family-regular; + font-size: $font-size-regular-small; + &.selected { + background-color: $mode-active-color; + } + &:hover:not(.selected) { + background-color: $mode-inactive-hover-color + } + &:focus { + outline: 0; + } + // Two buttons, first is curved to left, second to the right + &.simple-view { + border-radius: $button-radius-large 0 0 $button-radius-large; + } + &.advanced-view { + border-radius: 0 $button-radius-large $button-radius-large 0; + } + } + } + + .visual-container { + @import 'pivot-table'; + } +} diff --git a/app/styles/components/records-viewer.scss b/app/styles/components/records-viewer.scss index 1876388d..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"; @@ -29,6 +35,9 @@ } .view-controls { + display: flex; + justify-content: flex-end; + width: 180px; .view-control { margin-left: 15px; border: 2px solid $secondary-button-color; @@ -40,26 +49,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 +119,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 +139,8 @@ } } .records-display { + @import "pretty-json"; @import "records-table"; + @import "records-charter"; } -} \ No newline at end of file +} diff --git a/app/styles/components/validated-field-selection.scss b/app/styles/components/validated-field-selection.scss index 6e83a53d..3b557328 100644 --- a/app/styles/components/validated-field-selection.scss +++ b/app/styles/components/validated-field-selection.scss @@ -9,8 +9,8 @@ background-color: $background-box-color; padding: $spacing-vertical 0 $spacing-vertical 0; margin: 4px 0; - border: 1px solid $white; border-radius: 5px; + outline: none; &:hover { background-color: $background-box-hover-color; 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/styles/variables.scss b/app/styles/variables.scss index d9da782b..e838da10 100644 --- a/app/styles/variables.scss +++ b/app/styles/variables.scss @@ -37,6 +37,10 @@ $button-radius-regular: 4px; $button-radius-large: 5px; $button-radius-circle: 30px; +$special-button-active-color: #6E54A2; +$special-button-inactive-hover-color: #E9E9E9; +$special-button-inactive-color: #D3D3D3; + $font-size-small: 11px; $font-size-regular-small: 13px; $font-size-regular: 15px; 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}}