diff --git a/.eslintrc.js b/.eslintrc.js index b2aa738c..91cfd97d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,6 +81,7 @@ module.exports = { 'no-unneeded-ternary': 2, 'no-whitespace-before-property': 2, 'object-curly-spacing': [2, 'always'], + 'semi': 2, 'semi-spacing': 2, 'space-before-blocks': 2, 'space-before-function-paren': [2, 'never'], diff --git a/app/components/pivot-table.js b/app/components/pivot-table.js index b472f9cd..ca012a98 100644 --- a/app/components/pivot-table.js +++ b/app/components/pivot-table.js @@ -1,10 +1,15 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ import { computed } from '@ember/object'; import $ from 'jquery'; import Component from '@ember/component'; +import { debounce } from '@ember/runloop'; export default Component.extend({ rows: null, - columns: null, initialOptions: null, init() { @@ -29,6 +34,15 @@ export default Component.extend({ didInsertElement() { this._super(...arguments); + this.insertPivotTable(); + }, + + didUpdateAttrs() { + this._super(...arguments); + debounce(this, this.insertPivotTable, 500, true); + }, + + insertPivotTable() { let { rows, options } = this.getProperties('rows', 'options'); this.$('.pivot-table-container').pivotUI(rows, options); }, diff --git a/app/components/pretty-json.js b/app/components/pretty-json.js index 3591fc5b..ed64dd74 100644 --- a/app/components/pretty-json.js +++ b/app/components/pretty-json.js @@ -13,9 +13,13 @@ export default Component.extend({ data: null, defaultLevels: 2, - didInsertElement() { + didRender() { this._super(...arguments); + this.$().empty().append(this.getRenderData()); + }, + + getRenderData() { let formatter = new JSONFormatter(this.get('data'), this.get('defaultLevels'), { hoverPreviewEnabled: true }); - this.$().append(formatter.render()); + return formatter.render(); } }); diff --git a/app/components/queries-table.js b/app/components/queries-table.js index 8f8cbc5e..c7c730a7 100644 --- a/app/components/queries-table.js +++ b/app/components/queries-table.js @@ -14,7 +14,6 @@ export default Component.extend(PaginatedTable, { classNames: ['queries-table'], queries: null, pageSize: 10, - isFixed: true, extractors: EmberObject.create({ name(row) { let name = row.get('name'); diff --git a/app/components/query-information.js b/app/components/query-information.js new file mode 100644 index 00000000..f9f2c12f --- /dev/null +++ b/app/components/query-information.js @@ -0,0 +1,30 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; + +export default Component.extend({ + querier: service(), + query: null, + querySnapshot: null, + + isRunningQuery: alias('querier.isRunningQuery').readOnly(), + + actions: { + cancelClick() { + this.sendAction('cancelClick'); + }, + + reRunClick(query) { + this.sendAction('reRunClick', query); + }, + + queryClick(query) { + this.sendAction('queryClick', query); + } + } +}); diff --git a/app/components/query-input.js b/app/components/query-input.js index 9adb77d0..a19c6926 100644 --- a/app/components/query-input.js +++ b/app/components/query-input.js @@ -11,7 +11,6 @@ import Component from '@ember/component'; import { isEqual } from '@ember/utils'; import { SUBFIELD_SEPARATOR } from 'bullet-ui/models/column'; import { AGGREGATIONS } from 'bullet-ui/models/aggregation'; -import { EMIT_TYPES } from 'bullet-ui/models/window'; import BuilderAdapter from 'bullet-ui/mixins/builder-adapter'; export default Component.extend(BuilderAdapter, { @@ -30,7 +29,6 @@ export default Component.extend(BuilderAdapter, { scroller: service(), schema: null, isListening: false, - listenDuration: 0, hasError: false, hasSaved: false, @@ -39,9 +37,8 @@ export default Component.extend(BuilderAdapter, { return this.builderFilters(schema); }).readOnly(), - showAggregationSize: computed('query.{aggregation.type,window.emit.type,isWindowless}', function() { - return isEqual(this.get('query.aggregation.type'), AGGREGATIONS.get('RAW')) && - (this.get('query.isWindowless') || isEqual(this.get('query.window.emit.type'), EMIT_TYPES.get('TIME'))); + showAggregationSize: computed('query.{aggregation.type,isWindowless}', function() { + return isEqual(this.get('query.aggregation.type'), AGGREGATIONS.get('RAW')) && this.get('query.isWindowless'); }), didInsertElement() { @@ -79,7 +76,6 @@ export default Component.extend(BuilderAdapter, { reset() { this.setProperties({ isListening: false, - listenDuration: 0, hasError: false, hasSaved: false }); @@ -119,8 +115,7 @@ export default Component.extend(BuilderAdapter, { this.save().then(() => { this.setProperties({ isListening: true, - hasSaved: true, - listenDuration: this.get('query.duration') * 1000 + hasSaved: true }); this.$(this.get('queryBuilderInputs')).attr('disabled', true); this.sendAction('fireQuery'); diff --git a/app/components/records-charter.js b/app/components/records-charter.js index 3250758b..faed3d12 100644 --- a/app/components/records-charter.js +++ b/app/components/records-charter.js @@ -16,14 +16,16 @@ export default Component.extend({ columns: null, rows: null, chartType: 'bar', + config: null, simpleMode: true, notSimpleMode: not('simpleMode').readOnly(), - cannotModeSwitch: alias('model.isRaw').readOnly(), + cannotModeSwitch: alias('config.isRaw').readOnly(), canModeSwitch: not('cannotModeSwitch').readOnly(), pivotMode: or('notSimpleMode', 'cannotModeSwitch').readOnly(), - pivotOptions: computed('model.pivotOptions', function() { - return JSON.parse(this.get('model.pivotOptions')); + pivotOptions: computed('config.pivotOptions', function() { + let options = this.get('config.pivotOptions') || '{}'; + return JSON.parse(options); }).readOnly(), sampleRow: computed('rows', 'columns', function() { @@ -41,9 +43,9 @@ export default Component.extend({ return typicalRow; }), - independentColumns: computed('model', 'sampleRow', 'columns', function() { + independentColumns: computed('config', 'sampleRow', 'columns', function() { let { columns, sampleRow } = this.getProperties('columns', 'sampleRow'); - let isDistribution = this.get('model.isDistribution'); + let isDistribution = this.get('config.isDistribution'); if (isDistribution) { return A(columns.filter(c => this.isAny(c, 'Quantile', 'Range'))); } @@ -51,9 +53,9 @@ export default Component.extend({ return A(columns.filter(c => this.isType(sampleRow, c, 'string'))); }), - dependentColumns: computed('model', 'sampleRow', 'columns', function() { + dependentColumns: computed('config', 'sampleRow', 'columns', function() { let { columns, sampleRow } = this.getProperties('columns', 'sampleRow'); - let isDistribution = this.get('model.isDistribution'); + let isDistribution = this.get('config.isDistribution'); if (isDistribution) { return A(columns.filter(c => this.isAny(c, 'Count', 'Value', 'Probability'))); } diff --git a/app/components/records-table.js b/app/components/records-table.js index 2aec0fd6..2bbca61c 100644 --- a/app/components/records-table.js +++ b/app/components/records-table.js @@ -7,17 +7,21 @@ import EmberObject, { computed } from '@ember/object'; import Component from '@ember/component'; import Table from 'ember-light-table'; import PaginatedTable from 'bullet-ui/mixins/paginated-table'; +import { isNone } from '@ember/utils'; export default Component.extend(PaginatedTable, { classNames: ['records-table'], columnNames: null, cellComponent: 'cells/record-entry', pageSize: 10, - isFixed: true, + appendMode: false, + + sortedByColumn: null, + // Use natural types in the results useDefaultStringExtractor: false, - rawRows: null, + table: null, columns: computed('columnNames', function() { let names = this.get('columnNames'); @@ -33,8 +37,18 @@ export default Component.extend(PaginatedTable, { init() { this._super(...arguments); this.set('table', new Table(this.get('columns'))); + this.set('sortColumn', null); + }, + didReceiveAttrs() { + this._super(...arguments); this.set('rows', this.get('rawRows').map(row => EmberObject.create(row))); - this.addPages(1); + let sortColumn = this.get('sortColumn'); + let hasSortedColumn = !isNone(sortColumn); + this.reset(hasSortedColumn); + if (hasSortedColumn) { + this.sortBy(sortColumn.valuePath, sortColumn.ascending ? 'ascending' : 'descending'); + } + this.addPages(); } }); diff --git a/app/components/records-viewer.js b/app/components/records-viewer.js index c32b4752..34e21116 100644 --- a/app/components/records-viewer.js +++ b/app/components/records-viewer.js @@ -14,16 +14,17 @@ import Component from '@ember/component'; export default Component.extend({ fileSaver: service(), classNames: ['records-viewer'], + aggregateMode: false, showRawData: false, showTable: false, showChart: false, - // Copy of the model - model: null, + config: null, metadata: null, records: null, fileName: 'results', + model: null, - enableCharting: not('model.isSingleRow').readOnly(), + enableCharting: not('config.isSingleRow').readOnly(), columns: computed('records', function() { return A(this.extractUniqueColumns(this.get('records'))); @@ -50,6 +51,15 @@ export default Component.extend({ return this.makeCSVString(columns, rows); }).readOnly(), + init() { + this._super(...arguments); + if (this.get('config.isReallyRaw')) { + this.set('showRawData', true); + } else { + this.set('showTable', true); + } + }, + extractUniqueColumns(records) { let columns = new Set(); records.forEach(record => { diff --git a/app/components/result-viewer.js b/app/components/result-viewer.js new file mode 100644 index 00000000..051c1bd6 --- /dev/null +++ b/app/components/result-viewer.js @@ -0,0 +1,99 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { alias, and, not } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { isNone } from '@ember/utils'; + +export default Component.extend({ + classNames: ['result-viewer'], + querier: service(), + + query: null, + result: null, + selectedWindow: null, + autoUpdate: true, + // Tweaks the time for the window duration by this to adjust for Ember scheduling delays + jitter: -300, + + isRunningQuery: alias('querier.isRunningQuery').readOnly(), + isRaw: alias('result.isRaw').readOnly(), + errorWindow: alias('result.errorWindow').readOnly(), + hasData: alias('result.hasData').readOnly(), + numberOfWindows: alias('result.windows.length').readOnly(), + windowEmitEvery: alias('query.window.emit.every').readOnly(), + isTimeWindow: alias('query.window.isTimeBased').readOnly(), + isRecordWindow: not('isTimeWindow').readOnly(), + isRawRecordWindow: and('isRecordWindow', 'isRaw').readOnly(), + aggregateMode: alias('isRawRecordWindow').readOnly(), + + showAutoUpdate: computed('hasError', 'aggregateMode', 'hasData', function() { + return this.get('hasData') && !this.get('hasError') && !this.get('aggregateMode'); + }), + + hasError: computed('errorWindow', function() { + return !isNone(this.get('errorWindow')); + }).readOnly(), + + metadata: computed('hasError', 'autoUpdate', 'selectedWindow', 'result.windows.[]', function() { + return this.get('hasError') ? this.get('errorWindow.metadata') : this.getSelectedWindow('metadata'); + }).readOnly(), + + records: computed('autoUpdate', 'aggregateMode', 'selectedWindow', 'result.windows.[]', function() { + return this.get('aggregateMode') ? this.getAllWindowRecords() : this.getSelectedWindow('records'); + }).readOnly(), + + queryDuration: computed('query.duration', function() { + return this.get('query.duration') * 1000; + }).readOnly(), + + windowDuration: computed('windowEmitEvery', function() { + return this.get('jitter') + (this.get('windowEmitEvery') * 1000); + }).readOnly(), + + config: computed('result.{isRaw,isReallyRaw,isDistribution,isSingleRow}', function() { + return { + isRaw: this.get('result.isRaw'), + isReallyRaw: this.get('result.isReallyRaw'), + isDistribution: this.get('result.isDistribution'), + isSingleRow: this.get('result.isSingleRow'), + pivotOptions: this.get('result.pivotOptions') + }; + }).readOnly(), + + didReceiveAttrs() { + this._super(...arguments); + this.set('selectedWindow', null); + this.set('autoUpdate', true); + }, + + getSelectedWindow(property) { + let windowProperty = this.get(`result.windows.lastObject.${property}`); + if (!this.get('autoUpdate')) { + let selectedWindow = this.get('selectedWindow'); + windowProperty = isNone(selectedWindow) ? windowProperty : selectedWindow[property]; + } + return windowProperty; + }, + + getAllWindowRecords() { + return this.get('result.windows').getEach('records').reduce((p, c) => p.concat(c), []); + }, + + actions: { + changeWindow(selectedWindow) { + this.set('selectedWindow', selectedWindow); + this.set('autoUpdate', false); + }, + + changeAutoUpdate(autoUpdate) { + // Turn On => reset selectedWindow. Turn Off => Last window + this.set('selectedWindow', autoUpdate ? null : this.get('result.windows.lastObject')); + this.set('autoUpdate', autoUpdate); + } + } +}); diff --git a/app/components/result-window-placeholder.js b/app/components/result-window-placeholder.js new file mode 100644 index 00000000..d39ee739 --- /dev/null +++ b/app/components/result-window-placeholder.js @@ -0,0 +1,12 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['result-window-placeholder'], + tagName: 'span', + windowCount: 0 +}); diff --git a/app/components/results-table.js b/app/components/results-table.js index 6bec3e51..001486e0 100644 --- a/app/components/results-table.js +++ b/app/components/results-table.js @@ -13,15 +13,14 @@ export default Component.extend(PaginatedTable, { classNames: ['results-table'], results: null, pageSize: 5, - isFixed: true, extractors: EmberObject.create({ created(row) { return row.get('created'); }, - records(row) { - return row.get('records.length'); + windows(row) { + return row.get('windows.length'); } }), @@ -31,7 +30,7 @@ export default Component.extend(PaginatedTable, { columns: A([ { label: 'Date', valuePath: 'created', width: '150px', cellComponent: 'cells/result-date-entry' }, - { label: '# Records', valuePath: 'records', width: '80px', cellComponent: 'cells/result-number-entry' } + { label: '# Windows', valuePath: 'windows', width: '80px', cellComponent: 'cells/result-number-entry' } ]), init() { diff --git a/app/components/schema-table.js b/app/components/schema-table.js index 3a137881..625f6c02 100644 --- a/app/components/schema-table.js +++ b/app/components/schema-table.js @@ -15,7 +15,6 @@ export default Component.extend(PaginatedTable, { classNameBindings: ['isNested'], queries: null, pageSize: 10, - isFixed: true, /** * If set to true, the table behaves as a nested table without headers and will not expand further. Meant to * be used when the table is nested within itself. diff --git a/app/components/timed-progress-bar.js b/app/components/timed-progress-bar.js index bcfbd615..90d2e6d8 100644 --- a/app/components/timed-progress-bar.js +++ b/app/components/timed-progress-bar.js @@ -4,42 +4,81 @@ * See the LICENSE file associated with the project for terms. */ import { cancel, later } from '@ember/runloop'; -import { htmlSafe } from '@ember/string'; import { computed } from '@ember/object'; import Component from '@ember/component'; export default Component.extend({ + // Component API duration: 10000, active: false, - updateInterval: 500, + updateInterval: 200, + // The value doesn't matter. Using this as a 'observer' to trigger timing again. + retriggerOnChangeIn: null, + + // Ember Progress Bar configuration + shape: 'Circle', + strokeWidth: 8.0, + color: '#555', + doneColor: '#21D87D', + trailColor: '#eee', + // https://github.com/jeremyckahn/shifty/blob/e13d78f887b3783b6d93e501d26cc37a9fa2d206/src/easing-functions.js + easing: 'linear', + animationDuration: 200, + useStep: true, + + // Private component configuration finished: undefined, startTime: null, endTime: null, runTime: null, percentNow: 0, futureTimer: null, + timingDone: true, + + showDone: computed('timingDone', 'active', function() { + return this.get('timingDone') || !this.get('active'); + }).readOnly(), - progressWidth: computed('percentNow', function() { - let percent = this.get('percentNow'); - return htmlSafe(`width: ${percent}%`); + progress: computed('percentNow', 'timingDone', function() { + // Deliberately based on timingDone instead of showDone to show the progress percent at which the query became done + return this.get('timingDone') ? 1 : this.get('percentNow'); + }).readOnly(), + + strokeColor: computed('showDone', 'color', function() { + return this.get('showDone') ? this.get('doneColor') : this.get('color'); }), - valueNow: computed('percentNow', 'timingDone', function() { - if (this.get('timingDone')) { - return 'Collecting results...'; + options: computed('strokeWidth', 'strokeColor', 'trailColor', function() { + let { strokeWidth, strokeColor, trailColor, easing, animationDuration, useStep } + = this.getProperties('strokeWidth', 'strokeColor', 'trailColor', 'easing', 'animationDuration', 'useStep'); + + let options = { + strokeWidth, + color: strokeColor, + trailColor, + easing, + duration: animationDuration + }; + if (useStep) { + options.step = this.get('step'); } - return `${this.get('percentNow')}%`; - }), + return options; + }).readOnly(), didReceiveAttrs() { this._super(...arguments); + this.destroyTimer(); // Don't do any timing unless active. Also any changes with active true should restart timer. if (this.get('active')) { - this.destroyTimer(); this.startTiming(); } }, + step(state, path) { + let displayText = (path.value() * 100).toFixed(0); + path.setText(`${displayText}%`); + }, + destroyTimer() { // Docs say this should return false or undefined if it doesn't exist cancel(this.get('futureTimer')); @@ -52,7 +91,7 @@ export default Component.extend({ startTiming() { let now = Date.now(); let magnitude = parseFloat(this.get('duration')); - magnitude = magnitude <= 0 ? 100 : magnitude; + magnitude = magnitude <= 0 ? 1 : magnitude; let end = new Date(now + magnitude).getTime(); this.setProperties({ @@ -60,6 +99,8 @@ export default Component.extend({ endTime: end, duration: magnitude, runTime: end - now, + percentNow: 0.0, + timingDone: false, futureTimer: later(this, this.timer, this.get('updateInterval')) }); }, @@ -67,7 +108,7 @@ export default Component.extend({ timer() { let timeNow = Date.now(); let delta = (timeNow - this.get('startTime')) / this.get('runTime'); - this.set('percentNow', Math.min(Math.floor(100 * delta), 100)); + this.set('percentNow', Math.min(delta, 1)); if (timeNow >= this.get('endTime')) { this.set('timingDone', true); this.sendAction('finished'); diff --git a/app/components/window-input.js b/app/components/window-input.js index 661c4648..de5d97ec 100644 --- a/app/components/window-input.js +++ b/app/components/window-input.js @@ -31,12 +31,13 @@ export default Component.extend({ }), // Helper equalities for template - everyForRecordBasedWindow: alias('settings.defaultValues.everyForRecordBasedWindow').readOnly(), - everyForTimeBasedWindow: alias('settings.defaultValues.everyForTimeBasedWindow').readOnly(), + defaultEveryForRecordWindow: alias('settings.defaultValues.everyForRecordBasedWindow').readOnly(), + defaultEveryForTimeWindow: alias('settings.defaultValues.everyForTimeBasedWindow').readOnly(), isWindowless: alias('query.isWindowless').readOnly(), isTimeBasedWindow: equal('emitType', EMIT_TYPES.get('TIME')).readOnly(), isRecordBasedWindow: equal('emitType', EMIT_TYPES.get('RECORD')).readOnly(), isRawAggregation: equal('query.aggregation.type', AGGREGATIONS.get('RAW')).readOnly(), + recordBasedWindowDisabled: computed('isRawAggregation', 'disabled', function() { return this.get('disabled') || !this.get('isRawAggregation'); }).readOnly(), @@ -45,8 +46,9 @@ export default Component.extend({ allIncludeTypeDisabled: computed('isRawAggregation', 'includeDisabled', function() { return this.get('includeDisabled') || this.get('isRawAggregation'); }).readOnly(), + everyFieldName: computed('isRecordBasedWindow', function() { - return this.get('isRecordBasedWindow') ? 'every (records)' : 'every (seconds)'; + return `Frequency (${this.get('isRecordBasedWindow') ? 'records' : 'seconds'})`; }).readOnly(), replaceWindow(emitType, emitEvery, includeType) { @@ -59,7 +61,7 @@ export default Component.extend({ return this.get('queryManager').addWindow(this.get('query')); }, - removeWindow() { + deleteWindow() { return this.get('queryManager').deleteWindow(this.get('query')); }, @@ -67,9 +69,9 @@ export default Component.extend({ changeEmitType(emitType) { if (isEqual(emitType, EMIT_TYPES.get('RECORD'))) { this.set('includeType', INCLUDE_TYPES.get('WINDOW')); - this.replaceWindow(emitType, this.get('everyForRecordBasedWindow'), INCLUDE_TYPES.get('WINDOW')); + this.replaceWindow(emitType, this.get('defaultEveryForRecordWindow'), INCLUDE_TYPES.get('WINDOW')); } else { - this.replaceWindow(emitType, this.get('everyForTimeBasedWindow'), this.get('includeType')); + this.replaceWindow(emitType, this.get('defaultEveryForTimeWindow'), this.get('includeType')); } }, @@ -81,8 +83,8 @@ export default Component.extend({ this.addWindow(); }, - removeWindow() { - this.removeWindow(); + deleteWindow() { + this.deleteWindow(); } } }); diff --git a/app/components/result-metadata.js b/app/components/window-metadata.js similarity index 94% rename from app/components/result-metadata.js rename to app/components/window-metadata.js index d0027ed0..14f28924 100644 --- a/app/components/result-metadata.js +++ b/app/components/window-metadata.js @@ -7,7 +7,7 @@ import { computed } from '@ember/object'; import Component from '@ember/component'; export default Component.extend({ - classNames: ['result-metadata'], + classNames: ['window-metadata'], classNameBindings: ['expanded:is-expanded'], expanded: false, expansionIconClasses: computed('expanded', function() { diff --git a/app/mixins/paginated-table.js b/app/mixins/paginated-table.js index 3eb4fed3..c3634739 100644 --- a/app/mixins/paginated-table.js +++ b/app/mixins/paginated-table.js @@ -14,6 +14,8 @@ export default Mixin.create({ firstNewRow: 0, extractors: null, useDefaultStringExtractor: true, + sortColumn: null, + appendMode: false, numberOfRows: computed('rows.[]', function() { return this.get('rows.length'); @@ -79,15 +81,19 @@ export default Mixin.create({ }); }, - reset() { + reset(forceReset = false) { let table = this.get('table'); - table.setRows([]); - this.set('firstNewRow', 0); + // Reset table if not in appendMode + if (!this.get('appendMode') || forceReset) { + table.setRows([]); + this.set('firstNewRow', 0); + } }, actions: { onColumnClick(column) { - this.reset(); + this.set('sortColumn', column); + this.reset(true); this.sortBy(column.valuePath, column.ascending ? 'ascending' : 'descending'); this.addPages(); }, diff --git a/app/mixins/queryable.js b/app/mixins/queryable.js new file mode 100644 index 00000000..65bc7c85 --- /dev/null +++ b/app/mixins/queryable.js @@ -0,0 +1,41 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +import Mixin from '@ember/object/mixin'; + +export default Mixin.create({ + resultHandler(routeContext) { + routeContext.transitionTo('result', routeContext.get('savedResult.id')); + }, + + errorHandler(error, routeContext) { + console.error(error); // eslint-disable-line no-console + routeContext.transitionTo('errored'); + }, + + windowHandler(message, routeContext) { + routeContext.get('queryManager').addSegment(routeContext.get('savedResult'), message); + }, + + submitQuery(query, result, routeContext) { + let handlers = { + success: this.resultHandler, + error: this.errorHandler, + message: this.windowHandler + }; + routeContext.set('savedResult', result); + routeContext.get('querier').send(query, handlers, routeContext); + }, + + lateSubmitQuery(query, routeContext) { + let handlers = { + success: () => { }, + error: this.errorHandler, + message: this.windowHandler + }; + // savedResult already exists and points to result + routeContext.get('querier').send(query, handlers, routeContext); + } +}); diff --git a/app/models/query.js b/app/models/query.js index c5578d6c..71df3cb5 100644 --- a/app/models/query.js +++ b/app/models/query.js @@ -7,6 +7,7 @@ import { isEmpty, isEqual } from '@ember/utils'; import { A } from '@ember/array'; import { computed } from '@ember/object'; import DS from 'ember-data'; +import { pluralize } from 'ember-inflector'; import { validator, buildValidations } from 'ember-cp-validations'; import { AGGREGATIONS } from 'bullet-ui/models/aggregation'; import { EMIT_TYPES, INCLUDE_TYPES } from 'bullet-ui/models/window'; @@ -53,9 +54,9 @@ export default DS.Model.extend(Validations, { isWindowless: computed('window', function() { return isEmpty(this.get('window.id')); - }), + }).readOnly(), - hasUnsavedFields: computed('projections.@each.field', 'aggregation.groups.@each.field', function() { + hasUnsavedFields: computed('projections.@each.name', 'aggregation.groups.@each.name', function() { let projections = this.getWithDefault('projections', A()); let groups = this.getWithDefault('aggregation.groups', A()); return this.hasNoName(projections) || this.hasNoName(groups); @@ -72,7 +73,7 @@ export default DS.Model.extend(Validations, { groupsSummary: computed('aggregation.groups.@each.name', function() { return this.summarizeFieldLike(this.get('aggregation.groups')); - }), + }).readOnly(), metricsSummary: computed('aggregation.metrics.@each.{type,name}', function() { let metrics = this.getWithDefault('aggregation.metrics', A()); @@ -85,7 +86,7 @@ export default DS.Model.extend(Validations, { } return isEmpty(name) ? `${type}(${field})` : name; }).join(', '); - }), + }).readOnly(), aggregationSummary: computed('aggregation.type', 'aggregation.attributes.{type,newName,threshold}', 'groupsSummary', 'metricsSummary', function() { let type = this.get('aggregation.type'); @@ -116,7 +117,7 @@ export default DS.Model.extend(Validations, { return metricsSummary; } return `${groupsSummary}, ${metricsSummary}`; - }), + }).readOnly(), fieldsSummary: computed('projectionsSummary', 'aggregationSummary', function() { let projectionsSummary = this.get('projectionsSummary'); @@ -126,7 +127,7 @@ export default DS.Model.extend(Validations, { return isEmpty(projectionsSummary) ? 'All' : projectionsSummary; } return this.get('aggregationSummary'); - }), + }).readOnly(), windowSummary: computed('isWindowless', 'window.{emit.type,emit.every,include.type}', function() { if (this.get('isWindowless')) { @@ -135,8 +136,8 @@ export default DS.Model.extend(Validations, { let emitType = this.get('window.emit.type'); let emitEvery = this.get('window.emit.every'); let includeType = this.get('window.include.type'); - return `Every ${emitEvery} ${isEqual(emitType, EMIT_TYPES.get('TIME')) ? 'seconds' : 'records'} ${isEqual(includeType, INCLUDE_TYPES.get('ALL')) ? ', Cumulative' : ''}`; - }), + return `Every ${emitEvery} ${this.getEmitUnit(emitType, emitEvery)}${this.getIncludeType(includeType)}`; + }).readOnly(), latestResult: computed('results.[]', function() { let results = this.get('results'); @@ -162,5 +163,14 @@ export default DS.Model.extend(Validations, { hasNoName(fieldLike) { return isEmpty(fieldLike) ? false : fieldLike.any(f => !isEmpty(f.get('field')) && isEmpty(f.get('name'))); + }, + + getEmitUnit(emitType, emitEvery) { + let unit = isEqual(emitType, EMIT_TYPES.get('TIME')) ? 'second' : 'record'; + return Number(emitEvery) === 1 ? unit : pluralize(unit); + }, + + getIncludeType(includeType) { + return isEqual(includeType, INCLUDE_TYPES.get('ALL')) ? ', Cumulative' : ''; } }); diff --git a/app/models/result.js b/app/models/result.js index 825b107e..69b6917a 100644 --- a/app/models/result.js +++ b/app/models/result.js @@ -3,8 +3,10 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -import { equal, or, alias } from '@ember/object/computed'; +import { equal, or } from '@ember/object/computed'; +import { isEmpty, isNone } from '@ember/utils'; import { computed } from '@ember/object'; +import { A } from '@ember/array'; import DS from 'ember-data'; import { AGGREGATIONS } from 'bullet-ui/models/aggregation'; @@ -15,32 +17,42 @@ export default DS.Model.extend({ } }), query: DS.belongsTo('query', { autoSave: true }), - // TODO: change it to async: true when metadata and records computed properties are removed. - segments: DS.hasMany('segment', { dependent: 'destroy' }), + + // Not using hasMany to handle extremely high volume (rate-limited) results. + windows: DS.attr('window-array', { + defaultValue() { + return A(); + } + }), + pivotOptions: DS.attr('string'), querySnapshot: DS.attr(), - // TODO: Remove them after result page has been changed. - metadata: alias('segments.lastObject.metadata'), - records: alias('segments.lastObject.records'), - - isRaw: equal('querySnapshot.type', AGGREGATIONS.get('RAW')), - isCountDistinct: equal('querySnapshot.type', AGGREGATIONS.get('COUNT_DISTINCT')), - isGroup: equal('querySnapshot.type', AGGREGATIONS.get('GROUP')), - isDistribution: equal('querySnapshot.type', AGGREGATIONS.get('DISTRIBUTION')), - isTopK: equal('querySnapshot.type', AGGREGATIONS.get('TOP_K')), + isRaw: equal('querySnapshot.type', AGGREGATIONS.get('RAW')).readOnly(), + isCountDistinct: equal('querySnapshot.type', AGGREGATIONS.get('COUNT_DISTINCT')).readOnly(), + isGroup: equal('querySnapshot.type', AGGREGATIONS.get('GROUP')).readOnly(), + isDistribution: equal('querySnapshot.type', AGGREGATIONS.get('DISTRIBUTION')).readOnly(), + isTopK: equal('querySnapshot.type', AGGREGATIONS.get('TOP_K')).readOnly(), isReallyRaw: computed('isRaw', 'querySnapshot.projectionsSize', function() { return this.get('isRaw') && this.get('querySnapshot.projectionsSize') === 0; - }), + }).readOnly(), isGroupAll: computed('isGroup', 'querySnapshot.groupsSize', function() { return this.get('isGroup') && this.get('querySnapshot.groupsSize') === 0; - }), + }).readOnly(), isGroupBy: computed('isGroup', 'querySnapshot.{metricsSize,groupsSize}', function() { return this.get('isGroup') && this.get('querySnapshot.metricsSize') >= 1 && this.get('querySnapshot.groupsSize') >= 1; - }), + }).readOnly(), + + isSingleRow: or('isCountDistinct', 'isGroupAll').readOnly(), + + errorWindow: computed('windows.[]', function() { + return this.get('windows').find(window => !isNone(window.metadata.errors)); + }).readOnly(), - isSingleRow: or('isCountDistinct', 'isGroupAll') + hasData: computed('windows.[]', function() { + return !isEmpty(this.get('windows')); + }).readOnly() }); diff --git a/app/models/segment.js b/app/models/segment.js deleted file mode 100644 index 71d8de4f..00000000 --- a/app/models/segment.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2018, Yahoo Inc. - * Licensed under the terms of the Apache License, Version 2.0. - * See the LICENSE file associated with the project for terms. - */ -import { A } from '@ember/array'; -import EmberObject from '@ember/object'; -import DS from 'ember-data'; - -export default DS.Model.extend({ - metadata: DS.attr({ - defaultValue() { - return EmberObject.create(); - } - }), - records: DS.attr({ - defaultValue() { - return A(); - } - }), - created: DS.attr('date', { - defaultValue() { - return new Date(Date.now()); - } - }), - result: DS.belongsTo('result', { autoSave: true }), - pivotOptions: DS.attr('string') -}); diff --git a/app/models/window.js b/app/models/window.js index ab1312da..31bed267 100644 --- a/app/models/window.js +++ b/app/models/window.js @@ -6,8 +6,9 @@ import EmberObject from '@ember/object'; import DS from 'ember-data'; import { validator, buildValidations } from 'ember-cp-validations'; +import { equal } from '@ember/object/computed'; -let emitType = EmberObject.extend({ +let EmitTypes = EmberObject.extend({ TIME: 'Time Based', RECORD: 'Record Based', @@ -24,14 +25,14 @@ let emitType = EmberObject.extend({ } }); -let includeType = EmberObject.extend({ +let IncludeTypes = EmberObject.extend({ WINDOW: 'Everything in Window', - ALL: 'Everything from Start', + ALL: 'Everything from Start of Query', init() { this._super(...arguments); this.set('API', { - 'Everything from Start': 'ALL' + 'Everything from Start of Query': 'ALL' }); }, @@ -40,18 +41,18 @@ let includeType = EmberObject.extend({ } }); -export const EMIT_TYPES = emitType.create(); -export const INCLUDE_TYPES = includeType.create(); +export const EMIT_TYPES = EmitTypes.create(); +export const INCLUDE_TYPES = IncludeTypes.create(); let Validations = buildValidations({ 'emit.every': { - description: 'emit frequency', validators: [ + description: 'Emit frequency', validators: [ validator('presence', true), validator('number', { integer: true, allowString: true, gte: 1, - message: 'emit frequency must be a positive integer' + message: 'Emit frequency must be a positive integer' }), validator('window-emit-frequency') ] @@ -75,5 +76,7 @@ export default DS.Model.extend(Validations, { }); } }), - query: DS.belongsTo('query', { autoSave: true }) + query: DS.belongsTo('query', { autoSave: true }), + + isTimeBased: equal('emit.type', EMIT_TYPES.get('TIME')) }); diff --git a/app/routes/query.js b/app/routes/query.js index 07c69157..ad3539a5 100644 --- a/app/routes/query.js +++ b/app/routes/query.js @@ -6,35 +6,17 @@ import { hash, resolve } from 'rsvp'; import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; +import Queryable from 'bullet-ui/mixins/queryable'; -export default Route.extend({ +export default Route.extend(Queryable, { querier: service(), queryManager: service(), - resultHandler(context) { - context.transitionTo('result', context.get('result.id')); - }, - - errorHandler(error, context) { - console.error(error); // eslint-disable-line no-console - context.transitionTo('errored'); - }, - - segmentHandler(message, context) { - context.get('queryManager').addSegment(context.get('result'), message); - }, - actions: { fireQuery() { this.get('queryManager').addResult(this.paramsFor('query').query_id).then(result => { this.store.findRecord('query', this.paramsFor('query').query_id).then(query => { - this.set('result', result); - let handlers = { - success: this.resultHandler, - error: this.errorHandler, - message: this.segmentHandler - }; - this.get('querier').send(query, handlers, this); + this.submitQuery(query, result, this); }); }); } diff --git a/app/routes/result.js b/app/routes/result.js index 19e5b0d5..f4f97eb2 100644 --- a/app/routes/result.js +++ b/app/routes/result.js @@ -6,21 +6,29 @@ import { hash } from 'rsvp'; import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import Queryable from 'bullet-ui/mixins/queryable'; -export default Route.extend({ +export default Route.extend(Queryable, { querier: service(), + queryManager: service(), model(params) { return this.store.findRecord('result', params.result_id).catch(() => { this.transitionTo('missing', 'not-found'); }); }, - // Force the fetching of query and filter + afterModel(model) { - return hash({ - segments: model.get('segments'), - query: model.get('query'), - filter: model.get('query').then(query => query.get('filter')) + // Fetch all the things + return model.get('query').then(query => { + return hash({ + filter: query.get('filter'), + projections: query.get('projections'), + aggregation: query.get('aggregation').then(aggregation => { + return hash({ groups: aggregation.get('groups'), metrics: aggregation.get('metrics') }); + }), + window: query.get('window') + }); }); }, @@ -29,9 +37,33 @@ export default Route.extend({ this.transitionTo('query', query.get('id')); }, + reRunClick(query) { + this.get('queryManager').addResult(query.get('id')).then(result => { + this.set('hasPendingSubmit', true); + this.set('pendingQuery', query); + this.set('savedResult', result); + // Sends us to a new result page but don't want to start the new query till we finish transitioning. + this.resultHandler(this); + }); + }, + + cancelClick() { + this.get('querier').cancel(); + }, + willTransition() { this.get('querier').cancel(); return true; + }, + + didTransition() { + if (this.get('hasPendingSubmit')) { + let pendingQuery = this.get('pendingQuery'); + this.lateSubmitQuery(pendingQuery, this); + this.set('hasPendingSubmit', false); + this.set('pendingQuery', null); + } + return true; } } }); diff --git a/app/services/querier.js b/app/services/querier.js index a3403d97..8bbc0414 100644 --- a/app/services/querier.js +++ b/app/services/querier.js @@ -7,6 +7,7 @@ import { A } from '@ember/array'; import $ from 'jquery'; import { isNone, isEmpty, isEqual } from '@ember/utils'; import EmberObject from '@ember/object'; +import { alias } from '@ember/object/computed'; import Service, { inject as service } from '@ember/service'; import Filterizer from 'bullet-ui/mixins/filterizer'; import { AGGREGATIONS, DISTRIBUTIONS } from 'bullet-ui/models/aggregation'; @@ -20,6 +21,9 @@ export default Service.extend(Filterizer, { subfieldSeparator: '.', delimiter: ',', apiMode: true, + pendingRequest: null, + + isRunningQuery: alias('stompWebsocket.isConnected').readOnly(), /** * Recreates a Ember Data like representation from an API query specification. @@ -143,12 +147,14 @@ export default Service.extend(Filterizer, { if (isEmpty(json)) { return false; } + let emit = EmberObject.create({ type: EMIT_TYPES.get(json.emit.type), - every: Number(json.emit.every) / 1000 + every: isEqual(json.emit.type, 'TIME') ? Number(json.emit.every) / 1000 : Number(json.emit.every) }); + let include = EmberObject.create(); - if (!isEmpty(json.include) && isEqual(json.include.type, INCLUDE_TYPES.apiKey(INCLUDE_TYPES.get('ALL')))) { + if (!isEmpty(json.include) && isEqual(json.include.type, 'ALL')) { include.set('type', INCLUDE_TYPES.get('ALL')); } else { include.set('type', INCLUDE_TYPES.get('WINDOW')); @@ -176,12 +182,15 @@ export default Service.extend(Filterizer, { }, reformatWindow(window) { + let emitType = window.get('emit.type'); + let emitEvery = window.get('emit.every'); let json = { emit: { - type: EMIT_TYPES.apiKey(window.get('emit.type')), - every: Number(window.get('emit.every')) * 1000 + type: EMIT_TYPES.apiKey(emitType), + every: isEqual(emitType, EMIT_TYPES.get('TIME')) ? Number(emitEvery) * 1000 : Number(emitEvery) } }; + let includeType = window.get('include.type'); if (isEqual(includeType, INCLUDE_TYPES.get('ALL'))) { json.include = { type: INCLUDE_TYPES.apiKey(includeType) }; @@ -349,24 +358,11 @@ export default Service.extend(Filterizer, { return object; }, - /** - * Exposes the low-level StompClient object in order to abort. - * - * @param {Object} data The Query model. - * @param {Object} handlers The Object which contains the functions to invoke on success, failure or message. - * @param {Object} context The context for the handlers. - */ send(data, handlers, context) { - data = this.reformat(data); - let stompClient = this.get('stompWebsocket').createStompClient(data, handlers, context); - this.set('pendingRequest', stompClient); + this.get('stompWebsocket').startStompClient(this.reformat(data), handlers, context); }, cancel() { - let pendingRequest = this.get('pendingRequest'); - if (pendingRequest) { - pendingRequest.disconnect(); - this.set('pendingRequest', null); - } + this.get('stompWebsocket').disconnect(); } }); diff --git a/app/services/query-manager.js b/app/services/query-manager.js index ec79dd97..cd6519eb 100644 --- a/app/services/query-manager.js +++ b/app/services/query-manager.js @@ -6,15 +6,27 @@ import EmberObject from '@ember/object'; import Service, { inject as service } from '@ember/service'; import { isBlank, isEmpty, isEqual } from '@ember/utils'; +import { computed, get, getProperties } from '@ember/object'; +import { debounce } from '@ember/runloop'; import { AGGREGATIONS, DISTRIBUTION_POINTS } from 'bullet-ui/models/aggregation'; import { pluralize } from 'ember-inflector'; import ZLib from 'npm:browserify-zlib'; import Base64 from 'npm:urlsafe-base64'; import { all, Promise, resolve } from 'rsvp'; +import config from '../config/environment'; export default Service.extend({ store: service(), querier: service(), + saveSegmentDebounceInterval: 100, + debounceSegmentSaves: config.APP.SETTINGS.debounceSegmentSaves, + + windowNumberProperty: computed('settings', function() { + let mapping = this.get('settings.defaultValues.metadataKeyMapping'); + let { windowSection, windowNumber } = getProperties(mapping, 'windowSection', 'windowNumber'); + return `${windowSection}.${windowNumber}`; + }).readOnly(), + copyModelRelationship(from, to, fields, inverseName, inverseValue) { fields.forEach(field => { @@ -122,14 +134,16 @@ export default Service.extend({ }, addSegment(result, data) { - let segment = this.get('store').createRecord('segment', { + let position = result.get('windows.length'); + result.get('windows').pushObject({ metadata: data.meta, records: data.records, - result: result - }); - return result.save().then(() => { - return segment.save(); + sequence: get(data.meta, this.get('windowNumberProperty')), + index: position, + created: new Date(Date.now()) }); + let shouldDebounce = this.get('debounceSegmentSaves'); + return shouldDebounce ? debounce(result, result.save, this.get('saveSegmentDebounceInterval')) : result.save(); }, setAggregationAttributes(query, fields) { @@ -312,22 +326,13 @@ export default Service.extend({ }, deleteResults(query) { - return query.get('results').then(results => { - let promises = results.toArray().map(r => this.deleteAllSegments(r)); - return all(promises); - }).then(() => { + return query.get('results').then(() => { return this.deleteMultiple('results', query, 'query').then(() => { return query.save(); }); }); }, - deleteAllSegments(result) { - return this.deleteMultiple('segments', result, 'result').then(() => { - return result.save(); - }); - }, - deleteQuery(query) { return all([ this.deleteSingle('filter', query, 'query'), @@ -344,12 +349,5 @@ export default Service.extend({ return this.get('store').findAll('query').then(queries => { queries.forEach(q => this.deleteResults(q)); }); - }, - - setIfNotEmpty(object, key, value) { - if (!isEmpty(value)) { - object.set(key, value); - } - return object; } }); diff --git a/app/services/stomp-websocket.js b/app/services/stomp-websocket.js index 3a083788..55de1522 100644 --- a/app/services/stomp-websocket.js +++ b/app/services/stomp-websocket.js @@ -3,19 +3,20 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -import { isEqual } from '@ember/utils'; +import { isEqual, isNone } from '@ember/utils'; import { computed } from '@ember/object'; -import Service, { inject as service } from '@ember/service'; +import Service from '@ember/service'; import SockJS from 'npm:sockjs-client'; import Stomp from 'npm:@stomp/stompjs'; const ACK_TYPE = 'ACK'; const COMPLETE_TYPE = 'COMPLETE'; +const FAIL_TYPE = 'FAIL'; const NEW_QUERY_TYPE = 'NEW_QUERY'; const SESSION_LENGTH = 64; export default Service.extend({ - querier: service(), + client: null, url: computed('settings', function() { return `${this.get('settings.queryHost')}/${this.get('settings.queryNamespace')}/${this.get('settings.queryPath')}`; @@ -29,12 +30,16 @@ export default Service.extend({ return this.get('settings.queryStompResponseChannel'); }), + isConnected: computed('client', function() { + return !isNone(this.get('client')); + }), + makeStompMessageHandler(stompClient, handlers, context) { return payload => { let { type, content } = JSON.parse(payload.body); if (!isEqual(type, ACK_TYPE)) { - if (isEqual(type, COMPLETE_TYPE)) { - this.get('querier').cancel(); + if (isEqual(type, COMPLETE_TYPE) || isEqual(type, FAIL_TYPE)) { + this.set('client', null); } handlers.message(JSON.parse(content), context); } @@ -58,11 +63,11 @@ export default Service.extend({ makeStompErrorHandler(handlers, context) { return (...args) => { - handlers.error(`Error when connecting the server: ${args}`, context); + handlers.error(`Error while communicating with the server: ${args}`, context); }; }, - createStompClient(data, handlers, context) { + startStompClient(data, handlers, context) { let url = this.get('url'); let ws = new SockJS(url, [], { sessionId: SESSION_LENGTH }); let stompClient = Stomp.over(ws); @@ -70,7 +75,16 @@ export default Service.extend({ let onStompConnect = this.makeStompConnectHandler(stompClient, data, handlers, context); let onStompError = this.makeStompErrorHandler(handlers, context); + + this.set('client', stompClient); stompClient.connect({ }, onStompConnect, onStompError); - return stompClient; + }, + + disconnect() { + let client = this.get('client'); + if (!isNone(client)) { + client.disconnect(); + this.set('client', null); + } } }); diff --git a/app/styles/components/column-field.scss b/app/styles/components/column-field.scss index 0b3999f5..d49d2af3 100644 --- a/app/styles/components/column-field.scss +++ b/app/styles/components/column-field.scss @@ -21,6 +21,6 @@ } .column-mainfield, .column-onlyfield { - @import "power-select-inputs"; + @import "power-select-query-field"; } } diff --git a/app/styles/components/control-container.scss b/app/styles/components/control-container.scss new file mode 100644 index 00000000..604c8132 --- /dev/null +++ b/app/styles/components/control-container.scss @@ -0,0 +1,58 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +.auto-update-wrapper { + @import "mode-toggle"; + .mode-toggle { + justify-content: flex-start; + .mode { + width: 45px; + height: 20px; + font-size: $font-size-regular-small; + } + } + span { + font-size: $font-size-regular; + font-color: $inactive-grey; + font-family: $font-family-regular; + } + &.no-visibility { + visibility: hidden; + } +} + +.window-selector { + $input-height: 50px; + $width: 400px; + @import "power-select-field"; + + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + + .ember-power-select-trigger { + border: 1px solid $inactive-grey; + border-radius: 2px; + display: flex; + justify-content: center; + align-items: center; + width: $width; + font-size: $font-size-regular-large; + } + .window-progress-indicator { + svg { + height: 5px !important; + width: $width !important; + } + } +} + +.query-progress-indicator { + svg { + height: 60px !important; + width: 60px !important; + } +} diff --git a/app/styles/components/mode-toggle.scss b/app/styles/components/mode-toggle.scss index 2e71fd68..714823c9 100644 --- a/app/styles/components/mode-toggle.scss +++ b/app/styles/components/mode-toggle.scss @@ -9,11 +9,7 @@ $mode-inactive-color: $special-button-inactive-color; display: flex; - justify-content: flex-end; - margin: 10px 0; - .mode { - width: 70px; padding: 0 0 2px 0; background-color: $mode-inactive-color; color: $white; diff --git a/app/styles/components/nested-info-container.scss b/app/styles/components/nested-info-container.scss new file mode 100644 index 00000000..a6d831d9 --- /dev/null +++ b/app/styles/components/nested-info-container.scss @@ -0,0 +1,21 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +.information-container { + color: $heading-text-color; + + .information-wrapper { + position: relative; + padding: 5px $information-wrapper-padding-right 15px $information-wrapper-padding-left; + background-color: $background-grey; + border-radius: 4px; + .information-text h4 { + margin-bottom: 15px; + font-size: $font-size-regular; + font-weight: $font-weight-strong; + text-transform: uppercase; + } + } +} diff --git a/app/styles/components/output-data-input.scss b/app/styles/components/output-data-input.scss index 48db8d27..fad0c11f 100644 --- a/app/styles/components/output-data-input.scss +++ b/app/styles/components/output-data-input.scss @@ -54,7 +54,7 @@ } &.metrics-container .metrics-selection { - @import "power-select-inputs"; + @import "power-select-query-field"; } } diff --git a/app/styles/components/power-select-inputs.scss b/app/styles/components/power-select-field.scss similarity index 63% rename from app/styles/components/power-select-inputs.scss rename to app/styles/components/power-select-field.scss index 307caff4..8335f5d1 100644 --- a/app/styles/components/power-select-inputs.scss +++ b/app/styles/components/power-select-field.scss @@ -3,28 +3,20 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -.power-select-label { - @extend %query-input-label; - left: $padding-base-horizontal + 14px; -} .ember-power-select-trigger { &:focus { box-shadow: none; } // Make the selects as tall as the inputs - height: $query-input-height; + height: $input-height; overflow-y: hidden; border-radius: $input-radius-regular; - border-color: transparent; - .ember-power-select-selected-item { + .ember-power-select-selected-item, .ember-power-select-placeholder { display: inline-block; font-family: $font-family-regular; font-weight: $font-weight-regular; font-size: $font-size-regular; - // margin-top: $font-size-small; - padding-top: $font-size-small - 2px; - padding-left: 2px; color: $text-color-regular; } } diff --git a/app/styles/components/power-select-query-field.scss b/app/styles/components/power-select-query-field.scss new file mode 100644 index 00000000..514d0837 --- /dev/null +++ b/app/styles/components/power-select-query-field.scss @@ -0,0 +1,17 @@ +/* + * 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. + */ +.power-select-label { + @extend %query-input-label; + left: $padding-base-horizontal + 14px; +} +$input-height: $query-input-height; + +@import "power-select-field"; +.ember-power-select-trigger { + border-color: transparent; + padding-top: $font-size-small - 2px; + padding-left: 2px; +} diff --git a/app/styles/components/query-blurb.scss b/app/styles/components/query-blurb.scss index 3ba54bc0..ccd85ae0 100644 --- a/app/styles/components/query-blurb.scss +++ b/app/styles/components/query-blurb.scss @@ -3,9 +3,8 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -.filter-summary-text, .fields-summary-text { +.filter-summary-text, .fields-summary-text, .window-summary-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } - diff --git a/app/styles/components/query-input.scss b/app/styles/components/query-input.scss index 534fb792..d8b70f1d 100644 --- a/app/styles/components/query-input.scss +++ b/app/styles/components/query-input.scss @@ -43,7 +43,6 @@ font-size: $font-size-regular-large; } - %query-input-label { position: absolute; font-size: $font-size-small; @@ -106,7 +105,6 @@ %validated-input-row { @import "validated-input"; margin-top: 10px; - margin-left: 5px; input { height: 45px; } @@ -222,7 +220,6 @@ .aggregation-size { padding-right: 0; width: 165px; - margin-left: -15px; margin-right: 0; } .query-duration { @@ -244,8 +241,6 @@ position: fixed; bottom: 0; - @import "timed-progress-bar"; - hr { margin-left: (-$query-panel-side-padding); margin-right: (-$grid-gutter-width/2) + (-$query-panel-side-padding); @@ -263,15 +258,6 @@ font-size: $font-size-regular; } - .cancel-button { - @extend %secondary-button; - border-width: 2px; - width: 140px; - height: 40px; - font-size: $font-size-regular; - font-weight: $font-weight-medium; - } - .save-button { @extend %secondary-button; border-width: 2px; @@ -287,7 +273,7 @@ // Running state page styling &.query-running { - .btn:not(.cancel-button) { + .btn { background-color: $inactive-button-color !important; border-color: $inactive-button-color !important; } diff --git a/app/styles/components/no-results-help.scss b/app/styles/components/query-killed-help.scss similarity index 97% rename from app/styles/components/no-results-help.scss rename to app/styles/components/query-killed-help.scss index 1673d88b..a2130e54 100644 --- a/app/styles/components/no-results-help.scss +++ b/app/styles/components/query-killed-help.scss @@ -3,10 +3,10 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -.bummer { +.killed { margin-top: 100px; - .bummer-image > img { + .killed-image > img { width: 100px; height: 100px; } @@ -65,4 +65,4 @@ } } } -} \ No newline at end of file +} diff --git a/app/styles/components/query-step-help.scss b/app/styles/components/query-step-help.scss new file mode 100644 index 00000000..ae48ac17 --- /dev/null +++ b/app/styles/components/query-step-help.scss @@ -0,0 +1,89 @@ +/* + * 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. + */ +.row { + margin-top: 100px; + + .image > img { + width: 100px; + height: 100px; + } + + .image.animate > img { + animation: rotate 2s linear infinite; + } + + @keyframes rotate { + from { + transform: rotate(0deg) + } + to { + transform: rotate(359deg) + } + } + + h2 { + margin-top: 25px; + font-family: $font-family-regular; + font-size: $font-size-ultra-large; + font-weight: $font-weight-strong; + color: $heading-text-color; + } + h3 { + margin-top: 25px; + font-family: $font-family-regular; + font-size: $font-size-large; + font-weight: $font-weight-strong; + color: $heading-text-color; + } + h4 { + font-family: $font-family-regular; + font-size: $font-size-regular-large; + font-weight: $font-weight-regular; + color: $header-help-color; + } + .steps { + text-align: left; + margin: auto; + margin-top: 30px; + width: 600px; + + h4 { + font-family: $font-family-regular; + font-size: $font-size-regular; + font-weight: $font-weight-strong; + color: $heading-text-color; + } + p { + font-family: $font-family-regular; + font-size: $font-size-regular; + font-weight: $font-weight-regular; + color: $header-help-color; + } + + ol { + list-style-type: none; + margin: 0; + counter-reset: li-counter; + li { + position: relative; + &:before { + position: absolute; + width: 23px; + height: 23px; + left: -35px; + + content: counter(li-counter); + counter-increment: li-counter; + text-align: center; + border-radius: 30px; + background-color: $table-body-blue-text-color; + color: white; + padding: 1.5px 0 0 0.5px; + } + } + } + } +} diff --git a/app/styles/components/records-charter.scss b/app/styles/components/records-charter.scss index b61cce0d..26fbf7fe 100644 --- a/app/styles/components/records-charter.scss +++ b/app/styles/components/records-charter.scss @@ -13,6 +13,13 @@ user-select: none; @import 'mode-toggle'; + .mode-toggle { + justify-content: flex-end; + margin: 10px 0; + .mode { + width: 70px; + } + } .visual-container { @import 'pivot-table'; diff --git a/app/styles/components/records-raw-viewer.scss b/app/styles/components/records-raw-viewer.scss index f143df11..e4c7804f 100644 --- a/app/styles/components/records-raw-viewer.scss +++ b/app/styles/components/records-raw-viewer.scss @@ -7,6 +7,14 @@ @import 'mode-toggle'; @import 'pretty-json'; + .mode-toggle { + justify-content: flex-end; + margin: 10px 0; + .mode { + width: 70px; + } + } + .raw-display { pre { background-color: $white; diff --git a/app/styles/components/records-viewer.scss b/app/styles/components/records-viewer.scss index dbeee3c6..a9d5750f 100644 --- a/app/styles/components/records-viewer.scss +++ b/app/styles/components/records-viewer.scss @@ -5,12 +5,9 @@ */ .records-viewer { $records-header-color: #00C877; - margin-right: $results-right-space; .records-title { - @import "section-heading-with-help"; - - margin-top: 35px; + margin-top: 15px; margin-bottom: 15px; padding-left: 0; @@ -19,6 +16,7 @@ align-items: center; .records-header { + margin-left: 110px; color: $records-header-color; font-size: $font-size-regular; font-weight: $font-weight-strong; diff --git a/app/styles/components/result-metadata.scss b/app/styles/components/result-metadata.scss deleted file mode 100644 index 0cdacb11..00000000 --- a/app/styles/components/result-metadata.scss +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ - .result-metadata { - @import "pretty-json"; - - margin-top: 5px; - - .expand-bar { - height: 25px; - margin-top: 10px; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - cursor: pointer; - background-color: $background-grey-darker; - } - - i { - color: $primary-button-color; - } - } - } diff --git a/app/styles/components/result-query-information.scss b/app/styles/components/result-query-information.scss new file mode 100644 index 00000000..15f5679f --- /dev/null +++ b/app/styles/components/result-query-information.scss @@ -0,0 +1,54 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +.query-title { + margin-bottom: 25px; + color: $heading-text-color; + font-size: $font-size-large; + font-weight: $font-weight-strong; +} + +.query-blurb-wrapper { + $button-width: 130px; + $button-height: 38px; + + @import "query-blurb"; + + cursor: pointer; + + &:hover { + background-color: $background-grey-darker; + } + + .query-blurb > .filter-summary-text, .query-blurb > .projections-summary-text { + display: block; + } + + .query-text { + margin-right: $results-right-space + $button-width; + } + + .query-button { + float: right; + display: inline-block; + position: absolute; + right: $results-right-space; + margin-top: (-$button-height)/2; + width: $button-width; + height: $button-height; + font-size: $font-size-regular; + font-weight: $font-weight-medium; + } + + .link-button { + @extend %primary-button; + top: 25%; + } + + .rerun-button,.cancel-button { + @extend %secondary-button; + top: 75%; + } +} diff --git a/app/styles/components/result-viewer.scss b/app/styles/components/result-viewer.scss new file mode 100644 index 00000000..0f8cf3fd --- /dev/null +++ b/app/styles/components/result-viewer.scss @@ -0,0 +1,30 @@ +/* + * Copyright 2018, Yahoo Inc. + * Licensed under the terms of the Apache License, Version 2.0. + * See the LICENSE file associated with the project for terms. + */ +.window-information { + @import "window-metadata"; + margin-bottom: 20px;; +} + +.control-container { + display: flex; + justify-content: space-between; + align-items: center; + align-content: center; + margin-bottom: 20px; + min-height: 10px; + + @import "control-container"; + +} + +.records-container { + @import "records-viewer"; + @import "query-step-help"; + margin-bottom: 20px; + .help-text { + margin-right: $results-right-space; + } +} diff --git a/app/styles/components/running-query.scss b/app/styles/components/running-query.scss new file mode 100644 index 00000000..63199809 --- /dev/null +++ b/app/styles/components/running-query.scss @@ -0,0 +1,26 @@ +/* + * 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. + */ +.running { + margin-top: 300px; + + .image > img { + width: 50px; + height: 50px; + } + h2 { + margin-top: 25px; + font-family: $font-family-regular; + font-size: $font-size-ultra-large; + font-weight: $font-weight-strong; + color: $heading-text-color; + } + h3 { + font-family: $font-family-regular; + font-size: $font-size-regular-large + 1; + font-weight: $font-weight-regular; + color: $header-help-color; + } +} diff --git a/app/styles/components/section-heading-with-help.scss b/app/styles/components/section-heading-with-help.scss index e6311d86..98371de0 100644 --- a/app/styles/components/section-heading-with-help.scss +++ b/app/styles/components/section-heading-with-help.scss @@ -16,4 +16,3 @@ } @import "info-popover"; } - diff --git a/app/styles/components/timed-progress-bar.scss b/app/styles/components/timed-progress-bar.scss index 63706d84..e69de29b 100644 --- a/app/styles/components/timed-progress-bar.scss +++ b/app/styles/components/timed-progress-bar.scss @@ -1,22 +0,0 @@ -/* - * 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. - */ -.progress { - $timed-progress-bar-color: $success-green; - $timed-progress-bar-height: 38px; - width: 90%; - - .progress-bar { - - border-radius: 4px; - font-size: $font-size-large; - - background-color: $timed-progress-bar-color; - // To vertically center - line-height: $timed-progress-bar-height; - } - border: 1px solid $timed-progress-bar-color; - height: $timed-progress-bar-height; -} diff --git a/app/styles/components/validated-input.scss b/app/styles/components/validated-input.scss index 56efb5bc..e7fd30d3 100644 --- a/app/styles/components/validated-input.scss +++ b/app/styles/components/validated-input.scss @@ -6,6 +6,7 @@ .validated-input { .error-tooltip-link { @extend %error-icon; + margin-left: 15px; } &.has-error > .labeled-input { margin-left: -15px; diff --git a/app/styles/components/window-input.scss b/app/styles/components/window-input.scss index e9ee4381..37b32f11 100644 --- a/app/styles/components/window-input.scss +++ b/app/styles/components/window-input.scss @@ -1,43 +1,45 @@ .window-input { - .add-button { - @extend %add-button; - margin-bottom: 10px; - i { - padding-right: 5px; - } - } - - .remove-button { - @extend %remove-button; - } - - @import "info-popover"; - padding: 15px; margin-top: 5px; margin-bottom: 15px; max-width: 800px; - background-color: $box-rectangle-color; - position: relative; - .ember-radio-button { - @extend %ember-radio-button; - display: inline-block; - margin-right: 30px; + .no-window-section { + .add-button { + @extend %add-button; + margin-top: 15px; + i { + padding-right: 5px; + } + } + margin-bottom: 30px; } - .subsection-header { - @extend %subsection-header; - } + .window-section { + display: flex; + flex-flow: column nowrap; + padding: 15px; + background-color: $box-rectangle-color; - .row { - @extend %validated-input-row; - .validated-input { - .labeled-input { - padding-right: 0; - width: 150px; - margin-left: -5px; - margin-right: 0; - } + .subsection-header { + @extend %subsection-header; } + + .delete-button { + @extend %remove-button; + min-height: 20px; + align-self: flex-end; + } + + .ember-radio-button { + @extend %ember-radio-button; + display: inline-block; + margin-right: 30px; + } + + .row { + @extend %validated-input-row; + } + } + } diff --git a/app/styles/components/window-metadata.scss b/app/styles/components/window-metadata.scss new file mode 100644 index 00000000..cc5f1d89 --- /dev/null +++ b/app/styles/components/window-metadata.scss @@ -0,0 +1,67 @@ +/* + * 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. + */ + .window-metadata { + @import "pretty-json"; + + background-color: $background-grey; + padding: 5px; + margin-top: 10px; + border-radius: 4px; + max-height: 50px; + overflow: hidden; + transition: max-height $default-transition-time ease-in-out; + + .information-text h4 { + margin-bottom: 15px; + font-size: $font-size-regular; + font-weight: $font-weight-strong; + text-transform: uppercase; + } + + .expand-bar { + display: flex; + align-items: center; + justify-content: center; + height: 25px; + i { + color: $inactive-grey; + } + } + + &:hover { + cursor: pointer; + background-color: $background-grey-darker; + i { + color: $black; + } + } + + &.is-expanded { + padding: 30px; + padding-bottom: 0; + max-height: 700px; + pre { + max-height: 600px; + } + &:hover { + cursor: default; + background-color: $background-grey; + i { + color: $inactive-grey; + } + } + .expand-bar { + &:hover { + cursor: pointer; + background-color: $background-grey-darker; + i { + color: $black; + } + } + margin: 0 -30px 0 -30px; + } + } + } diff --git a/app/styles/result.scss b/app/styles/result.scss index 4a6178ae..8d4573b7 100644 --- a/app/styles/result.scss +++ b/app/styles/result.scss @@ -3,106 +3,25 @@ * Licensed under the terms of the Apache License, Version 2.0. * See the LICENSE file associated with the project for terms. */ -$results-right-space: 30px; -$results-left-space: $left-bar-separation-width; -$results-inner-left-space: 20px; -$results-inner-right-space: 35px; -$information-wrapper-padding-left: 20px; -$information-wrapper-padding-right: 35px; +.results-container { + $results-right-space: 30px; + $results-left-space: $left-bar-separation-width; + $results-inner-left-space: 20px; + $results-inner-right-space: 35px; + $information-wrapper-padding-left: 20px; + $information-wrapper-padding-right: 35px; -.information-container { - margin: 22px $results-right-space 0 $results-left-space; - color: $heading-text-color; -} - -.query-information{ - .query-title { - margin-bottom: 25px; - color: $heading-text-color; - font-size: $font-size-large; - font-weight: $font-weight-strong; - } -} - -.information-container { - - .information-wrapper { - padding: 5px $information-wrapper-padding-right 15px $information-wrapper-padding-left; - background-color: $background-grey; - border-radius: 4px; - position: relative; - - .information-text { - h4 { - margin-bottom: 15px; - font-size: $font-size-regular; - font-weight: $font-weight-strong; - text-transform: uppercase; - } - } - - } -} -.query-information .query-blurb-wrapper { - @import "components/query-blurb"; - - $button-width: 130px; - $button-height: 38px; + @import "components/nested-info-container"; + @import "components/section-heading-with-help"; - cursor: pointer; - - &:hover { - background-color: $background-grey-darker; - } - - .query-blurb > .filter-summary-text, .query-blurb > .projections-summary-text { - display: block; - } - .query-text { - margin-right: $results-right-space + $button-width; - } - .query-link-button { - @extend %primary-button; - - float: right; - display: inline-block; - position: absolute; - top: 50%; - right: $results-right-space; - margin-top: (-$button-height)/2; - width: $button-width; - height: $button-height; - font-size: $font-size-regular; - font-weight: $font-weight-medium; - } -} - -.result-information { - margin-top: 10px; - margin-bottom: 30px; - .metadata-wrapper { - padding-bottom: 0; - .metadata-text { - @import "components/result-metadata"; - @import "components/section-heading-with-help"; + margin: 22px $results-right-space 0 $results-left-space; - .section-header .section-title { - margin-top: 10px; - } - .expand-bar { - margin-left: (-$information-wrapper-padding-left); - margin-right: (-$information-wrapper-padding-right); - } - } + .query-section { + @import "components/result-query-information"; } -} -.result-container { - @import "components/records-viewer"; - @import "components/no-results-help"; - margin-left: $left-bar-separation-width; - margin-bottom: 30px; - .help-text { - margin-right: $results-right-space; + .result-section { + margin-top: 20px; + @import "components/result-viewer"; } } diff --git a/app/templates/components/mode-toggle.hbs b/app/templates/components/mode-toggle.hbs index 26b1fd97..b0cad1d9 100644 --- a/app/templates/components/mode-toggle.hbs +++ b/app/templates/components/mode-toggle.hbs @@ -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. --}} -