| import * as m from 'mithril'; |
| |
| import {Actions} from '../common/actions'; |
| import {ColumnType} from '../common/query_result'; |
| import {PivotTableReduxQuery, PivotTableReduxResult} from '../common/state'; |
| import {PivotTree} from '../controller/pivot_table_redux_controller'; |
| |
| import {globals} from './globals'; |
| import {Panel} from './panel'; |
| import { |
| aggregationIndex, |
| ColumnSet, |
| generateQuery, |
| QueryGeneratorError, |
| sliceAggregationColumns, |
| Table, |
| TableColumn, |
| tables, |
| threadSliceAggregationColumns |
| } from './pivot_table_redux_query_generator'; |
| |
| interface ColumnSetCheckboxAttrs { |
| set: ColumnSet; |
| setKey: TableColumn; |
| } |
| |
| interface PathItem { |
| tree: PivotTree; |
| nextKey: ColumnType; |
| } |
| |
| // Helper component that controls whether a particular key is present in a |
| // ColumnSet. |
| class ColumnSetCheckbox implements m.ClassComponent<ColumnSetCheckboxAttrs> { |
| view({attrs}: m.Vnode<ColumnSetCheckboxAttrs>) { |
| return m('input[type=checkbox]', { |
| onclick: (e: InputEvent) => { |
| const target = e.target as HTMLInputElement; |
| if (target.checked) { |
| attrs.set.add(attrs.setKey); |
| } else { |
| attrs.set.delete(attrs.setKey); |
| } |
| globals.rafScheduler.scheduleFullRedraw(); |
| }, |
| checked: attrs.set.has(attrs.setKey) |
| }); |
| } |
| } |
| |
| export class PivotTableRedux extends Panel { |
| selectedPivotsMap = new ColumnSet(); |
| selectedAggregations = new ColumnSet(); |
| editMode = true; |
| |
| renderCanvas(): void {} |
| |
| generateQuery(): PivotTableReduxQuery { |
| return generateQuery(this.selectedPivotsMap, this.selectedAggregations); |
| } |
| |
| runQuery() { |
| try { |
| const query = this.generateQuery(); |
| const lastPivotTableState = globals.state.pivotTableRedux; |
| globals.dispatch(Actions.setPivotStateReduxState({ |
| pivotTableState: { |
| query, |
| queryId: lastPivotTableState.queryId + 1, |
| enabled: true, |
| queryResult: null |
| } |
| })); |
| } catch (e) { |
| console.log(e); |
| } |
| } |
| |
| renderTablePivotColumns(t: Table) { |
| return m( |
| 'li', |
| t.name, |
| m('ul', |
| t.columns.map( |
| col => |
| m('li', |
| m(ColumnSetCheckbox, { |
| set: this.selectedPivotsMap, |
| setKey: [t.name, col], |
| }), |
| col)))); |
| } |
| |
| renderResultsView() { |
| return m( |
| '.pivot-table-redux', |
| m('button.mode-button', |
| { |
| onclick: () => { |
| this.editMode = true; |
| globals.rafScheduler.scheduleFullRedraw(); |
| } |
| }, |
| 'Edit'), |
| this.renderResultsTable()); |
| } |
| |
| renderSectionRow( |
| path: PathItem[], tree: PivotTree, |
| result: PivotTableReduxResult): m.Vnode { |
| const renderedCells = []; |
| for (let j = 0; j + 1 < path.length; j++) { |
| renderedCells.push(m('td', m('span.indent', ' '), `${path[j].nextKey}`)); |
| } |
| |
| const treeDepth = result.metadata.pivotColumns.length; |
| const colspan = treeDepth - path.length + 1; |
| const button = |
| m('button', |
| { |
| onclick: () => { |
| tree.isCollapsed = !tree.isCollapsed; |
| globals.rafScheduler.scheduleFullRedraw(); |
| } |
| }, |
| m('i.material-icons', |
| tree.isCollapsed ? 'expand_more' : 'expand_less')); |
| |
| renderedCells.push( |
| m('td', {colspan}, button, `${path[path.length - 1].nextKey}`)); |
| |
| for (const value of tree.aggregates) { |
| renderedCells.push(m('td', `${value}`)); |
| } |
| |
| return m('tr', renderedCells); |
| } |
| |
| renderTree( |
| path: PathItem[], tree: PivotTree, result: PivotTableReduxResult, |
| sink: m.Vnode[]) { |
| if (tree.isCollapsed) { |
| sink.push(this.renderSectionRow(path, tree, result)); |
| return; |
| } |
| if (tree.children.size > 0) { |
| // Avoid rendering the intermediate results row for the root of tree |
| // and in case there's only one child subtree. |
| if (!tree.isCollapsed && path.length > 0 && tree.children.size !== 1) { |
| sink.push(this.renderSectionRow(path, tree, result)); |
| } |
| for (const [key, childTree] of tree.children.entries()) { |
| path.push({tree: childTree, nextKey: key}); |
| this.renderTree(path, childTree, result, sink); |
| path.pop(); |
| } |
| return; |
| } |
| |
| // Avoid rendering the intermediate results row if it has only one leaf |
| // row. |
| if (!tree.isCollapsed && path.length > 0 && tree.rows.length > 1) { |
| sink.push(this.renderSectionRow(path, tree, result)); |
| } |
| for (const row of tree.rows) { |
| const renderedCells = []; |
| const treeDepth = result.metadata.pivotColumns.length; |
| for (let j = 0; j < treeDepth; j++) { |
| if (j < path.length) { |
| renderedCells.push(m('td', m('span.indent', ' '), `${row[j]}`)); |
| } else { |
| renderedCells.push(m(`td`, `${row[j]}`)); |
| } |
| } |
| for (let j = 0; j < result.metadata.aggregationColumns.length; j++) { |
| const value = row[aggregationIndex(treeDepth, j, treeDepth)]; |
| renderedCells.push(m('td', `${value}`)); |
| } |
| |
| sink.push(m('tr', renderedCells)); |
| } |
| } |
| |
| renderTotalsRow(queryResult: PivotTableReduxResult) { |
| const overallValuesRow = |
| [m('td.total-values', |
| {'colspan': queryResult.metadata.pivotColumns.length}, |
| m('strong', 'Total values:'))]; |
| for (const aggValue of queryResult.tree.aggregates) { |
| overallValuesRow.push(m('td', `${aggValue}`)); |
| } |
| return m('tr', overallValuesRow); |
| } |
| |
| renderResultsTable() { |
| const state = globals.state.pivotTableRedux; |
| if (state.query !== null || state.queryResult === null) { |
| return m('div', 'Loading...'); |
| } |
| |
| const renderedRows: m.Vnode[] = []; |
| this.renderTree( |
| [], state.queryResult.tree, state.queryResult, renderedRows); |
| |
| const allColumns = state.queryResult.metadata.pivotColumns.concat( |
| state.queryResult.metadata.aggregationColumns); |
| return m( |
| 'table.query-table.pivot-table', |
| m('thead', m('tr', allColumns.map(column => m('td', column)))), |
| m('tbody', this.renderTotalsRow(state.queryResult), renderedRows)); |
| } |
| |
| renderQuery(): m.Vnode { |
| try { |
| return m( |
| 'div', |
| m('pre', this.generateQuery()), |
| m('button.mode-button', |
| { |
| onclick: () => { |
| this.editMode = false; |
| this.runQuery(); |
| globals.rafScheduler.scheduleFullRedraw(); |
| } |
| }, |
| 'Execute')); |
| } catch (e) { |
| if (e instanceof QueryGeneratorError) { |
| return m('div.query-error', e.message); |
| } else { |
| throw e; |
| } |
| } |
| } |
| |
| view() { |
| return this.editMode ? this.renderEditView() : this.renderResultsView(); |
| } |
| |
| renderEditView() { |
| return m( |
| '.pivot-table-redux.edit', |
| m('div', |
| m('h2', 'Pivots'), |
| m('ul', |
| tables.map( |
| t => this.renderTablePivotColumns(t), |
| ))), |
| m('div', |
| m('h2', 'Aggregations'), |
| m('ul', |
| ...sliceAggregationColumns.map( |
| t => |
| m('li', |
| m(ColumnSetCheckbox, { |
| set: this.selectedAggregations, |
| setKey: ['slice', t], |
| }), |
| t)), |
| ...threadSliceAggregationColumns.map( |
| t => |
| m('li', |
| m(ColumnSetCheckbox, { |
| set: this.selectedAggregations, |
| setKey: ['thread_slice', t], |
| }), |
| `thread_slice.${t}`)))), |
| this.renderQuery()); |
| } |
| } |