Skip to content

Commit c73e0ce

Browse files
authored
Merge pull request #109 from Caltech-IPAC/DM-5387_filter_editor
DM-5387: Filter Editor
2 parents 87cec7e + f367744 commit c73e0ce

16 files changed

+509
-209
lines changed

src/firefly/config/web.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</filter>
2525
<filter-mapping>
2626
<filter-name>NoCacheFilter</filter-name>
27-
<url-pattern>/firefly.nocache.js</url-pattern>
27+
<url-pattern>*.nocache.js</url-pattern>
2828
<url-pattern>/firefly_loader.js</url-pattern>
2929
</filter-mapping>
3030

src/firefly/js/core/FireflyViewer.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class FireflyViewer extends Component {
8282

8383
render() {
8484
var {isReady, menu={}, appTitle, appIcon, altAppIcon, dropDown,
85-
searchPanels, views, footer, style} = this.state;
85+
searchPanels, views, footer, style, showViewsSwitch} = this.state;
8686
const {visible, view} = dropDown || {};
8787
const searches = getDropDownNames();
8888

@@ -101,7 +101,7 @@ export class FireflyViewer extends Component {
101101
{...{searches, searchPanels} } />
102102
</header>
103103
<main>
104-
<DynamicResults views={views}/>
104+
<DynamicResults {...{views, showViewsSwitch}}/>
105105
</main>
106106
</div>
107107
);
@@ -123,7 +123,8 @@ FireflyViewer.propTypes = {
123123
footer: PropTypes.element,
124124
searchPanels: PropTypes.arrayOf(PropTypes.element),
125125
views: PropTypes.string, // combination of LO_VIEW separated by ' | '. ie. 'images | tables'.
126-
style: PropTypes.object
126+
style: PropTypes.object,
127+
showViewsSwitch: PropTypes.bool
127128
};
128129

129130
FireflyViewer.defaultProps = {
@@ -159,14 +160,18 @@ function BannerSection(props) {
159160

160161

161162
function DynamicResults(props) {
162-
var {views} = props;
163+
var {views, showViewsSwitch} = props;
163164
if (LO_VIEW.get(views)) {
164-
return <TriViewPanel/>;
165+
return <TriViewPanel {...{showViewsSwitch}}/>;
165166
}
166167
}
167168
DynamicResults.propTypes = {
168169
views: PropTypes.oneOfType([
169-
PropTypes.string,
170-
PropTypes.object])
170+
PropTypes.string,
171+
PropTypes.object]),
172+
showViewsSwitch: PropTypes.bool
173+
};
174+
DynamicResults.defaultProps = {
175+
showViewsSwitch: true
171176
};
172177

src/firefly/js/tables/FilterInfo.js

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
/*
77
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
88
*/
9-
const op_regex = new RegExp('(<|>|>=|<=|=|!=|like|in)');
9+
const op_regex = new RegExp('(!=|>=|<=|<|>|=|like|in)', 'i');
1010
const cond_regex = new RegExp('^' + op_regex.source + '\\s+(.+)', 'i');
1111
const filter_regex = new RegExp('(\\S+)\\s+' + op_regex.source + '\\s+(.+)', 'i');
1212

13-
export const FILTER_TTIPS = 'Valid values are one of (=, >, <, !=, >=, <=, LIKE) followed by a value separated by a space. \n' +
14-
`Or 'IN', followed by a list of values separated by commas. \n` +
15-
'Examples: > 12345; != 3000; IN a,b,c,d';
13+
export const FILTER_CONDITION_TTIPS =
14+
`Valid values are one of (=, >, <, !=, >=, <=, LIKE) followed by a value separated by a space.
15+
Or 'IN', followed by a list of values separated by commas.
16+
Examples: > 12345; != 3000; IN a,b,c,d`;
17+
18+
export const FILTER_TTIPS =
19+
`Filters are "column_name operator condition" separated by commas.
20+
${FILTER_CONDITION_TTIPS}`;
21+
1622

1723
/**
1824
* convenience class to handle the table's filter information.
@@ -68,21 +74,81 @@ export class FilterInfo {
6874
}
6975
}
7076

77+
/**
78+
* given a list of filters separated by semicolon,
79+
* transform them into valid filters if they are not already so.
80+
* @param filterInfo
81+
* @returns {string}
82+
*/
83+
static autoCorrectFilter(filterInfo) {
84+
if (filterInfo) {
85+
const filters = filterInfo.split(';').map( (v) => {
86+
const parts = v.split(op_regex).map( (s) => s.trim());
87+
if (parts.length != 3) return v;
88+
return `${parts[0]} ${FilterInfo.autoCorrect(parts[1]+parts[2])}`;
89+
});
90+
return filters.join(';');
91+
} else {
92+
return filterInfo;
93+
}
94+
}
95+
7196
/**
7297
* validate the conditions
7398
* @param conditions
7499
* @returns {boolean}
75100
*/
76-
static isValid(conditions) {
101+
static isConditionValid(conditions) {
77102
return !conditions || conditions.split(';').reduce( (rval, v) => {
78103
return rval && cond_regex.test(v.trim());
79104
}, true);
80105
}
81106

82-
static validator(conditions) {
107+
/**
108+
* validator for column's filter. it validates only the condition portion of the filter.
109+
* @param conditions
110+
* @returns {{valid: boolean, value: (string|*), message: string}}
111+
*/
112+
static conditionValidator(conditions) {
83113
conditions = FilterInfo.autoCorrect(conditions);
84-
const valid = FilterInfo.isValid(conditions);
85-
return {valid, value: conditions, message: FILTER_TTIPS};
114+
const valid = FilterInfo.isConditionValid(conditions);
115+
return {valid, value: conditions, message: FILTER_CONDITION_TTIPS};
116+
}
117+
118+
/**
119+
* validate the filterInfo string
120+
* @param conditions
121+
* @param columns array of column definitions
122+
* @returns {[boolean, string]} isValid plus an error message if isValid is false.
123+
*/
124+
static isValid(filterInfo, columns = []) {
125+
const rval = [true, ''];
126+
const allowCols = columns.concat({name:'ROWID'});
127+
if (filterInfo && filterInfo.trim().length > 0) {
128+
return filterInfo.split(';').reduce( ([isValid, msg], v) => {
129+
const parts = v.split(op_regex).map( (s) => s.trim());
130+
if (parts.length !== 3) {
131+
msg += `\n"${v}" is not a valid filter.`;
132+
} else if (!allowCols.some( (col) => col.name === parts[0])) {
133+
msg +=`\n"${v}" column not found.\n`;
134+
}
135+
return [!msg, msg];
136+
}, rval);
137+
} else {
138+
return rval;
139+
}
140+
}
141+
142+
/**
143+
* validator for free-form filters field
144+
* @param filterInfo string serialized filter list
145+
* @param columns array of column definitions
146+
* @returns {{valid: boolean, value: (string|*), message: string}}
147+
*/
148+
static validator(columns, filterInfo) {
149+
filterInfo = FilterInfo.autoCorrectFilter(filterInfo);
150+
const [valid, message] = FilterInfo.isValid(filterInfo, columns);
151+
return {valid, value: filterInfo, message};
86152
}
87153

88154
serialize() {

src/firefly/js/tables/TableConnector.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,14 @@ export class TableConnector {
107107
TblCntlr.dispatchTableSelect(this.tbl_id, selectInfoCls.data);
108108
}
109109

110-
onOptionUpdate({pageSize, columns, showUnits, showFilters, colSortDir}) {
110+
onOptionUpdate({pageSize, columns, showUnits, showFilters, sortInfo, filterInfo}) {
111111
if (pageSize) {
112112
this.onPageSizeChange(pageSize);
113113
}
114-
const changes = omitBy({columns, showUnits, showFilters, colSortDir}, isUndefined);
114+
if (!isUndefined(filterInfo)) {
115+
this.onFilter(filterInfo);
116+
}
117+
const changes = omitBy({columns, showUnits, showFilters, optSortInfo:sortInfo}, isUndefined);
115118
if (!isEmpty(changes)) {
116119
changes.tbl_ui_id = this.tbl_ui_id;
117120
TblCntlr.dispatchTableUiUpdate(changes);

src/firefly/js/tables/TableUtil.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -362,32 +362,45 @@ export function smartMerge(target, source) {
362362
* @param sortInfoStr
363363
*/
364364
export function sortTable(origTableModel, sortInfoStr) {
365+
var tableModel = cloneDeep(origTableModel);
366+
set(tableModel, 'request.sortInfo', sortInfoStr);
367+
const {data, columns} = tableModel.tableData;
368+
sortTableData(data, columns, sortInfoStr);
369+
return tableModel;
370+
}
371+
372+
/**
373+
* sort table data in place.
374+
* @param tableData
375+
* @param columns
376+
* @param sortInfoStr
377+
* @returns {*}
378+
*/
379+
export function sortTableData(tableData, columns, sortInfoStr) {
365380
const sortInfoCls = SortInfo.parse(sortInfoStr);
366381
const colName = get(sortInfoCls, 'sortColumns.0');
367382
const dir = get(sortInfoCls, 'direction', UNSORTED);
368-
if (dir === UNSORTED || get(origTableModel, 'tableData.data.length', 0) === 0) return origTableModel;
383+
if (dir === UNSORTED || get(tableData, 'length', 0) === 0) return tableData;
369384

370385
const multiplier = dir === SORT_ASC ? 1 : -1;
371-
const colIdx = getColumnIdx(origTableModel, colName);
372-
const col = get(origTableModel, ['tableData','columns', colIdx]);
373-
374-
var tableModel = cloneDeep(origTableModel);
375-
set(tableModel, 'request.sortInfo', sortInfoStr);
386+
const colIdx = columns.findIndex( (col) => {return col.name === colName;} );
387+
const col = columns[colIdx];
388+
if (!col) return tableData;
376389

377390
var comparator;
378391
if (!col.type || ['char', 'c'].includes(col.type) ) {
379392
comparator = (r1, r2) => {
380-
const [s1, s2] = [r1[colIdx], r2[colIdx]];
381-
return multiplier * (s1 > s2 ? 1 : -1);
382-
};
393+
const [s1, s2] = [r1[colIdx], r2[colIdx]];
394+
return multiplier * (s1 > s2 ? 1 : -1);
395+
};
383396
} else {
384397
comparator = (r1, r2) => {
385-
const [v1, v2] = [r1[colIdx], r2[colIdx]];
386-
return multiplier * (Number(v1) - Number(v2));
387-
};
398+
const [v1, v2] = [r1[colIdx], r2[colIdx]];
399+
return multiplier * (Number(v1) - Number(v2));
400+
};
388401
}
389-
tableModel.tableData.data.sort(comparator);
390-
return tableModel;
402+
tableData.sort(comparator);
403+
return tableData;
391404
}
392405

393406
export function getTblInfoById(tbl_id, aPageSize) {
@@ -443,7 +456,24 @@ export function getTableSourceUrl(columns, request, filename) {
443456
return encodeServerUrl(SAVE_TABLE_URL, {file_name, Request: request});
444457
}
445458

446-
459+
/**
460+
* returns a map of cname -> width. The width is the number of characters needed to display
461+
* the header and the data as a table given columns and dataAry.
462+
* @param columns array of column object
463+
* @param dataAry array of array.
464+
* @returns {Object} a map of cname -> width
465+
*/
466+
export function calcColumnWidths(columns, dataAry) {
467+
return columns.reduce( (pv, cv, idx) => {
468+
const cname = cv.name;
469+
var width = Math.max(cname.length, get(cv, 'units.length', 0));
470+
width = dataAry.reduce( (maxWidth, row) => {
471+
return Math.max(maxWidth, get(row, [idx, 'length'], 0));
472+
}, width); // max width of data
473+
pv[cname] = width;
474+
return pv;
475+
}, {ROWID: 8});
476+
}
447477

448478
export function uniqueTblId() {
449479
const id = uniqueId('tbl_id-');

src/firefly/js/tables/ui/BasicTableView.jsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import React, {PropTypes} from 'react';
66
import sCompare from 'react-addons-shallow-compare';
77
import FixedDataTable from 'fixed-data-table';
88
import Resizable from 'react-component-resizable';
9-
import {debounce, defer, get, isEmpty, pick, padEnd} from 'lodash';
9+
import {debounce, defer, get, isEmpty, padEnd} from 'lodash';
1010

11+
import {calcColumnWidths} from '../TableUtil.js';
1112
import {SelectInfo} from '../SelectInfo.js';
1213
import {FilterInfo} from '../FilterInfo.js';
1314
import {SortInfo} from '../SortInfo.js';
@@ -133,7 +134,7 @@ export class BasicTableView extends React.Component {
133134
}
134135

135136
render() {
136-
const {columns, data, hlRowIdx, showUnits, showFilters, filterInfo, renderers,
137+
const {columns, data, hlRowIdx, showUnits, showFilters, filterInfo, renderers, bgColor,
137138
selectable, selectInfoCls, sortInfo, callbacks, textView, rowHeight, showMask} = this.props;
138139
const {widthPx, heightPx, columnWidths} = this.state;
139140
const {onSort, onFilter, onRowSelect, onSelectAll, onFilterSelected} = this;
@@ -143,7 +144,7 @@ export class BasicTableView extends React.Component {
143144
// const filterInfoCls = FilterInfo.parse(filterInfo);
144145
// const sortInfoCls = SortInfo.parse(sortInfo);
145146
//
146-
const makeColumnsProps = {columns, data, selectable, selectInfoCls, renderers,
147+
const makeColumnsProps = {columns, data, selectable, selectInfoCls, renderers, bgColor,
147148
columnWidths, filterInfo, sortInfo, showUnits, showFilters,
148149
onSort, onFilter, onRowSelect, onSelectAll, onFilterSelected};
149150

@@ -188,6 +189,7 @@ BasicTableView.propTypes = {
188189
rowHeight: PropTypes.number,
189190
showMask: PropTypes.bool,
190191
currentPage: PropTypes.number,
192+
bgColor: PropTypes.string,
191193
renderers: PropTypes.objectOf(
192194
PropTypes.shape({
193195
cellRenderer: PropTypes.func,
@@ -240,22 +242,24 @@ function makeColWidth(columns, showUnits) {
240242
}, {});
241243
}
242244

243-
function makeColumns ({columns, columnWidths, data, selectable, showUnits, showFilters, renderers,
245+
function makeColumns ({columns, columnWidths, data, selectable, showUnits, showFilters, renderers, bgColor,
244246
selectInfoCls, filterInfo, sortInfo, onRowSelect, onSelectAll, onSort, onFilter, onFilterSelected}) {
245247
if (!columns) return false;
246248

247249
var colsEl = columns.map((col, idx) => {
248250
if (col.visibility && col.visibility !== 'show') return false;
249251
const HeadRenderer = get(renderers, [col.name, 'headRenderer'], HeaderCell);
250252
const CellRenderer = get(renderers, [col.name, 'cellRenderer'], TextCell);
253+
const fixed = col.fixed || false;
254+
const style = col.fixed && bgColor && {backgroundColor: bgColor};
251255

252256
return (
253257
<Column
254258
key={col.name}
255259
columnKey={col.name}
256260
header={<HeadRenderer {...{col, showUnits, showFilters, filterInfo, sortInfo, onSort, onFilter}} />}
257-
cell={<CellRenderer data={data} col={idx} />}
258-
fixed={false}
261+
cell={<CellRenderer style={style} data={data} colIdx={idx} />}
262+
fixed={fixed}
259263
width={columnWidths[col.name]}
260264
isResizable={true}
261265
allowCellsRecycling={true}
@@ -268,7 +272,7 @@ function makeColumns ({columns, columnWidths, data, selectable, showUnits, showF
268272
key='selectable-checkbox'
269273
columnKey='selectable-checkbox'
270274
header={<SelectableHeader {...{checked, onSelectAll, showUnits, showFilters, onFilterSelected}} />}
271-
cell={<SelectableCell selectInfoCls={selectInfoCls} onRowSelect={onRowSelect} />}
275+
cell={<SelectableCell style={{backgroundColor: bgColor}} selectInfoCls={selectInfoCls} onRowSelect={onRowSelect} />}
272276
fixed={true}
273277
width={25}
274278
allowCellsRecycling={true}
@@ -280,20 +284,25 @@ function makeColumns ({columns, columnWidths, data, selectable, showUnits, showF
280284

281285

282286
function tableToText(columns, dataAry, showUnits=false) {
283-
var textHead = columns.reduce( (pval, cval, idx) => {
284-
return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cval.name, columns[idx].width)}|` : '');
287+
288+
const colWidths = calcColumnWidths(columns, dataAry);
289+
290+
var textHead = columns.reduce( (pval, col, idx) => {
291+
return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.name, colWidths[col.name])}|` : '');
285292
}, '|');
286293

287294
if (showUnits) {
288-
textHead += '\n' + columns.reduce( (pval, cval, idx) => {
289-
return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cval.units || '', columns[idx].width)}|` : '');
295+
textHead += '\n' + columns.reduce( (pval, col, idx) => {
296+
return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.units || '', colWidths[col.name])}|` : '');
290297
}, '|');
291298
}
292299

293300
var textData = dataAry.reduce( (pval, row) => {
294301
return pval +
295302
row.reduce( (pv, cv, idx) => {
296-
return pv + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cv || '', columns[idx].width)} ` : '');
303+
const cname = get(columns, [idx, 'name']);
304+
if (!cname) return pv; // not defined in columns.. can ignore
305+
return pv + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cv || '', colWidths[cname])} ` : '');
297306
}, ' ') + '\n';
298307
}, '');
299308
return textHead + '\n' + textData;

0 commit comments

Comments
 (0)