/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
import {get, set, has, isEmpty, isUndefined, uniqueId, cloneDeep, omitBy, isNil, isPlainObject, isArray, padEnd} from 'lodash';
import Enum from 'enum';
import {makeFileRequest, MAX_ROW} from './TableRequestUtil.js';
import * as TblCntlr from './TablesCntlr.js';
import {SortInfo, SORT_ASC, UNSORTED} from './SortInfo.js';
import {FilterInfo} from './FilterInfo.js';
import {SelectInfo} from './SelectInfo.js';
import {flux} from '../Firefly.js';
import {encodeServerUrl, uniqueID} from '../util/WebUtil.js';
import {fetchTable, queryTable, selectedValues} from '../rpc/SearchServicesJson.js';
import {DEF_BASE_URL} from '../core/JsonUtils.js';
import {ServerParams} from '../data/ServerParams.js';
import {doUpload} from '../ui/FileUpload.jsx';
import {dispatchAddActionWatcher, dispatchCancelActionWatcher} from '../core/MasterSaga.js';
import {getWsConnId} from '../core/messaging/WebSocketClient.js';
import {MetaConst} from '../data/MetaConst';
// this is so test can mock the function when used within it's module
const local = {
isTableLoaded,
getTblById,
getTblInfoById,
getColumn,
getCellValue,
getSelectedData
};
export default local;
export const COL_TYPE = new Enum(['ALL', 'NUMBER', 'TEXT']);
const char_types = ['char', 'c', 's', 'str'];
const num_types = ['double', 'd', 'long', 'l', 'int', 'i', 'float', 'f'];
/**
* tableRequest will be sent to the server as a json string.
* @param {TableRequest} tableRequest is a table request params object
* @param {number} [hlRowIdx] set the highlightedRow. default to startIdx.
* @returns {Promise.<TableModel>}
* @public
* @func doFetchTable
* @memberof firefly.util.table
*/
export function doFetchTable(tableRequest, hlRowIdx) {
const {tbl_id} = tableRequest;
const tableModel = getTblById(tbl_id) || {};
if (tableModel.origTableModel) {
return Promise.resolve(processRequest(tableModel, tableRequest, hlRowIdx));
} else {
return fetchTable(tableRequest, hlRowIdx);
}
}
/**
* return a promise of a tableModel for the given tbl_id.
* @param {string} tbl_id the table ID to watch for.
* @returns {Promise.<TableModel>}
* @public
* @func onTableLoad
* @memberof firefly.util.table
*/
export function onTableLoaded(tbl_id) {
if (isFullyLoaded(tbl_id)) {
return Promise.resolve(getTblById(tbl_id));
} else {
return new Promise((resolve) => {
dispatchAddActionWatcher({
actions:[TblCntlr.TABLE_UPDATE, TblCntlr.TABLE_REPLACE],
callback: doOnTblLoaded,
params: {tbl_id, resolve}
});
});
}
}
/**
* returns true is there is data within the given range. this is needed because
* of paging table not loading the full table.
* @param {number} startIdx
* @param {number} endIdx
* @param {TableModel} tableModel
* @returns {boolean}
* @func isTblDataAvail
* @memberof firefly.util.table
*/
export function isTblDataAvail(startIdx, endIdx, tableModel) {
if (!tableModel) return false;
endIdx = endIdx >0 ? Math.min( endIdx, tableModel.totalRows) : startIdx;
if (startIdx >=0 && endIdx > startIdx) {
const data = get(tableModel, 'tableData.data', []);
const dataCount = Object.keys(data.slice(startIdx, endIdx)).length;
return dataCount === (endIdx-startIdx);
} else return false;
}
/**
* returns the table model with the given tbl_id
* @param tbl_id
* @returns {TableModel}
* @public
* @func getTblById
* @memberof firefly.util.table
*/
export function getTblById(tbl_id) {
return get(flux.getState(),[TblCntlr.TABLE_SPACE_PATH, 'data', tbl_id]);
}
/**
* returns all table group IDs
* @returns {string[]}
* @memberof firefly.util.table
* @func getAllTableGroupIds
*/
export function getAllTableGroupIds() {
const groups = get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'results']) || {};
return Object.keys(groups);
}
/**
* returns the table group information
* @param {string} tbl_group the group name to look for
* @returns {TableGroup}
* @public
* @memberof firefly.util.table
* @func getTableGroup
*/
export function getTableGroup(tbl_group='main') {
return get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'results', tbl_group]);
}
/**
* returns the table group name given a tbl_id. it will return undefined if
* the given tbl_id is not in a group.
* @param {string} tbl_id table id
* @returns {TableGroup}
* @public
* @memberof firefly.util.table
* @func findGroupByTblId
*/
export function findGroupByTblId(tbl_id) {
const resultsRoot = get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'results'], {});
const groupName = Object.keys(resultsRoot).find( (tbl_grp_id) => {
return has(resultsRoot, [tbl_grp_id, 'tables', tbl_id]);
});
return groupName;
}
export function removeTablesFromGroup(tbl_group_id = 'main') {
const tblAry = getTblIdsByGroup(tbl_group_id);
tblAry && tblAry.forEach((tbl_id) => {
TblCntlr.dispatchTableRemove(tbl_id, false); // all table will be removed. not need to fireActiveTableChanged
});
}
/**
* returns an array of tbl_id for the given tbl_group_id
* @param {string} tbl_group_id table group name. defaults to 'main' if not given
* @returns {String[]} array of tbl_id
* @public
* @func getTblIdsByGroup
* @memberof firefly.util.table
*/
export function getTblIdsByGroup(tbl_group_id = 'main') {
const tableGroup = get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'results', tbl_group_id]);
return Object.keys(get(tableGroup, 'tables', []));
}
/**
* returns the table information for the given id and group.
* @param {string} tbl_id table id.
* @param {string} tbl_group table group name. defaults to 'main' if not given
* @returns {TableModel}
* @public
* @func getTableInGroup
* @memberof firefly/util/table
*/
export function getTableInGroup(tbl_id, tbl_group='main') {
return get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'results', tbl_group, 'tables', tbl_id]);
}
/**
* get the table working state by tbl_ui_id
* @param {string} tbl_ui_id table UI id.
* @returns {Object}
* @memberof firefly.util.table
* @func getTableUiById
*/
export function getTableUiById(tbl_ui_id) {
return get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'ui', tbl_ui_id]);
}
/**
* returns the first table working state for the given tbl_id
* @param {string} tbl_id
* @returns {Object}
* @public
* @func getTableUiByTblId
* @memberof firefly.util.table
*/
export function getTableUiByTblId(tbl_id) {
const uiRoot = get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'ui'], {});
return Object.keys(uiRoot).find( (ui_id) => get(uiRoot, [ui_id, 'tbl_id']) === tbl_id);
}
/**
* returns the working state of the currently expanded table.
* @returns {Object}
* @memberof firefly.util.table
* @func getTblExpandedInfo
*/
export function getTblExpandedInfo() {
return get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH, 'ui', 'expanded'], {});
}
/**
* returns true if the table referenced by the given tbl_id is fully loaded.
* @param {string} tbl_id
* @returns {boolean}
* @memberof firefly.util.table
* @func isFullyLoaded
*/
export function isFullyLoaded(tbl_id) {
return isTableLoaded(getTblById(tbl_id));
}
/**
* return the resultSetID of this table.
* This is a unique ID used to identify the resultset on the server used to populate this table.
* @see TableRequestUtil.setResultSetID for the reverse
* @param {string} tbl_id
* @returns {string}
*/
export function getResultSetID(tbl_id) {
return get(getTblById(tbl_id), 'tableMeta.resultSetID', '');
}
/**
* return the resultSetRequest of this table.
* This is a JSON string of the request used to created this table
* @see TableRequestUtil.setResultSetRequest for the reverse
* @param {string} tbl_id
* @returns {string}
*/
export function getResultSetRequest(tbl_id) {
return get(getTblById(tbl_id), 'tableMeta.resultSetRequest', '');
}
/**
* Returns the first index of the found row. It will search the table on the client first.
* If none is found and the table is partially loaded, it will search the server-side as well.
* @param {string} tbl_id the tbl_id of the table to search on.
* @param {string} filterInfo filter info string used to find the first row that matches it.
* @returns {Promise.<number>} Returns the index of the found row, else -1.
* @func findIndex
* @memberof firefly.util.table
*/
export function findIndex(tbl_id, filterInfo) {
const tableModel = getTblById(tbl_id);
if (!tableModel) return Promise.resolve(-1);
const comparators = filterInfo.split(';').map((s) => s.trim()).map((s) => FilterInfo.createComparator(s, tableModel));
const idx = get(tableModel, 'tableData.data', []).findIndex((row, idx) => comparators.reduce( (rval, matcher) => rval && matcher(row, idx), true));
if (idx >= 0) {
return Promise.resolve(idx);
} else {
const inclCols = 'ROW_NUM as ORG_ROWNUM';
return queryTable(tableModel.request, {filters: filterInfo, inclCols}).then( (tableModel) => {
return get(tableModel, ['tableData','data']) ? get(getColumnValues(tableModel, 'ORG_ROWNUM'), '0', -1) : -1;
});
}
}
/**
* Returns the column index with the given name; otherwise, -1.
* @param {TableModel} tableModel
* @param {string} colName
* @returns {number}
* @public
* @func getColumnIdx
* @memberof firefly.util.table
*/
export function getColumnIdx(tableModel, colName) {
const cols = getAllColumns(tableModel);
return cols.findIndex((col) => {
return col.name === colName;
});
}
/**
* Returns the column data type with the given name
* @param {TableModel} tableModel
* @param {string} colName
* @returns {string}
* @public
*/
export function getColumnType(tableModel, colName) {
const cols = getAllColumns(tableModel);
return get(cols.find((col)=> col.name === colName), 'type', '');
}
/**
* returns column information for the given name.
* @param {TableModel} tableModel
* @param {string} colName
* @returns {TableColumn}
* @public
* @func getColumn
* @memberof firefly.util.table
*/
export function getColumn(tableModel, colName) {
const colIdx = getColumnIdx(tableModel, colName);
if (colIdx >= 0) {
return get(tableModel, `tableData.columns.${colIdx}`);
}
}
/**
* returns column information for the given ID.
* @param {TableModel} tableModel
* @param {string} ID
* @returns {TableColumn}
* @public
* @func getColumn
* @memberof firefly.util.table
*/
export function getColumnByID(tableModel, ID) {
const colIdx = getColumns(tableModel).findIndex((col) => col.ID === ID);
if (colIdx >= 0) {
return get(tableModel, `tableData.columns.${colIdx}`);
}
}
export function getFilterCount(tableModel) {
const filterInfo = get(tableModel, 'request.filters');
const filterCount = filterInfo ? filterInfo.split(';').length : 0;
return filterCount;
}
export function clearFilters(tableModel) {
const {request, tbl_id} = tableModel || {};
if (request && request.filters) {
TblCntlr.dispatchTableFilter({tbl_id, filters: ''});
}
}
export function clearSelection(tableModel) {
if (tableModel) {
const selectInfoCls = SelectInfo.newInstance({rowCount: tableModel.totalRows});
TblCntlr.dispatchTableSelect(tableModel.tbl_id, selectInfoCls.data);
}
}
/**
* return the tbl_id of the active table for the given group.
* @param {string} tbl_group group name; defaults to 'main' if not given.
* @returns {string}
* @public
* @memberof firefly.util.table
* @func getActiveTableId
*/
export function getActiveTableId(tbl_group='main') {
return get(flux.getState(), [TblCntlr.TABLE_SPACE_PATH,'results',tbl_group,'active']);
}
/**
*
* @param {TableModel} tableModel
* @param {number} rowIdx
* @param {string} colName
* @return {string}
* @public
* @func getCellValue
* @memberof firefly.util.table
*/
export function getCellValue(tableModel, rowIdx, colName) {
if (get(tableModel, 'tableData.data.length', 0) > 0) {
const colIdx = getColumnIdx(tableModel, colName);
return get(tableModel, ['tableData', 'data', rowIdx, colIdx]);
}
}
/**
* returns an array of all the values for a column
* @param {TableModel} tableModel
* @param {string} colName
* @return {Object[]}
* @func getColumnValues
* @public
* @memberof firefly.util.table
*/
export function getColumnValues(tableModel, colName) {
const colIdx = getColumnIdx(tableModel, colName);
if (colIdx >= 0 && colIdx < get(tableModel, 'tableData.columns.length', 0)) {
return get(tableModel, 'tableData.data', []).map( (r) => r[colIdx]);
} else {
return [];
}
}
/**
* returns an array of all the values for a row
* @param {TableModel} tableModel
* @param {number} rowIdx
* @return {Object[]}
* @public
* @memberof firefly.util.table
* @func getRowValues
*/
export function getRowValues(tableModel, rowIdx) {
return get(tableModel, ['tableData', 'data', rowIdx], []);
}
/**
* returns an array of all the values for a columns
* @param {string} tbl_id
* @param {string[]} columnNames defaults to all columns
* @return {Promise.<TableModel>}
* @func getSelectedData
* @public
* @memberof firefly.util.table
*/
export function getSelectedData(tbl_id, columnNames=[]) {
const {tableModel, totalRows, selectInfo, request} = local.getTblInfoById(tbl_id);
const selectedRows = [...SelectInfo.newInstance(selectInfo).getSelected()]; // get selected row idx as an array
if (selectedRows.length === 0 || isTblDataAvail(0, totalRows -1, tableModel)) {
return Promise.resolve(getSelectedDataSync(tbl_id, columnNames));
} else {
return selectedValues({columnNames, request, selectedRows});
}
}
/**
* Similar to getSelectedData, but will only check data available on the client.
* It will not attempt to fetch required data. This is good for client table.
* @param tbl_id
* @param columnNames
*/
export function getSelectedDataSync(tbl_id, columnNames=[]) {
const {tableModel, tableMeta, selectInfo} = local.getTblInfoById(tbl_id);
const selectedRows = [...SelectInfo.newInstance(selectInfo).getSelected()]; // get selected row idx as an array
if (columnNames.length === 0) {
columnNames = local.getColumns(tableModel).map( (c) => c.name); // return all columns
}
const meta = cloneDeep(tableMeta);
const columns = columnNames.map((cname) => local.getColumn(tableModel, cname))
.filter((c) => c)
.map((c) => cloneDeep(c));
const data = selectedRows.sort()
.map( (rIdx) => columns.map((c) => local.getCellValue(tableModel, rIdx, c.name)));
return {tableMeta: meta, totalRows: data.length, tableData: {columns, data}};
}
/**
* return true if the given table is fully loaded.
* @param {TableModel} tableModel
* @returns {boolean}
* @memberof ffirefly.util.table
* @func isTableLoaded
*/
export function isTableLoaded(tableModel={}) {
const {isFetching} = tableModel;
const status = get(tableModel, 'tableMeta.Loading-Status');
return !isFetching && status === 'COMPLETED';
}
/**
* This function merges the source object into the target object
* by traversing and comparing every like path. If a value was
* merged at any data node in the data graph, the node and all of its
* parent nodes will be shallow cloned and returned. Otherwise, the target's value
* will be returned.
* @param {Object} target
* @param {Object} source
* @returns {Object}
* @public
* @memberof firefly.util.table
* @func smartMerge
*/
export function smartMerge(target, source) {
if (!target) return source;
if (isPlainObject(source) && isPlainObject(target)) {
const objChanges = {};
Object.keys(source).forEach((k) => {
const nval = smartMerge(target[k], source[k]);
if (nval !== target[k]) {
objChanges[k] = nval;
}
});
return (isEmpty(objChanges)) ? target : Object.assign({}, target, objChanges);
} else if (isArray(source) && isArray(target)){
const aryChanges = [];
source.forEach((v, idx) => {
const nval = smartMerge(target[idx], source[idx]);
if (nval !== target[idx]) {
aryChanges[idx] = nval;
}
});
if (isEmpty(aryChanges)) return target;
else {
const nAry = target.slice();
aryChanges.forEach((v, idx) => nAry[idx] = v);
return nAry;
}
} else {
return (target === source) ? target : source;
}
}
/**
* sort table data in-place.
* @param {TableData} tableData
* @param {TableColumn[]} columns
* @param {string} sortInfoStr
* @returns {TableData}
* @public
* @memberof firefly.util.table
* @func sortTableData
*/
export function sortTableData(tableData, columns, sortInfoStr) {
const sortInfoCls = SortInfo.parse(sortInfoStr);
const colName = get(sortInfoCls, 'sortColumns.0');
const dir = get(sortInfoCls, 'direction', UNSORTED);
if (dir === UNSORTED || get(tableData, 'length', 0) === 0) return tableData;
const multiplier = dir === SORT_ASC ? 1 : -1;
const colIdx = columns.findIndex( (col) => {return col.name === colName;} );
const col = columns[colIdx];
if (!col) return tableData;
var comparator;
if (!col.type || ['char', 'c'].includes(col.type) ) {
comparator = (r1, r2) => {
let [s1, s2] = [r1[colIdx], r2[colIdx]];
s1 = s1 === '' ? '\u0002' : s1 === null ? '\u0001' : isUndefined(s1) ? '\u0000' : s1;
s2 = s2 === '' ? '\u0002' : s2 === null ? '\u0001' : isUndefined(s2) ? '\u0000' : s2;
return multiplier * (s1 > s2 ? 1 : -1);
};
} else {
comparator = (r1, r2) => {
let [v1, v2] = [r1[colIdx], r2[colIdx]];
v1 = v1 === null ? -Number.MAX_VALUE : isUndefined(v1) ? Number.NEGATIVE_INFINITY : Number(v1);
v2 = v2 === null ? -Number.MAX_VALUE : isUndefined(v2) ? Number.NEGATIVE_INFINITY : Number(v2);
return multiplier * (Number(v1) - Number(v2));
};
}
tableData.sort(comparator);
return tableData;
}
/**
* filter the given table. This function update the table data in-place.
* @param {TableModel} table
* @param {string} filterInfoStr filters are separated by comma(',').
* @memberof firefly.util.table
* @func filterTable
*/
export function filterTable(table, filterInfoStr) {
if (filterInfoStr) {
const comparators = filterInfoStr.split(';').map((s) => s.trim()).map((s) => FilterInfo.createComparator(s, table));
table.tableData.data = table.tableData.data.filter((row, idx) => {
return comparators.reduce( (rval, match) => rval && match(row, idx), true);
} );
table.totalRows = table.tableData.data.length;
}
return table.tableData;
}
export function cloneClientTable (tableModel) {
const nTable = cloneDeep(tableModel);
nTable.origTableModel = tableModel;
nTable.isFetching = false;
set(nTable, 'tableMeta.Loading-Status', 'COMPLETED');
if (nTable.tableData) {
const {data=[], columns=[]} = nTable.tableData;
// add ROW_IDX to working table for tracking original row index
columns.push({name: 'ROW_IDX', type: 'int', visibility: 'hidden'});
data.forEach((r, idx) => r.push(String(idx)));
}
return nTable;
}
export function processRequest(tableModel, tableRequest, hlRowIdx) {
const {filters, sortInfo, inclCols} = tableRequest;
const {origTableModel=tableModel} = tableModel;
let {startIdx=0, pageSize} = tableRequest;
const nTable = cloneClientTable(origTableModel);
let {data, columns} = nTable.tableData;
nTable.request = tableRequest;
pageSize = pageSize || data.length || MAX_ROW;
if (filters) {
filterTable(nTable, filters);
}
data = nTable.tableData.data;
if (sortInfo) {
data = sortTableData(data, columns, sortInfo);
}
if (inclCols) {
const colAry = inclCols.split(',').map((s) => s.trim());
columns = columns.filters( (c) => colAry.includes(c));
const inclIdices = columns.map( (c) => origTableModel.tableData.indexOf(c));
data = data.map( (r) => r.filters( (c, idx) => inclIdices.includes(idx)));
}
const prevHlRowIdx = get(tableRequest, ['META_OPTIONS', MetaConst.HIGHLIGHTED_ROW_BY_ROWIDX], 0);
if (hlRowIdx) {
const currentPage = Math.floor(hlRowIdx / pageSize) + 1;
startIdx = (currentPage-1) * pageSize;
nTable.highlightedRow = hlRowIdx;
} else if (prevHlRowIdx) {
// preserve previous highlightedRow if possible.
let relRowIdx = data.findIndex( (r, rIdx) => getCellValue(nTable, rIdx, 'ROW_IDX') === prevHlRowIdx );
relRowIdx = relRowIdx < 0 ? 0 : relRowIdx;
const currentPage = Math.floor(relRowIdx / pageSize) + 1;
startIdx = (currentPage-1) * pageSize;
nTable.highlightedRow = relRowIdx;
} else {
nTable.highlightedRow = startIdx;
}
data = data.slice(startIdx, startIdx + pageSize);
nTable.tableData.data = data;
nTable.tableData.columns = columns;
// set selections from the original table
const idxCol = getColumnIdx(nTable, 'ROW_IDX');
if (idxCol >= 0) {
const nTableSelectInfoCls = SelectInfo.newInstance({rowCount: data.length});
const origSelectInfoCls = SelectInfo.newInstance(get(origTableModel, 'selectInfo'));
data.forEach((row, i) => {
const origIdx = parseInt(row[idxCol]);
// set selection
nTableSelectInfoCls.setRowSelect(i, origSelectInfoCls.isSelected(origIdx));
});
nTable.selectInfo = nTableSelectInfoCls.data;
}
return nTable;
}
/**
* collects all available table information given the tbl_id
* @param {string} tbl_id
* @param {number} aPageSize use this pageSize instead of the one in the request.
* @returns {{tableModel, tbl_id, title, totalRows, request, startIdx, endIdx, hlRowIdx, currentPage, pageSize, totalPages, highlightedRow, selectInfo, error, tableMeta, bgStatus}}
* @public
* @memberof firefly.util.table
* @func getTblInfoById
*/
export function getTblInfoById(tbl_id, aPageSize) {
const tableModel = getTblById(tbl_id);
return getTblInfo(tableModel, aPageSize);
}
/**
* collects all available table information given the tableModel.
* @param {TableModel} tableModel
* @param {number} aPageSize use this pageSize instead of the one in the request.
* @returns {{tableModel, tbl_id, title, totalRows, request, startIdx, endIdx, hlRowIdx, currentPage, pageSize, totalPages, highlightedRow, selectInfo, error, tableMeta, backgroundable}}
* @public
* @memberof firefly.util.table
* @func getTblInfo
*/
export function getTblInfo(tableModel, aPageSize) {
if (!tableModel) return {};
var {tbl_id, request, highlightedRow=0, totalRows=0, tableMeta={}, selectInfo, error} = tableModel;
const title = tableMeta.title || get(request, 'META_INFO.title');
const pageSize = aPageSize || get(request, 'pageSize', MAX_ROW); // there should be a pageSize.. default to 1 in case of error. pageSize cannot be 0 because it'll overflow.
if (highlightedRow < 0 ) {
highlightedRow = 0;
} else if (highlightedRow >= totalRows-1) {
highlightedRow = totalRows-1;
}
const currentPage = highlightedRow >= 0 ? Math.floor(highlightedRow / pageSize)+1 : 1;
const hlRowIdx = highlightedRow >= 0 ? highlightedRow % pageSize : 0;
const startIdx = (currentPage-1) * pageSize;
const endIdx = Math.min(startIdx+pageSize, totalRows) || get(tableModel,'tableData.data.length', startIdx) ;
const totalPages = Math.ceil((totalRows || 0)/pageSize);
const backgroundable = get(tableModel, 'request.META_INFO.backgroundable', false);
return { tableModel, tbl_id, title, totalRows, request, startIdx, endIdx, hlRowIdx, currentPage, pageSize,totalPages, highlightedRow, selectInfo, error, tableMeta, backgroundable};
}
/**
* Return the row data as an object keyed by the column name
* @param {TableModel} tableModel
* @param {Number} [rowIdx] = the index of the row to return, default to highlighted row
* @return {Object<String,String>} the values of the row keyed by the column name
* @public
* @memberof firefly.util.table
* @func getTblRowAsObj
*/
export function getTblRowAsObj(tableModel, rowIdx= undefined) {
if (!tableModel) return {};
const {highlightedRow, tableData} = tableModel;
if (!tableData) return {};
const {data, columns}= tableData;
if (isUndefined(rowIdx)) rowIdx= highlightedRow;
if (rowIdx<0 || rowIdx>= get(tableData, 'data.length',0)) return {};
const row= data[rowIdx];
if (!row) return {};
return row.reduce( (obj,v, idx) => {
obj[columns[idx].name]= v;
return obj;
}, {});
}
/**
* returns the url to download a snapshot of the current table data.
* @param {string} tbl_ui_id UI id of the table
* @param {object} params supplement parameter setting such as the information for workspace
* @returns {string}
* @public
* @memberof firefly.util.table
* @func getTableSourceUrl
*/
export function getTableSourceUrl(tbl_ui_id, params) {
const {columns, request} = getTableUiById(tbl_ui_id) || {};
return makeTableSourceUrl(columns, request, params);
}
/**
* Async version of getTableSourceUrl. If the given tbl_ui_id is backed by a local TableModel,
* then we need to push/upload the content of the server before it can be referenced via url.
* @param {string} tbl_ui_id UI id of the table
* @param {object} params supplement parameter setting such as the information for workspace
* @returns {Promise.<string, Error>}
*/
export function getAsyncTableSourceUrl(tbl_ui_id, params) {
const {tbl_id, columns} = getTableUiById(tbl_ui_id) || {};
const ipacTable = tableToIpac(getTblById(tbl_id));
const blob = new Blob([ipacTable]);
//const file = new File([new Blob([ipacTable])], filename);
return doUpload(blob).then( ({status, cacheKey}) => {
const request = makeFileRequest('save as text', cacheKey, null, {pageSize: MAX_ROW});
return makeTableSourceUrl(columns, request, params);
});
}
function makeTableSourceUrl(columns, request, otherParams) {
const tableRequest = Object.assign(cloneDeep(request), {startIdx: 0,pageSize : MAX_ROW});
const visiCols = columns.filter( (col) => get(col, 'visibility', 'show') === 'show')
.map( (col) => col.name);
if (visiCols.length !== columns.length) {
tableRequest['inclCols'] = visiCols.map( (c) => c.includes('"') ? c : '"' + c + '"').join(); // add quotes to cname unless it's already quoted.
}
Reflect.deleteProperty(tableRequest, 'tbl_id');
const {wsCmd, file_name} = otherParams || {};
const params = omitBy({
[ServerParams.COMMAND]: (!wsCmd ? ServerParams.TABLE_SAVE : ServerParams.WS_PUT_TABLE_FILE),
[ServerParams.REQUEST]: JSON.stringify(tableRequest),
file_name: (!wsCmd) && (file_name || get(tableRequest, 'META_INFO.title'))
}, isNil);
if (otherParams) {
Object.assign(params, omitBy(otherParams, isNil));
}
return wsCmd ? params : encodeServerUrl(DEF_BASE_URL, params);
}
export function setHlRowByRowIdx(nreq, tableModel) {
const hlRowIdx = getCellValue(tableModel, tableModel.highlightedRow, 'ROW_IDX');
if (hlRowIdx) {
set(nreq, ['META_OPTIONS', MetaConst.HIGHLIGHTED_ROW_BY_ROWIDX], hlRowIdx);
}
}
/**
* convert this table into IPAC format
* @param tableModel
* @returns {string}
*/
export function tableToIpac(tableModel) {
const {tableData, tableMeta} = tableModel;
const {columns, data} = tableData || {};
const colWidths = calcColumnWidths(columns, data);
const meta = Object.entries(tableMeta).map(([k,v]) => `\\${k} = ${v}`)
.concat(columns.filter( (c) => c.visibility === 'hidden').map( (c) => `\\col.${c.name}.Visibility = ${c.visibility}`))
.concat(columns.filter( (c) => c.filterable).map( (c) => `\\col.${c.name}.Filterable = ${c.filterable}`))
.concat(columns.filter( (c) => c.sortable).map( (c) => `\\col.${c.name}.Sortable = ${c.sortable}`))
.concat(columns.filter( (c) => c.label).map( (c) => `\\col.${c.name}.Label = ${c.label}`))
.concat(columns.filter( (c) => c.desc).map( (c) => `\\col.${c.name}.ShortDescription = ${c.desc}`))
.join('\n');
const head = [
columns.map((c, idx) => padEnd(c.name, colWidths[idx])),
columns.map((c, idx) => padEnd(c.type, colWidths[idx])),
columns.find((c) => c.units) && columns.map((c, idx) => padEnd(c.units, colWidths[idx])),
columns.find((c) => c.nullString) && columns.map((c, idx) => padEnd(c.nullString, colWidths[idx]))
]
.filter( (ary) => ary)
.map( (ary) => '|' + ary.join('|') + '|')
.join('\n');
const dataStr = data.map( (row) => ' ' + row.map( (c, idx) => padEnd(c, colWidths[idx])).join(' ') + ' ' )
.join('\n');
return [meta, '\\', head, dataStr].join('\n');
}
export function tableTextView(columns, dataAry, showUnits=false, tableMeta) {
const colWidths = calcColumnWidths(columns, dataAry);
const meta = tableMeta && Object.entries(tableMeta).map(([k,v]) => `\\${k} = ${v}`).join('\n');
const head = [
columns.map((c, idx) => get(c,'visibility', 'show') === 'show' && padEnd(c.label || c.name, colWidths[idx])),
columns.map((c, idx) => get(c,'visibility', 'show') === 'show' && padEnd(c.type, colWidths[idx])),
showUnits && columns.map((c, idx) => get(c,'visibility', 'show') === 'show' && padEnd(c.units, colWidths[idx]))
]
.filter( (ary) => ary)
.map( (ary) => '|' + ary.filter( (v) => v ).join('|') + '|')
.join('\n');
const dataStr = dataAry.map((row) => ' ' +
row.map((c, idx) => get(columns, `${idx}.visibility`, 'show') === 'show' && padEnd(c, colWidths[idx]))
.filter((v) => v)
.join(' ')
+ ' ')
.join('\n');
return [meta, head, dataStr].join('\n');
}
/**
* returns a details view of the highlightedRow in a form of a tableModel
* with columns Name, Value, Type, Units, and Description
* @param {string} tbl_id tbl_id of the table
* @param {number} highlightedRow the row to generate the details for
* @param {string} details_tbl_id tbl_id of the details table. defaults to tbl_id + '_details'
*/
export function tableDetailsView(tbl_id, highlightedRow, details_tbl_id) {
const tableModel = getTblById(tbl_id);
const dataCols = getColumns(tableModel);
const {totalRows} = tableModel || {};
const nTblId = details_tbl_id || tbl_id + '_details';
if (totalRows <= 0 || highlightedRow < 0 || highlightedRow > tableModel.totalRows) {
return {tbl_id: nTblId, error: 'No Data Found'};
}
const columns = [
{name: 'Name', type: 'char', desc: 'Column name'},
{name: 'Value', type: 'char'},
{name: 'Type', type: 'char'},
{name: 'Units', type: 'char'},
{name: 'Description', type: 'char'}
];
const data = dataCols.map((c) => {
const name = c.label || c.name;
const value = getCellValue(tableModel, highlightedRow, c.name) || '';
const type = c.type || '';
const units = c.units || '';
const desc = c.desc || '';
return [name, value, type, units, desc];
});
const prevDetails = getTblById(nTblId) || {};
const {request={}} = prevDetails;
let nTable = {
tbl_id: nTblId,
request,
title: 'Additional Information',
tableData: {columns, data},
totalRows: data.length,
highlightedRow: prevDetails.highlightedRow
};
if (request.sortInfo || request.filters) {
setHlRowByRowIdx(request, prevDetails);
nTable = processRequest(nTable, request);
}
return nTable;
}
/**
* returns an object map of the column name and its width.
* The width is the number of characters needed to display
* the header and the data in a table given columns and dataAry.
* @param {TableColumn[]} columns array of column object
* @param {TableData} dataAry array of array.
* @returns {number[]} an array of widths corresponding to the given columns array.
* @memberof firefly.util.table
* @func calcColumnWidths
*/
export function calcColumnWidths(columns, dataAry) {
return columns.map( (cv, idx) => {
let width = cv.prefWidth || cv.width;
if (width) {
return width;
}
const cname = cv.label || cv.name;
width = Math.max(cname.length, get(cv, 'units.length', 0), get(cv, 'type.length', 0));
width = dataAry.reduce( (maxWidth, row) => {
return Math.max(maxWidth, get(row, [idx, 'length'], 0));
}, width); // max width of data
return width;
});
}
/**
* There are some inconsistencies in how a request is created.
* This fixes any of the inconsistencies it finds.
* @param request
*/
export function fixRequest(request) {
// ensure tbl_id exists and it's set correctly.
const tbl_id = request.tbl_id || get(request, 'META_INFO.tbl_id', uniqueTblId());
request.tbl_id = tbl_id;
set(request, 'META_INFO.tbl_id', tbl_id);
}
/**
* create a unique table id (tbl_id)
* @returns {string}
* @public
* @memberof firefly.util.table
* @func uniqueTblId
*/
export function uniqueTblId() {
const uid = getWsConnId();
const id = uniqueId(`tbl_id-c${uid}-`);
if (getTblById(id)) {
return uniqueTblId();
} else {
return id;
}
}
/**
* create a unique table UI id (tbl_ui_id)
* @returns {string}
* @public
* @memberof firefly.util.table
* @func uniqueTblUiId
*/
export function uniqueTblUiId() {
const uid = getWsConnId();
const id = uniqueId(`tbl_ui_id-c${uid}-`);
if (getTableUiById(id)) {
return uniqueTblUiId();
} else {
return id;
}
}
/**
* This function provides a patch until we can reliably determine that the ra/dec columns use radians or degrees.
* @param tableOrMeta the table object or the tableMeta object
* @memberof firefly.util.table
* @func isTableUsingRadians
*
*/
export function isTableUsingRadians(tableOrMeta) {
if (!tableOrMeta) return false;
const tableMeta= tableOrMeta.tableMeta || tableOrMeta;
return has(tableMeta, 'HIERARCH.AFW_TABLE_VERSION');
}
export function createErrorTbl(tbl_id, error) {
return set({tbl_id, error: error||'something went wrong'}, 'tableMeta.Loading-Status', 'COMPLETED');
}
/**
* this function invoke the given callback when changes are made to the given tbl_id
* @param {string} tbl_id table id to watch
* @param {Object} actions an array of table actions to watch
* @param {function} callback callback to execute when table is loaded.
* @param {string} [watcherId] action watcher id to be used
* @return {function} returns a function used to cancel
*/
export function watchTableChanges(tbl_id, actions, callback, watcherId) {
const accept = (a) => tbl_id === (get(a, 'payload.tbl_id') || get(a, 'payload.request.tbl_id'));
return monitorChanges(actions, accept, callback, watcherId);
}
/**
* this function invoke the given callback when the given actions occur.
* @param {Object} actions an array of actions to watch
* @param {function} accept a function used to filter incoming actions. if not given, it will accept all.
* @param {function} callback callback to execute when action occurs.
* @param {string} [watcherId] action watcher id to be used
* @return {function} returns a function used to cancel
*/
export function monitorChanges(actions, accept, callback, watcherId) {
if (!Array.isArray(actions) || actions.length === 0 || !callback) return;
const id = watcherId || uniqueID();
const mCallback = (action) => {
if (accept(action)) {
callback(action);
}
};
dispatchAddActionWatcher({id, actions, callback:mCallback});
return () => dispatchCancelActionWatcher(id);
}
/**
* @summary returns the non-hidden columns of the given table. If type is given, it
* will only return columns that match type.
* @param {TableModel} tableModel
* @param {COL_TYPE} type one of predefined COL_TYPE. defaults to 'ALL'.
* @returns {Array<TableColumn>}
* @public
* @memberof firefly.util.table
* @func getColumns
*/
export function getColumns(tableModel, type=COL_TYPE.ALL) {
return getColsByType(getAllColumns(tableModel), type);
}
/**
* @summary returns all columns of the given table including hidden ones.
* @param {TableModel} tableModel
* @returns {Array<TableColumn>}
* @public
* @memberof firefly.util.table
* @func getAllColumns
*/
export function getAllColumns(tableModel) {
return get(tableModel, 'tableData.columns', []);
}
/**
* @summary returns only the non-hidden columns matching the given type.
* @param {Array<TableColumn>} tblColumns
* @param {COL_TYPE} type one of predefined COL_TYPE. defaults to 'ALL'.
* @returns {Array<TableColumn>}
*/
export function getColsByType(tblColumns=[], type=COL_TYPE.ALL) {
const matcher = type === COL_TYPE.TEXT ? isTextType : isNumericType;
return tblColumns.filter((col) => get(col, 'visibility') !== 'hidden'
&& (type === COL_TYPE.ALL || matcher(col)));
}
export function getColByUtype(tableModel, utype) {
return getColumns(tableModel).filter( (col) => col.utype === utype);
}
export function getColByUCD(tableModel, ucd) {
return getColumns(tableModel).filter( (col) => col.UCD === ucd);
}
/**
* returns a 2-part array, [release date column, datarights column] if this table
* contains proprietary data
* @param tableModel
* @returns {string[]}
*/
export function getProprietaryInfo(tableModel) {
const rcname = get(tableModel, ['tableMeta', MetaConst.RELEASE_DATE_COL]);
const dcname = get(tableModel, ['tableMeta', MetaConst.DATARIGHTS_COL]);
return rcname || dcname ? [rcname, dcname] : [];
}
export function hasRowAccess(tableModel, rowIdx) {
const [rcname, dcname] = getProprietaryInfo(tableModel);
if (!rcname && !dcname) return true; // no proprietary info
if (dcname) {
const rights = getCellValue(tableModel, rowIdx, dcname) || '';
if (['public', 'secure', '1', 'true', 't'].includes(rights.trim().toLocaleLowerCase()) ) {
return true;
}
}
if (rcname) {
const rdate = getCellValue(tableModel, rowIdx, rcname) || '';
if ( rdate && new Date() > new Date(rdate)) {
return true;
}
}
return false;
}
export function isNumericType(col={}) {
return num_types.includes(col.type);
}
export function isTextType(col={}) {
return char_types.includes(col.type);
}
/**
* @param {string} tbl_id
* @returns {string} returns a key used by table to store backgrounding information. This is used by BgMaskPanel.
*/
export function makeBgKey(tbl_id) {
return `tables:${tbl_id}`;
}
/**
* If input is '"x"', outputs 'x', but if input is '"x"+"y"' or 'log("x")' output is the same as input
* @param s
* @returns {*}
*/
export function stripColumnNameQuotes(s) {
const newS = s.replace(/^"(.+)"$/, '$1');
return newS.includes('"') ? s : newS;
}
export function tblDropDownId(tbl_id) {
return `table_dropDown-${tbl_id}`;
}
/*-------------------------------------private------------------------------------------------*/
/**
* Action watcher callback for table update, which is invoked when
* the table given by tbl_id is fully loaded.
* @callback actionWatcherCallback
* @param action action that triggered this watcher
* @param cancelSelf function to cancel this watcher
* @param params parameters object
* @param {string} params.tbl_id table id to watch
* @param {function} params.resolve callback to execute when table is loaded.
*/
function doOnTblLoaded(action, cancelSelf, {tbl_id, resolve}) {
if (!resolve) cancelSelf();
if (tbl_id === get(action, 'payload.tbl_id')) {
const tableModel = getTblById(tbl_id);
if (get(tableModel, 'error')) {
// there was an error loading this table.
resolve(createErrorTbl(tbl_id, tableModel.error));
cancelSelf();
} else if (isTableLoaded(tableModel) && get(tableModel, 'tableData.columns.length')) {
resolve(getTblInfoById(tbl_id));
cancelSelf();
}
}
}