blob: c35abaae00f4f6927b4c0738214b8a299357fc88 [file] [log] [blame]
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());
}
}