From 74e491abee6aa8d774779b70f56d22d04cd46d4f Mon Sep 17 00:00:00 2001 From: loi Date: Thu, 7 Apr 2016 11:26:20 -0700 Subject: [PATCH] DM-5722: Add table client-side sorting --- .../ipac/firefly/server/util/QueryUtil.java | 11 + .../server/util/ipactable/JsonTableUtil.java | 45 ++-- .../server/util/ipactable/TableDef.java | 9 +- .../server/visualize/VisServerCommands.java | 12 +- .../server/visualize/VisServerOps.java | 27 +- .../edu/caltech/ipac/util/IpacTableUtil.java | 5 +- src/firefly/js/tables/SortInfo.js | 10 +- src/firefly/js/tables/TableStore.js | 47 ++-- src/firefly/js/tables/TableUtil.js | 65 ++++- src/firefly/js/tables/ui/BasicTable.jsx | 236 ++++-------------- src/firefly/js/tables/ui/BasicTableView.jsx | 233 +++++++++++++++++ src/firefly/js/tables/ui/TablePanel.jsx | 26 +- .../js/tables/ui/TablePanelOptions.jsx | 129 ++++++---- src/firefly/js/ui/DropDownContainer.css | 2 +- .../js/visualize/ui/FitsHeaderView.jsx | 47 +--- 15 files changed, 521 insertions(+), 383 deletions(-) create mode 100644 src/firefly/js/tables/ui/BasicTableView.jsx diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/util/QueryUtil.java b/src/firefly/java/edu/caltech/ipac/firefly/server/util/QueryUtil.java index 90aaa8035d..233b2ec6ab 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/util/QueryUtil.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/util/QueryUtil.java @@ -24,6 +24,7 @@ import edu.caltech.ipac.firefly.server.query.DataAccessException; import edu.caltech.ipac.firefly.server.util.ipactable.DataGroupPart; import edu.caltech.ipac.firefly.server.util.ipactable.DataGroupReader; +import edu.caltech.ipac.firefly.server.util.ipactable.TableDef; import edu.caltech.ipac.util.AppProperties; import edu.caltech.ipac.util.CollectionUtil; import edu.caltech.ipac.util.DataGroup; @@ -266,6 +267,16 @@ public static RawDataSet convertToRawDataset(DataGroup dg, int startIdx, int pag return dataset; } + public static DataGroupPart convertToDataGroupPart(DataGroup dg, int startIdx, int pageSize) { + DataGroup page = dg.subset(startIdx, startIdx+pageSize); + page.setRowIdxOffset(startIdx); + TableDef tableDef = new TableDef(); + tableDef.setSource("unknown"); + tableDef.setStatus(DataGroupPart.State.COMPLETED); + tableDef.setCols(Arrays.asList(page.getDataDefinitions())); + + return new DataGroupPart(tableDef, dg, startIdx, page.size()); + } /** * return Float.NaN if val is null or not a float diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/JsonTableUtil.java b/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/JsonTableUtil.java index 10efc6a160..d2471e1cf1 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/JsonTableUtil.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/JsonTableUtil.java @@ -8,6 +8,7 @@ import edu.caltech.ipac.firefly.data.SortInfo; import edu.caltech.ipac.firefly.data.TableServerRequest; import edu.caltech.ipac.firefly.data.table.TableMeta; +import edu.caltech.ipac.firefly.server.util.QueryUtil; import edu.caltech.ipac.firefly.util.DataSetParser; import edu.caltech.ipac.firefly.visualize.Band; import edu.caltech.ipac.util.*; @@ -46,8 +47,10 @@ public static JSONObject toJsonTableModel(DataGroupPart page, TableMeta meta, Ta } } } - for (String key : request.getMeta().keySet()) { - meta.setAttribute(key, request.getMeta(key)); + if (request != null && request.getMeta() != null) { + for (String key : request.getMeta().keySet()) { + meta.setAttribute(key, request.getMeta(key)); + } } String tblId = meta.getAttribute(TableServerRequest.TBL_ID); @@ -58,7 +61,7 @@ public static JSONObject toJsonTableModel(DataGroupPart page, TableMeta meta, Ta tableModel.put("totalRows", page.getRowCount()); if (page.getData() != null ) { - tableModel.put("tableData", toJsonTableData(page.getData(), page.getTableDef(), meta)); + tableModel.put("tableData", toJsonTableData(page.getData(), page.getTableDef(), meta)); } @@ -203,37 +206,17 @@ private static Object guessType(TableMeta meta) { //============================= //LZ DM-4494 - public static String toJsonString(DataGroup dataGroup, Long fileSize, String tableID) throws IOException { - - DataGroupPart dp = new DataGroupPart(); - dp.setData(dataGroup); - - TableServerRequest request = new TableServerRequest("fitsHeaderTale"); - request.setMeta(TableServerRequest.TBL_ID, tableID); - - TableMeta meta = new TableMeta("fitsHeader"); - meta.setFileSize((fileSize)); - - JSONObject jsonObj = JsonTableUtil.toJsonTableModel(dp, meta, request); - - - - return jsonObj.toJSONString(); - - - } - - //LZ DM-4494 - public static String dataGroupMapToJasonString(HashMap dataMap,HashMap fileSizeMap, String tableID) throws IOException { - - + public static JSONObject toJsonTableModelMap(Map dataMap, Map metaMap, TableServerRequest request) throws IOException { JSONObject jsoObj = new JSONObject(); - for (Band band : dataMap.keySet()) { - jsoObj.put(band.name(), toJsonString(dataMap.get(band), fileSizeMap.get(band), tableID)); + for (Object key : dataMap.keySet()) { + TableMeta meta = metaMap.get(key); + DataGroupPart dp = QueryUtil.convertToDataGroupPart(dataMap.get(key), 0, Integer.MAX_VALUE); + JSONObject aJsonTable = JsonTableUtil.toJsonTableModel(dp, meta, request); - } + jsoObj.put(key, aJsonTable); - return jsoObj.toJSONString(); + } + return jsoObj; } //============================= //============================= //============================= //============================= //============================= //============================= diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/TableDef.java b/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/TableDef.java index 1ea172495c..b201b1589d 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/TableDef.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/util/ipactable/TableDef.java @@ -30,6 +30,7 @@ public void addAttributes(DataGroup.Attribute... attributes) { } } } + public void setCols(List cols) { this.cols = cols; } public List getCols() { return cols; @@ -99,14 +100,6 @@ public void setRowCount(int rowCount) { this.rowCount = rowCount < 0 ? 0 : rowCount; } - public int getColCount() { - return colCount; - } - - public void setColCount(int colCount) { - this.colCount = colCount; - } - public int getRowStartOffset() { return rowStartOffset; } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerCommands.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerCommands.java index 9113110a79..99fddcbfd3 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerCommands.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerCommands.java @@ -16,7 +16,6 @@ import edu.caltech.ipac.firefly.server.servlets.CommandService; import edu.caltech.ipac.firefly.server.util.Logger; import edu.caltech.ipac.firefly.server.util.ipactable.JsonTableUtil; -import edu.caltech.ipac.firefly.server.util.ipactable.TableDef; import edu.caltech.ipac.firefly.visualize.Band; import edu.caltech.ipac.firefly.visualize.FileAndHeaderInfo; import edu.caltech.ipac.firefly.visualize.PlotState; @@ -286,12 +285,13 @@ public String doCommand(Map paramMap) throws IllegalArgumentEx //TableServerRequest req=TableServerRequest.parse(sp.getRequired(ServerParams.FITS_HEADER)); PlotState state= sp.getState(); - Object[] dataInfo = VisServerOps.getFitsHeader(state); - HashMap dataGroupMap= (HashMap ) dataInfo[0]; - HashMap fileSizeMap = ( HashMap ) dataInfo[1]; + Object[] dataInfo = VisServerOps.getFitsHeader(state, tableID); + HashMap dataGroupMap= (HashMap ) dataInfo[0]; + HashMap metaMap = ( HashMap ) dataInfo[1]; - - return JsonTableUtil.dataGroupMapToJasonString(dataGroupMap, fileSizeMap,tableID); + TableServerRequest treq = new TableServerRequest("fitsHeaderTale"); + treq.setPageSize(Integer.MAX_VALUE); + return JsonTableUtil.toJsonTableModelMap(dataGroupMap, metaMap, treq).toJSONString(); } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java index d014f0d5fb..cac91cf278 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java @@ -5,7 +5,9 @@ import edu.caltech.ipac.firefly.data.BandInfo; import edu.caltech.ipac.firefly.data.DataEntry; +import edu.caltech.ipac.firefly.data.TableServerRequest; import edu.caltech.ipac.firefly.data.table.RawDataSet; +import edu.caltech.ipac.firefly.data.table.TableMeta; import edu.caltech.ipac.firefly.server.Counters; import edu.caltech.ipac.firefly.server.ServerContext; import edu.caltech.ipac.firefly.server.cache.UserCache; @@ -1014,7 +1016,7 @@ public static WebPlotResult getFitsHeaderInfo(PlotState state) { */ public static Map getFitsHeaderExtend(PlotState state) throws FitsException { - HashMap dataMap = new HashMap(); + HashMap dataMap = new HashMap(); ActiveCallCtx ctx = null; try { @@ -1025,7 +1027,7 @@ public static Map getFitsHeaderExtend(PlotState state) throws FitsException { FitsRead fr = plot.getHistogramOps(band, ctx.getFitsReadGroup()).getFitsRead(); DataGroup dg = getFitsHeaders(fr.getHeader(), plot.getPlotDesc()); - dataMap.put(band, dg); + dataMap.put(band.name(), dg); } return dataMap; @@ -1045,31 +1047,38 @@ public static Map getFitsHeaderExtend(PlotState state) throws FitsException { * @return */ - public static Object[] getFitsHeader(PlotState state) throws FitsException { + public static Object[] getFitsHeader(PlotState state, String tableID) throws FitsException { - HashMap dataMap = new HashMap< Band, DataGroup>(); + HashMap dataMap = new HashMap(); - HashMap fileSizeMap = new HashMap(); + HashMap metaMap = new HashMap(); try { for (Band band : state.getBands()) { File f = PlotStateUtil.getWorkingFitsFile(state, band); if (f == null) f = PlotStateUtil.getOriginalFile(state, band); Fits fits = new Fits(f); - fileSizeMap.put(band,f.length() ); + TableServerRequest request = new TableServerRequest("fitsHeaderTale"); + request.setMeta(TableServerRequest.TBL_ID, tableID); + + TableMeta meta = new TableMeta("fitsHeader"); + meta.setFileSize(f.length()); + meta.setAttribute(TableServerRequest.TBL_ID, tableID + '-' + band.name()); + + metaMap.put(band.name(), meta); BasicHDU hdu[] = fits.read(); Header header = hdu[0].getHeader(); if (header.containsKey("EXTEND") && header.getBooleanValue("EXTEND")) { - dataMap = (HashMap) getFitsHeaderExtend(state); + dataMap = (HashMap) getFitsHeaderExtend(state); } else { DataGroup dg = getFitsHeaders(header, "fits data"); - dataMap.put(band, dg); + dataMap.put(band.name(), dg); } } ////LZcounters.incrementVis("Fits header"); - Object[] mapObj = {dataMap, fileSizeMap}; + Object[] mapObj = {dataMap, metaMap}; return mapObj; } diff --git a/src/firefly/java/edu/caltech/ipac/util/IpacTableUtil.java b/src/firefly/java/edu/caltech/ipac/util/IpacTableUtil.java index 518920edf5..7535cac7c5 100644 --- a/src/firefly/java/edu/caltech/ipac/util/IpacTableUtil.java +++ b/src/firefly/java/edu/caltech/ipac/util/IpacTableUtil.java @@ -368,10 +368,7 @@ public static TableDef getMetaInfo(BufferedReader reader, File src) throws IOExc meta.addAttributes(attribs.toArray(new DataGroup.Attribute[attribs.size()])); } if (cols != null) { - for (DataType c : cols) { - meta.addCols(c); - } - meta.setColCount(cols.size()); + meta.setCols(cols); } if (src != null) { long totalRow = meta.getLineWidth() == 0 ? 0 : diff --git a/src/firefly/js/tables/SortInfo.js b/src/firefly/js/tables/SortInfo.js index b7a8064b73..e12e74602d 100644 --- a/src/firefly/js/tables/SortInfo.js +++ b/src/firefly/js/tables/SortInfo.js @@ -17,10 +17,16 @@ export const UNSORTED = ''; **/ export class SortInfo { constructor(direction=UNSORTED, sortColumns=[]) { - this.direction = direction !== SORT_DESC ? SORT_ASC : direction; + this.direction = direction; this.sortColumns = sortColumns; } + /** + * returns the sort direction of the given column name based on + * this SortInfo. + * @param colName + * @returns {*} + */ getDirection(colName) { if (this.sortColumns.includes(colName)) { return this.direction; @@ -33,7 +39,7 @@ export const UNSORTED = ''; const dir = this.getDirection(colName); this.direction = dir === UNSORTED ? SORT_ASC : dir === SORT_ASC ? SORT_DESC : UNSORTED; - this.sortColumns = [colName]; + this.sortColumns = UNSORTED ? [] : [colName]; return this; } diff --git a/src/firefly/js/tables/TableStore.js b/src/firefly/js/tables/TableStore.js index c680c89b13..9d093b84bc 100644 --- a/src/firefly/js/tables/TableStore.js +++ b/src/firefly/js/tables/TableStore.js @@ -11,6 +11,12 @@ import * as TblUtil from './TableUtil.js'; import {SelectInfo} from './SelectInfo.js'; export class TableStore { + + /** + * + * @param props props from Table + * @param changeListener function called when data changed + */ constructor(props, changeListener) { this.init(props); this.changeListener = changeListener; @@ -39,15 +45,17 @@ export class TableStore { } } + onUnmount() {} + onSort(sortInfoString) { - const {request} = this.cState.tableModel; - request.sortInfo = sortInfoString; + var {request} = this.cState.tableModel; + request = Object.assign({}, request, {sortInfo: sortInfoString}); this.handleAction(TblCntlr.TABLE_FETCH, {request}); } onFilter(filterIntoString) { - const {request} = this.cState.tableModel; - request.filters = filterIntoString; + var {request} = this.cState.tableModel; + request = Object.assign({}, request, {filters: filterIntoString}); this.handleAction(TblCntlr.TABLE_FETCH, {request}); } @@ -93,15 +101,14 @@ export class TableStore { this.handleAction(TblCntlr.TABLE_SELECT, {tbl_id, selectInfo}); } - onOptionUpdate({pageSize, columns, showUnits, showFilters}) { + onOptionUpdate({pageSize, columns, showUnits, showFilters, colSortDir}) { if (pageSize) { this.onPageSizeChange(pageSize); - } else { - const changes = omitBy({columns, showUnits, showFilters}, isUndefined); - if ( !isEmpty(changes) ) { - changes.tbl_ui_id = this.cState.tbl_ui_id; - this.handleAction(TblUiCntlr.TBL_UI_STATE_UPDATE, changes); - } + } + const changes = omitBy({columns, showUnits, showFilters, colSortDir}, isUndefined); + if ( !isEmpty(changes) ) { + changes.tbl_ui_id = this.cState.tbl_ui_id; + this.handleAction(TblUiCntlr.TBL_UI_STATE_UPDATE, changes); } } @@ -116,17 +123,25 @@ export class TableStore { } handleAction(type, payload) { + var {tableModel, origTableModel} = this.cState; + if (! origTableModel && tableModel) { + // for localStore + // need to save orig tableModel when user wants to reset. + this.cState.origTableModel = cloneDeep(tableModel); + origTableModel = this.cState.origTableModel; + } switch (type) { - case (TblCntlr.TABLE_FETCH_UPDATE) : - throw new Error('sorting and filtering is not implemented for localstore, yet.'); + case (TblCntlr.TABLE_FETCH) : + if (get(payload, ['request', 'sortInfo']) !== get(tableModel, ['request', 'sortInfo'])) { + tableModel = TblUtil.sortTable(origTableModel, payload.request.sortInfo); + } + this.updateState({tableModel}); break; default: - var tableModel = Object.assign({}, this.cState.tableModel, payload); + tableModel = Object.assign({}, this.cState.tableModel, payload); this.updateState({tableModel}); } - - } /** diff --git a/src/firefly/js/tables/TableUtil.js b/src/firefly/js/tables/TableUtil.js index 1aecab067e..c5b178825d 100644 --- a/src/firefly/js/tables/TableUtil.js +++ b/src/firefly/js/tables/TableUtil.js @@ -2,10 +2,11 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {get, isEmpty, uniqueId, padEnd, cloneDeep} from 'lodash'; +import {get, set, isEmpty, uniqueId, padEnd, cloneDeep} from 'lodash'; import * as TblCntlr from './TablesCntlr.js'; import * as TblUiCntlr from './TablesUiCntlr.js'; import {SelectInfo} from './SelectInfo.js'; +import {SortInfo, SORT_ASC, UNSORTED} from './SortInfo.js'; import {flux} from '../Firefly.js'; import {fetchUrl, encodeServerUrl} from '../util/WebUtil.js'; import {getRootPath, getRootURL} from '../util/BrowserUtil.js'; @@ -142,12 +143,16 @@ export function isFullyLoaded(tbl_id) { return isTableLoaded(findTblById(tbl_id)); } +export function findColumnIdx(tableModel, colName) { + const cols = get(tableModel, 'tableData.columns', []); + return cols.findIndex((col) => { + return col.name === colName; + }); +} + export function getCellValue(tableModel, rowIdx, colName) { - if (tableModel.tableData && tableModel.tableData.data) { - const cols = tableModel.tableData.columns; - const colIdx = cols.findIndex((col) => { - return col.name === colName; - }); + if (get(tableModel, 'tableData.data.length', 0) > 0) { + const colIdx = findColumnIdx(tableModel, colName); // might be undefined if row is not loaded return get(tableModel.tableData.data, [rowIdx, colIdx]); } @@ -228,6 +233,41 @@ export function smartMerge(target, source) { } } +/** + * sort the given tableModel based on the given request + * @param origTableModel original table model. this is returned when direction is UNSORTED. + * @param sortInfoStr + */ +export function sortTable(origTableModel, sortInfoStr) { + const sortInfoCls = SortInfo.parse(sortInfoStr); + const colName = get(sortInfoCls, 'sortColumns.0'); + const dir = get(sortInfoCls, 'direction', UNSORTED); + if (dir === UNSORTED || get(origTableModel, 'tableData.data.length', 0) === 0) return origTableModel; + + const multiplier = dir === SORT_ASC ? 1 : -1; + const colIdx = findColumnIdx(origTableModel, colName); + const col = get(origTableModel, ['tableData','columns', colIdx]); + + var tableModel = cloneDeep(origTableModel); + set(tableModel, 'request.sortInfo', sortInfoStr); + + var comparator; + if (!col.type || ['char', 'c'].includes(col.type) ) { + comparator = (r1, r2) => { + const [s1, s2] = [r1[colIdx], r2[colIdx]]; + return multiplier * (s1 > s2 ? 1 : -1); + }; + } else { + comparator = (r1, r2) => { + const [v1, v2] = [r1[colIdx], r2[colIdx]]; + return multiplier * (Number(v1) - Number(v2)); + }; + } + tableModel.tableData.data.sort(comparator); + return tableModel; +} + + export function gatherTableState(tableModel) { var {tbl_id, highlightedRow} = tableModel; @@ -262,6 +302,19 @@ export function tableToText(columns, dataAry, showUnits=false) { return textHead + '\n' + textData; } +export function prepareTableData(tableModel) { + if (!tableModel.tableData.columns) return {}; + const selectInfo = get(tableModel, 'selectInfo', {}); + const {startIdx, endIdx, hlRowIdx, currentPage, pageSize,totalPages} = gatherTableState(tableModel); + var data = tableModel.tableData.data.slice(startIdx, endIdx); + var tableRowCount = data.length; + const filterInfo = get(tableModel, 'request.filters'); + const filterCount = filterInfo ? filterInfo.split(';').length : 0; + const sortInfo = get(tableModel, 'request.sortInfo'); + + return {startIdx, hlRowIdx, currentPage, pageSize,totalPages, tableRowCount, sortInfo, selectInfo, filterInfo, filterCount, data}; +} + /** * diff --git a/src/firefly/js/tables/ui/BasicTable.jsx b/src/firefly/js/tables/ui/BasicTable.jsx index 814b7d433f..5cda0e3757 100644 --- a/src/firefly/js/tables/ui/BasicTable.jsx +++ b/src/firefly/js/tables/ui/BasicTable.jsx @@ -2,59 +2,33 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import React, {PropTypes} from 'react'; +import React, {Component, PropTypes} from 'react'; import sCompare from 'react-addons-shallow-compare'; -import FixedDataTable from 'fixed-data-table'; -import Resizable from 'react-component-resizable'; -import {debounce, get, isEmpty, pick} from 'lodash'; +import {isEmpty} from 'lodash'; +import * as TblUtil from '../TableUtil.js'; +import {BasicTableView} from './BasicTableView.jsx'; +import {RemoteTableStore, TableStore} from '../TableStore.js'; import {SelectInfo} from '../SelectInfo.js'; -import {FilterInfo} from '../FilterInfo.js'; -import {SortInfo} from '../SortInfo'; -import {tableToText} from '../TableUtil.js'; -import {TextCell, HeaderCell} from './TableRenderer.js'; -import './TablePanel.css'; - -const {Table, Column} = FixedDataTable; - -export class BasicTable extends React.Component { +export class BasicTable extends Component { constructor(props) { super(props); - this.state = { - showMask: false, - widthPx: 75, - heightPx: 75, - columnWidths: makeColWidth(props.columns, props.data, props.showUnits) - }; - - this.onResize = debounce((size) => { - if (size) { - var widthPx = size.width; - var heightPx = size.height; - this.setState({widthPx, heightPx}); - } - }, 100); - - this.onColumnResizeEndCallback = this.onColumnResizeEndCallback.bind(this); - this.rowClassName = this.rowClassName.bind(this); - } - onColumnResizeEndCallback(newColumnWidth, columnKey) { - var columnWidths = Object.assign({}, this.state.columnWidths, {[columnKey]: newColumnWidth}); - this.setState({columnWidths}); + if (props.tbl_id) { + this.tableStore = RemoteTableStore.newInstance(props, (v) => this.setState(v)); + } else if (props.tableModel) { + this.tableStore = TableStore.newInstance(props, (v) => this.setState(v)); + } + this.state = this.tableStore.cState; } - rowClassName(index) { - const {hlRowIdx} = this.props; - return (hlRowIdx === index) ? 'tablePanel__Row_highlighted' : ''; + componentWillReceiveProps(nProps) { + this.tableStore.init(nProps); } - componentWillReceiveProps(nProps) { - if (isEmpty(this.state.columnWidths) && !isEmpty(nProps.columns)) { - this.setState({columnWidths: makeColWidth(nProps.columns, nProps.data, nProps.showUnits)}); - } - this.setState({showMask: false}); + componentWillUnmount() { + this.tableStore && this.tableStore.onUnmount(); } shouldComponentUpdate(nProps, nState) { @@ -62,168 +36,54 @@ export class BasicTable extends React.Component { } render() { - const {columns, data, hlRowIdx, showUnits, showFilters, filterInfo, - sortInfo, tableStore, textView, rowHeight} = this.props; - const {widthPx, heightPx, columnWidths, showMask} = this.state; + var {tableModel, columns, showUnits, showFilters, textView} = this.state; + const {selectable, border, renderers} = this.props; + const {tableStore} = this; + if (isEmpty(columns) || isEmpty(tableModel)) return false; + const {hlRowIdx, selectInfo, filterInfo, sortInfo, data} = TblUtil.prepareTableData(tableModel); + const selectInfoCls = SelectInfo.newInstance(selectInfo, 0); - if (isEmpty(columns)) return (
); - - const filterInfoCls = FilterInfo.parse(filterInfo); - const sortInfoCls = SortInfo.parse(sortInfo); - - const showMaskNow = () => this.setState({showMask: true}); - const onSelect = (checked, rowIndex) => tableStore.onRowSelect && tableStore.onRowSelect(checked, rowIndex); - const onSelectAll = (checked) => tableStore.onSelectAll && tableStore.onSelectAll(checked); - const onSort = (cname) => { - showMaskNow(); - tableStore.onSort && tableStore.onSort(sortInfoCls.toggle(cname).serialize()); - }; - - const onFilter = ({fieldKey, valid, value}) => { - if (valid && !filterInfoCls.isEqual(fieldKey, value)) { - filterInfoCls.setFilter(fieldKey, value); - showMaskNow(); - tableStore.onFilter && tableStore.onFilter(filterInfoCls.serialize()); - } - }; - - const colProps = pick(this.props, ['columns', 'data', 'selectable', 'selectInfoCls', 'tableStore', 'renderers']); - Object.assign(colProps, {columnWidths, filterInfoCls, sortInfoCls, showUnits, showFilters}, {onSort, onFilter, onSelect, onSelectAll}); - - const headerHeight = 22 + (showUnits && 12) + (showFilters && 20); return ( - - { textView ? : - tableStore.onRowHighlight && tableStore.onRowHighlight(index)} - rowClassNameGetter={this.rowClassName} - scrollToRow={hlRowIdx} - width={widthPx} - height={heightPx}> - { makeColumns(colProps) } -
- } - {showMask &&
} - {isEmpty(data) &&
No Data Found
} - +
+
+ +
+
); } } BasicTable.propTypes = { - columns: PropTypes.arrayOf(PropTypes.object), - data: PropTypes.arrayOf(PropTypes.array), - hlRowIdx: PropTypes.number, - selectInfoCls: PropTypes.instanceOf(SelectInfo), - filterInfo: PropTypes.string, - sortInfo: PropTypes.string, - selectable: PropTypes.bool, + tbl_id: PropTypes.string, + tbl_ui_id: PropTypes.string, + tableModel: PropTypes.object, showUnits: PropTypes.bool, showFilters: PropTypes.bool, - textView: PropTypes.bool, - rowHeight: PropTypes.number, + selectable: PropTypes.bool, + border: PropTypes.bool, renderers: PropTypes.objectOf( PropTypes.shape({ cellRenderer: PropTypes.func, headRenderer: PropTypes.func }) - ), - tableStore: PropTypes.shape({ - onRowHighlight: PropTypes.func, - onRowSelect: PropTypes.func, - onSelectAll: PropTypes.func, - onSort: PropTypes.func, - onFilter: PropTypes.func - }) + ) }; BasicTable.defaultProps = { + tbl_ui_id: TblUtil.uniqueTblUiId(), selectable: false, - showUnits: false, - showFilters: false, - rowHeight: 20 + border: true }; - -const TextView = ({columns, data, showUnits, widthPx, heightPx}) => { - const text = tableToText(columns, data, showUnits); - return ( -
-
-
{text}
-
-
- ); -}; - -function makeColWidth(columns, data, showUnits) { - return !columns ? {} : columns.reduce((widths, col, cidx) => { - const label = col.name; - var nchar = col.prefWidth; - const unitLength = showUnits ? get(col, 'units.length', 0) : 0; - if (!nchar) { - nchar = Math.max(label.length, unitLength, get(data, `0.${cidx}.length`, 0)); - } - widths[col.name] = nchar * 8 + 20; // 20 is for the padding and sort symbol - return widths; - }, {}); -} - -function makeColumns ({columns, columnWidths, data, selectable, showUnits, showFilters, renderers, - selectInfoCls, filterInfoCls, sortInfoCls, onSelect, onSelectAll, onSort, onFilter}) { - if (!columns) return false; - - var colsEl = columns.map((col, idx) => { - if (col.visibility !== 'show') return false; - const HeadRenderer = get(renderers, [col.name, 'headRenderer'], HeaderCell); - const CellRenderer = get(renderers, [col.name, 'cellRenderer'], TextCell); - - return ( - } - cell={} - fixed={false} - width={columnWidths[col.name]} - isResizable={true} - allowCellsRecycling={true} - /> - ); - }); - if (selectable) { - const headerCB = () => { - return ( -
- onSelectAll(e.target.checked)}/> -
- ); - }; - - const cellCB = ({rowIndex}) => { - const onRowSelect = (e) => onSelect(e.target.checked, rowIndex); - return ( -
- -
- ); - }; - - var cbox = ; - colsEl.splice(0, 0, cbox); - } - return colsEl; -} - diff --git a/src/firefly/js/tables/ui/BasicTableView.jsx b/src/firefly/js/tables/ui/BasicTableView.jsx new file mode 100644 index 0000000000..da8e6f7537 --- /dev/null +++ b/src/firefly/js/tables/ui/BasicTableView.jsx @@ -0,0 +1,233 @@ +/* + * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt + */ + +import React, {PropTypes} from 'react'; +import sCompare from 'react-addons-shallow-compare'; +import FixedDataTable from 'fixed-data-table'; +import Resizable from 'react-component-resizable'; +import {debounce, get, isEmpty, pick} from 'lodash'; + +import {SelectInfo} from '../SelectInfo.js'; +import {FilterInfo} from '../FilterInfo.js'; +import {SortInfo} from '../SortInfo'; +import {tableToText} from '../TableUtil.js'; +import {TextCell, HeaderCell} from './TableRenderer.js'; + +import './TablePanel.css'; + +const {Table, Column} = FixedDataTable; + +export class BasicTableView extends React.Component { + constructor(props) { + super(props); + this.state = { + showMask: false, + widthPx: 75, + heightPx: 75, + columnWidths: makeColWidth(props.columns, props.data, props.showUnits) + }; + + this.onResize = debounce((size) => { + if (size) { + var widthPx = size.width; + var heightPx = size.height; + this.setState({widthPx, heightPx}); + } + }, 100); + + this.onColumnResizeEndCallback = this.onColumnResizeEndCallback.bind(this); + this.rowClassName = this.rowClassName.bind(this); + } + + onColumnResizeEndCallback(newColumnWidth, columnKey) { + var columnWidths = Object.assign({}, this.state.columnWidths, {[columnKey]: newColumnWidth}); + this.setState({columnWidths}); + } + + rowClassName(index) { + const {hlRowIdx} = this.props; + return (hlRowIdx === index) ? 'tablePanel__Row_highlighted' : ''; + } + + componentWillReceiveProps(nProps) { + if (isEmpty(this.state.columnWidths) && !isEmpty(nProps.columns)) { + this.setState({columnWidths: makeColWidth(nProps.columns, nProps.data, nProps.showUnits)}); + } + this.setState({showMask: false}); + } + + shouldComponentUpdate(nProps, nState) { + return sCompare(this, nProps, nState); + } + + render() { + const {columns, data, hlRowIdx, showUnits, showFilters, filterInfo, + sortInfo, callbacks, textView, rowHeight} = this.props; + const {widthPx, heightPx, columnWidths, showMask} = this.state; + + if (isEmpty(columns)) return (
); + + const filterInfoCls = FilterInfo.parse(filterInfo); + const sortInfoCls = SortInfo.parse(sortInfo); + + const showMaskNow = () => this.setState({showMask: true}); + const onSelect = (checked, rowIndex) => callbacks.onRowSelect && callbacks.onRowSelect(checked, rowIndex); + const onSelectAll = (checked) => callbacks.onSelectAll && callbacks.onSelectAll(checked); + const onSort = (cname) => { + if (callbacks.onSort) { + showMaskNow(); + callbacks.onSort(sortInfoCls.toggle(cname).serialize()); + } + }; + + const onFilter = ({fieldKey, valid, value}) => { + if (callbacks.onFilter) { + if (valid && !filterInfoCls.isEqual(fieldKey, value)) { + filterInfoCls.setFilter(fieldKey, value); + showMaskNow(); + callbacks.onFilter(filterInfoCls.serialize()); + } + } + }; + + const colProps = pick(this.props, ['columns', 'data', 'selectable', 'selectInfoCls', 'callbacks', 'renderers']); + Object.assign(colProps, {columnWidths, filterInfoCls, sortInfoCls, showUnits, showFilters}, {onSort, onFilter, onSelect, onSelectAll}); + + const headerHeight = 22 + (showUnits && 12) + (showFilters && 20); + return ( + + { textView ? : + callbacks.onRowHighlight && callbacks.onRowHighlight(index)} + rowClassNameGetter={this.rowClassName} + scrollToRow={hlRowIdx} + width={widthPx} + height={heightPx}> + { makeColumns(colProps) } +
+ } + {showMask &&
} + {isEmpty(data) &&
No Data Found
} + + ); + } +} + +BasicTableView.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object), + data: PropTypes.arrayOf(PropTypes.array), + hlRowIdx: PropTypes.number, + selectInfoCls: PropTypes.instanceOf(SelectInfo), + filterInfo: PropTypes.string, + sortInfo: PropTypes.string, + selectable: PropTypes.bool, + showUnits: PropTypes.bool, + showFilters: PropTypes.bool, + textView: PropTypes.bool, + rowHeight: PropTypes.number, + renderers: PropTypes.objectOf( + PropTypes.shape({ + cellRenderer: PropTypes.func, + headRenderer: PropTypes.func + }) + ), + callbacks: PropTypes.shape({ + onRowHighlight: PropTypes.func, + onRowSelect: PropTypes.func, + onSelectAll: PropTypes.func, + onSort: PropTypes.func, + onFilter: PropTypes.func + }) +}; + +BasicTableView.defaultProps = { + selectable: false, + showUnits: false, + showFilters: false, + rowHeight: 20 +}; + +const TextView = ({columns, data, showUnits, widthPx, heightPx}) => { + const text = tableToText(columns, data, showUnits); + return ( +
+
+
{text}
+
+
+ ); +}; + +function makeColWidth(columns, data, showUnits) { + return !columns ? {} : columns.reduce((widths, col, cidx) => { + const label = col.name; + var nchar = col.prefWidth; + const unitLength = showUnits ? get(col, 'units.length', 0) : 0; + if (!nchar) { + nchar = Math.max(label.length, unitLength, get(data, `0.${cidx}.length`, 0)); + } + widths[col.name] = nchar * 8 + 20; // 20 is for the padding and sort symbol + return widths; + }, {}); +} + +function makeColumns ({columns, columnWidths, data, selectable, showUnits, showFilters, renderers, + selectInfoCls, filterInfoCls, sortInfoCls, onSelect, onSelectAll, onSort, onFilter}) { + if (!columns) return false; + + var colsEl = columns.map((col, idx) => { + if (col.visibility !== 'show') return false; + const HeadRenderer = get(renderers, [col.name, 'headRenderer'], HeaderCell); + const CellRenderer = get(renderers, [col.name, 'cellRenderer'], TextCell); + + return ( + } + cell={} + fixed={false} + width={columnWidths[col.name]} + isResizable={true} + allowCellsRecycling={true} + /> + ); + }); + if (selectable) { + const headerCB = () => { + return ( +
+ onSelectAll(e.target.checked)}/> +
+ ); + }; + + const cellCB = ({rowIndex}) => { + const onRowSelect = (e) => onSelect(e.target.checked, rowIndex); + return ( +
+ +
+ ); + }; + + var cbox = ; + colsEl.splice(0, 0, cbox); + } + return colsEl; +} + diff --git a/src/firefly/js/tables/ui/TablePanel.jsx b/src/firefly/js/tables/ui/TablePanel.jsx index 82dae1089d..cfb980154f 100644 --- a/src/firefly/js/tables/ui/TablePanel.jsx +++ b/src/firefly/js/tables/ui/TablePanel.jsx @@ -4,12 +4,12 @@ import React, {Component, PropTypes} from 'react'; import sCompare from 'react-addons-shallow-compare'; -import {isEmpty, get, cloneDeep, omitBy, isUndefined} from 'lodash'; +import {isEmpty} from 'lodash'; import {download} from '../../util/WebUtil.js'; import * as TblUtil from '../TableUtil.js'; import {TablePanelOptions} from './TablePanelOptions.jsx'; -import {BasicTable} from './BasicTable.jsx'; +import {BasicTableView} from './BasicTableView.jsx'; import {RemoteTableStore, TableStore} from '../TableStore.js'; import {SelectInfo} from '../SelectInfo.js'; import {InputField} from '../../ui/InputField.jsx'; @@ -47,12 +47,12 @@ export class TablePanel extends Component { } render() { - var {tableModel, columns, showOptions, showUnits, showFilters, textView} = this.state; + var {tableModel, columns, showOptions, showUnits, showFilters, textView, colSortDir} = this.state; const {selectable, expandable, expandedMode, border, renderers} = this.props; const {tableStore} = this; if (isEmpty(columns) || isEmpty(tableModel)) return false; const {startIdx, hlRowIdx, currentPage, pageSize, totalPages, tableRowCount, selectInfo, - filterInfo, filterCount, sortInfo, data} = prepareTableData(tableModel); + filterInfo, filterCount, sortInfo, data} = TblUtil.prepareTableData(tableModel); const selectInfoCls = SelectInfo.newInstance(selectInfo, startIdx); const viewIcoStyle = 'tablepanel ' + (textView ? 'tableView' : 'textView'); @@ -86,7 +86,7 @@ export class TablePanel extends Component {
- {showOptions && { return (
dispatchSetLayoutMode(LO_EXPANDED.none)}/>
diff --git a/src/firefly/js/tables/ui/TablePanelOptions.jsx b/src/firefly/js/tables/ui/TablePanelOptions.jsx index 5983591ab5..fb4b1de5f9 100644 --- a/src/firefly/js/tables/ui/TablePanelOptions.jsx +++ b/src/firefly/js/tables/ui/TablePanelOptions.jsx @@ -3,63 +3,23 @@ */ import React from 'react'; -import {isEmpty, cloneDeep} from 'lodash'; +import {isEmpty, cloneDeep, get} from 'lodash'; -import {BasicTable} from './BasicTable.jsx'; +import {BasicTableView} from './BasicTableView.jsx'; import {SelectInfo} from '../SelectInfo.js'; +import {SortInfo, SORT_ASC} from '../SortInfo.js'; +import {sortTable} from '../TableUtil.js'; import {InputField} from '../../ui/InputField.jsx'; import {intValidator} from '../../util/Validate.js'; -function prepareOptionData(columns) { - - var data = columns.map( (v, idx) => { - return [v.name]; - } ); - var cols = [{name: 'Column', visibility: 'show', prefWidth: 20}]; - var selectInfoCls = SelectInfo.newInstance({}); - selectInfoCls.data.rowCount = data.length; - columns.forEach( (v, idx) => { - selectInfoCls.setRowSelect(idx, v.visibility === 'show'); - } ); - var tableRowCount = data.length; - - return {cols, data, tableRowCount, selectInfoCls }; -} - export const TablePanelOptions = (props) => { - const {columns, pageSize, showUnits, showFilters, onChange} = props; + const {columns, pageSize, showUnits, showFilters, onChange, colSortDir} = props; if (isEmpty(columns)) return false; - var onSelectAll = (checked) => { - const nColumns = cloneDeep(columns); - nColumns.forEach((v) => { - v.visibility = checked ? 'show' : 'hide'; - }); - onChange && onChange({columns: nColumns}); - }; - - var onRowSelect = (checked, rowIdx) => { - const nColumns = cloneDeep(columns); - nColumns[rowIdx].visibility = checked ? 'show' : 'hide'; - onChange && onChange({columns: nColumns}); - }; - - var onPageSize = (pageSize) => { - if (pageSize.valid) { - onChange && onChange({pageSize: pageSize.value}); - } - }; - - var onPropChanged = (v, prop) => { - onChange && onChange({[prop]: v}); - }; - - var onReset = () => { - onChange && onChange({pageSize: 50, showUnits: false, showFilters: false, columns: []}); - }; - - const {cols, data, selectInfoCls} = prepareOptionData(columns); + const {cols, data, sortInfo, selectInfoCls} = prepareOptionData(columns, colSortDir); + const callbacks = makeCallbacks(onChange, columns, data); + const {onPageSize, onPropChanged, onReset, ...tableCallbacks} = callbacks; return (
@@ -85,13 +45,14 @@ export const TablePanelOptions = (props) => {
-
@@ -100,10 +61,78 @@ export const TablePanelOptions = (props) => { TablePanelOptions.propTypes = { columns: React.PropTypes.arrayOf(React.PropTypes.object), + colSortDir: React.PropTypes.oneOf(['ASC', 'DESC', '']), pageSize: React.PropTypes.number, showUnits: React.PropTypes.bool, showFilters: React.PropTypes.bool, onChange: React.PropTypes.func }; +function prepareOptionData(columns, colSortDir) { + + if (colSortDir) { + // sort the columns according to colSortDir + const multiplier = colSortDir === SORT_ASC ? 1 : -1; + const comparator = (r1, r2) => { + const [s1, s2] = [r1.name, r2.name]; + return multiplier * (s1 > s2 ? 1 : -1); + }; + columns = columns.slice(); // shallow clone + columns.sort(comparator); + } + var data = columns.map( (v, idx) => { + return [v.name]; + } ); + + var cols = [{name: 'Column', visibility: 'show', prefWidth: 20}]; + const sortInfo = SortInfo.newInstance(colSortDir, 'Column').serialize(); + + var selectInfoCls = SelectInfo.newInstance({}); + selectInfoCls.data.rowCount = data.length; + columns.forEach( (v, idx) => { + selectInfoCls.setRowSelect(idx, v.visibility === 'show'); + } ); + var tableRowCount = data.length; + + return {cols, data, tableRowCount, selectInfoCls, sortInfo}; +} + +function makeCallbacks(onChange, columns, data) { + var onSelectAll = (checked) => { + const nColumns = cloneDeep(columns); + nColumns.forEach((v) => { + v.visibility = checked ? 'show' : 'hide'; + }); + onChange && onChange({columns: nColumns}); + }; + + var onRowSelect = (checked, rowIdx) => { + const selColName = get(data, [rowIdx, 0]); + const nColumns = cloneDeep(columns); + const selCol = nColumns.find((col) => col.name === selColName); + selCol && (selCol.visibility = checked ? 'show' : 'hide'); + onChange && onChange({columns: nColumns}); + }; + + var onPageSize = (pageSize) => { + if (pageSize.valid) { + onChange && onChange({pageSize: pageSize.value}); + } + }; + + var onSort = (sortInfoString) => { + const colSortDir = SortInfo.parse(sortInfoString).direction; + onChange && onChange({colSortDir}); + }; + + var onPropChanged = (v, prop) => { + onChange && onChange({[prop]: v}); + }; + + var onReset = () => { + onChange && onChange({pageSize: 50, showUnits: false, showFilters: false, columns: null}); + }; + + return {onSelectAll, onRowSelect, onPageSize, onSort, onPropChanged, onReset}; +} diff --git a/src/firefly/js/ui/DropDownContainer.css b/src/firefly/js/ui/DropDownContainer.css index 1a8cdd60d9..8328e822fd 100644 --- a/src/firefly/js/ui/DropDownContainer.css +++ b/src/firefly/js/ui/DropDownContainer.css @@ -1,6 +1,6 @@ .DD-ToolBar { display: flex; - z-index: 10; + z-index: 110; position: absolute; width: 100%; background-color: rgba(200,200,200,0.92); diff --git a/src/firefly/js/visualize/ui/FitsHeaderView.jsx b/src/firefly/js/visualize/ui/FitsHeaderView.jsx index 936f721083..ee34e2cf60 100644 --- a/src/firefly/js/visualize/ui/FitsHeaderView.jsx +++ b/src/firefly/js/visualize/ui/FitsHeaderView.jsx @@ -15,7 +15,6 @@ import CompleteButton from '../../ui/CompleteButton.jsx'; import {getSizeAsString} from '../../util/WebUtil.js'; import HelpIcon from '../../ui/HelpIcon.jsx'; import {SortInfo} from '../../tables/SortInfo'; -import {TableStore} from '../../tables/TableStore.js'; import Band from '../Band.js'; @@ -142,7 +141,7 @@ function render3BandFitsHeaders(plot, fitsHeaderInfo) { function getFileSizeAndPixelSize(plot, band, fitsHeaderInfo) { - const tableModel = JSON.parse(fitsHeaderInfo[band]); + const tableModel = fitsHeaderInfo[band]; const pt = plot.projection.getPixelScaleArcSec(); const pixelSize = pt.toFixed(2) + '"'; const meta = tableModel.tableMeta; @@ -155,39 +154,6 @@ function getFileSizeAndPixelSize(plot, band, fitsHeaderInfo) { ); } -/** - * this method prepare the data needs to make the table - * @param band - * @param fitsHeaderInfo - * @returns {{columns: *, data: *, sortInfo: (*|string|{id, name, isReady}), tableStore: TableStore}} - */ -function prepareData(band, fitsHeaderInfo) { - const tableModel = JSON.parse(fitsHeaderInfo[band]); - - const tableData = tableModel.tableData; - const data = tableData.data; - const columns = tableData.columns; - const meta = tableModel.tableMeta; - var columnNames = []; - for (var i = 0; i < columns.length; i++) { - columnNames[i] = columns[i].name; - } - const sortInfo = SortInfo.newInstance('', columnNames).serialize(); - - /*var request = {}; - request['sortInfo'] = sortInfo; - meta['request'] = request; - //var newTableModel = Object.assign({}, tableModel, {request:request }); - - tableModel['request'] = request; - - const tableStore = TableStore.newInstance(tableModel);//this is not working - return {columns, data, sortInfo, tableStore}; - - const tableStore = TableStore.newInstance(tableModel);//this is not working*/ - return {columns, data, sortInfo};//, tableStore}; -} - /** * display the data into a tabular format * @param band @@ -195,17 +161,12 @@ function prepareData(band, fitsHeaderInfo) { * @returns {XML} */ function getTable(band, fitsHeaderInfo) { - - - // const {columns, data, sortInfo, tableStore} = prepareData(band, fitsHeaderInfo); - const {columns, data, sortInfo} = prepareData(band, fitsHeaderInfo); + const tableModel = fitsHeaderInfo[band]; return ( ); }