Skip to content

Commit f0fbb11

Browse files
authored
Misc improvements to JBrowse search (#210)
* Misc improvements to JBrowse search
1 parent a4f485f commit f0fbb11

File tree

7 files changed

+124
-72
lines changed

7 files changed

+124
-72
lines changed

jbrowse/api-src/org/labkey/api/jbrowse/JBrowseFieldDescriptor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ public void setFlex(Integer flex)
169169
_flex = flex;
170170
}
171171

172+
public JBrowseFieldDescriptor inDefaultColumns(boolean isInDefaultColumns)
173+
{
174+
_isInDefaultColumns = isInDefaultColumns;
175+
return this;
176+
}
177+
172178
public JSONObject toJSON() {
173179
JSONObject fieldDescriptorJSON = new JSONObject();
174180
fieldDescriptorJSON.put("name", _fieldName);

jbrowse/src/client/JBrowse/VariantSearch/components/FilterForm.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import React, { useState } from "react";
2-
import { makeStyles } from "@material-ui/core/styles";
3-
import TextField from "@material-ui/core/TextField";
4-
import Button from "@material-ui/core/Button";
5-
import Select from "@material-ui/core/Select";
6-
import MenuItem from "@material-ui/core/MenuItem";
7-
import FormControl from "@material-ui/core/FormControl";
8-
import InputLabel from "@material-ui/core/InputLabel";
9-
import CardActions from "@material-ui/core/CardActions";
10-
import Card from "@material-ui/core/Card";
11-
import CardContent from "@material-ui/core/CardContent";
12-
import { searchStringToInitialFilters } from "../../utils";
1+
import React, { useState } from 'react';
2+
import { makeStyles } from '@material-ui/core/styles';
3+
import TextField from '@material-ui/core/TextField';
4+
import Button from '@material-ui/core/Button';
5+
import Select from '@material-ui/core/Select';
6+
import MenuItem from '@material-ui/core/MenuItem';
7+
import FormControl from '@material-ui/core/FormControl';
8+
import InputLabel from '@material-ui/core/InputLabel';
9+
import CardActions from '@material-ui/core/CardActions';
10+
import Card from '@material-ui/core/Card';
11+
import CardContent from '@material-ui/core/CardContent';
12+
import { FieldModel, searchStringToInitialFilters } from '../../utils';
1313

1414
const useStyles = makeStyles((theme) => ({
1515
formControl: {
@@ -85,9 +85,17 @@ const useStyles = makeStyles((theme) => ({
8585
}
8686
}));
8787

88-
const FilterForm = (props) => {
89-
const { availableOperators, handleQuery, setFilters, handleClose, fieldTypeInfo } = props
88+
export declare type FilterFormProps = {
89+
availableOperators: any,
90+
handleQuery: (filters: string[]) => void,
91+
setFilters: (filters: string[]) => void,
92+
handleClose?: any,
93+
fieldTypeInfo: FieldModel[]
94+
}
9095

96+
const FilterForm = (props: FilterFormProps ) => {
97+
const { availableOperators, handleQuery, setFilters, handleClose, fieldTypeInfo } = props
98+
// TODO: this should use a typed class for Filter. see utils.ts
9199
const [filters, localSetFilters] = useState(searchStringToInitialFilters(availableOperators));
92100
const [highlightedInputs, setHighlightedInputs] = useState<{ [index: number]: { field: boolean, operator: boolean, value: boolean } }>({});
93101

@@ -159,6 +167,8 @@ const handleSubmit = (event) => {
159167
</Button>
160168
</div>
161169

170+
{/* TODO: this should read the FieldModel and interpret allowableValues, perhaps isMultiValued, etc. */}
171+
{/* TODO: consider also using something like FieldModel.supportsFilter */}
162172
<div className={classes.formScroll}>
163173
{filters.map((filter, index) => (
164174
<div key={index} className={`${classes.filterRow}`}>
@@ -218,6 +228,7 @@ const handleSubmit = (event) => {
218228
handleFilterChange(index, "value", event.target.value)
219229
}
220230
>
231+
{/* TODO: remove this. Maybe make a free-text field?*/}
221232
<MenuItem value="ONPRC">ONPRC</MenuItem>
222233
</Select>
223234
</FormControl>

jbrowse/src/client/JBrowse/VariantSearch/components/FilterFormModal.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Modal, Paper, Typography } from "@material-ui/core";
2-
import React from "react";
3-
import FilterForm from "./FilterForm";
1+
import { Modal, Paper, Typography } from '@material-ui/core';
2+
import React from 'react';
3+
import FilterForm, { FilterFormProps } from './FilterForm';
44

5-
export const FilterFormModal = ({ open, handleClose, ...props }) => {
5+
export const FilterFormModal = ({ open, handleClose, filterProps }: {open: boolean, handleClose: any, filterProps: FilterFormProps }) => {
66
const body = (
77
<Paper style={{position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', padding: '1em'}}>
88
<Typography variant="h6">Filters</Typography>
9-
<FilterForm {...props} handleClose={handleClose} />
9+
<FilterForm {...filterProps} handleClose={handleClose} />
1010
</Paper>
1111
);
1212

jbrowse/src/client/JBrowse/VariantSearch/components/VariantTableWidget.tsx

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { observer } from 'mobx-react';
22
import {
33
DataGrid,
4-
getGridNumericColumnOperators,
54
GridColDef,
65
GridColumns,
76
GridRenderCellParams,
@@ -17,7 +16,6 @@ import { getConf } from '@jbrowse/core/configuration';
1716
import { AppBar, Box, Button, Dialog, Paper, Popover, Toolbar, Tooltip, Typography } from '@material-ui/core';
1817
import { APIDataToRows } from '../dataUtils';
1918
import ArrowPagination from './ArrowPagination';
20-
import { multiModalOperator, multiValueComparator } from '../constants';
2119
import { FilterFormModal } from './FilterFormModal';
2220
import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache';
2321
import { EVAdapterClass } from '../../Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter';
@@ -217,7 +215,7 @@ const VariantTableWidget = observer(props => {
217215

218216
const [filterModalOpen, setFilterModalOpen] = useState(false);
219217
const [filters, setFilters] = useState([]);
220-
const [fieldTypeInfo, setFieldTypeInfo] = useState([]);
218+
const [fieldTypeInfo, setFieldTypeInfo] = useState<FieldModel[]>([]);
221219

222220
const [adapter, setAdapter] = useState<EVAdapterClass | undefined>(undefined)
223221

@@ -240,42 +238,15 @@ const VariantTableWidget = observer(props => {
240238

241239
await fetchFieldTypeInfo(sessionId, trackGUID,
242240
(res: FieldModel[]) => {
243-
let columns: any = [];
244-
245-
for (const fieldObj of res) {
246-
const field = fieldObj.name;
247-
const type = fieldObj.type;
248-
let muiFieldType;
249-
250-
switch (type) {
251-
case 'Flag':
252-
case 'String':
253-
case 'Character':
254-
case 'Impact':
255-
muiFieldType = "string";
256-
break;
257-
case 'Float':
258-
case 'Integer':
259-
muiFieldType = "number";
260-
break;
261-
}
262-
263-
let column: any = { field: field, renderCell: (params: any) => { return <TableCellWithPopover value={params.value} /> }, description: fieldObj.description , headerName: fieldObj.label ?? field, minWidth: 25, width: fieldObj.colWidth ?? 50, maxWidth: 100, type: muiFieldType, flex: 1, headerAlign: 'left', align: "left", hide: fieldObj.isHidden }
264-
265-
if (fieldObj.isMultiValued || field == "af") {
266-
column.sortComparator = multiValueComparator
267-
column.filterOperators = getGridNumericColumnOperators().map(op => multiModalOperator(op))
268-
}
269-
270-
column.orderKey = fieldObj.orderKey;
271-
272-
columns.push(column)
273-
}
241+
res.sort((a, b) => a.orderKey - b.orderKey);
274242

275-
columns.sort((a, b) => a.orderKey - b.orderKey);
276-
const columnsWithoutOrderKey = columns.map(({ orderKey, ...rest }) => rest);
243+
let columns: GridColDef[] = res.map((x) => {
244+
return {...x.toGridColDef(),
245+
renderCell: (params: any) => { return <TableCellWithPopover value={params.value} /> }
246+
}
247+
})
277248

278-
setColumns(columnsWithoutOrderKey);
249+
setColumns(columns);
279250
const operators = fieldTypeInfoToOperators(res)
280251
setAvailableOperators(operators)
281252
setFieldTypeInfo(res)
@@ -408,17 +379,15 @@ const VariantTableWidget = observer(props => {
408379
};
409380

410381
const filterModal = (
411-
<FilterFormModal open={filterModalOpen} handleClose={() => setFilterModalOpen(false)}
412-
sessionId={sessionId}
413-
trackGUID={trackGUID}
414-
handleQuery={(filters) => handleQuery(filters)}
415-
setFilters={setFilters}
416-
handleFailureCallback={() => {}}
417-
availableOperators={availableOperators}
418-
fieldTypeInfo={fieldTypeInfo}
419-
components={{
420-
headerCell: renderHeaderCell,
421-
}}
382+
<FilterFormModal
383+
open={filterModalOpen}
384+
handleClose={() => setFilterModalOpen(false)}
385+
filterProps={{
386+
setFilters: setFilters,
387+
availableOperators: availableOperators,
388+
fieldTypeInfo: fieldTypeInfo,
389+
handleQuery: (filters) => handleQuery(filters)
390+
}}
422391
/>
423392
);
424393

jbrowse/src/client/JBrowse/utils.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { isEmptyObject } from 'jquery';
22
import jexl from 'jexl';
33
import { createViewState, loadPlugins } from '@jbrowse/react-linear-genome-view';
44
import { ActionURL, Ajax } from '@labkey/api';
5+
import { getGridNumericColumnOperators, GridColDef } from '@mui/x-data-grid';
6+
import { multiModalOperator, multiValueComparator } from './VariantSearch/constants';
57

68
export function arrayMax(array) {
79
return Array.isArray(array) ? Math.max(...array) : array
@@ -426,7 +428,6 @@ export class FieldModel {
426428
description: string
427429
type: string
428430
isInDefaultColumns: boolean
429-
isIndexed: boolean
430431
isMultiValued: boolean
431432
isHidden: boolean
432433
colWidth: number
@@ -435,6 +436,60 @@ export class FieldModel {
435436
allowableValues: string[]
436437
category: string
437438
url: string
439+
flex: number
440+
supportsFilter: boolean = true
441+
442+
getMuiType(): string {
443+
let muiFieldType;
444+
445+
switch (this.type) {
446+
case 'Flag':
447+
case 'String':
448+
case 'Character':
449+
case 'Impact':
450+
muiFieldType = "string";
451+
break;
452+
case 'Float':
453+
case 'Integer':
454+
muiFieldType = "number";
455+
break;
456+
}
457+
458+
return muiFieldType
459+
}
460+
461+
toGridColDef(): GridColDef {
462+
let gridCol: GridColDef = {
463+
field: this.name,
464+
description: this.description,
465+
headerName: this.label ?? this.name,
466+
minWidth: 25,
467+
width: this.colWidth ?? 50,
468+
type: this.getMuiType(),
469+
flex: this.flex || 1,
470+
headerAlign: 'left',
471+
align: "left",
472+
//TODO: consider whether we really need a separate isHidden
473+
hide: this.isHidden || this.isInDefaultColumns === false
474+
}
475+
476+
// TODO: can we pass the JEXL format string here? How does this impact filter/sorting?
477+
// if (this.formatString) {
478+
// gridCol.type = "string"
479+
// gridCol.valueFormatter = (params: GridValueFormatterParams) => {
480+
// const context = {...params.row}
481+
// return jexl.evalSync(this.formatString, context)
482+
// }
483+
// }
484+
485+
// TODO: does this really apply here? Can we drop it?
486+
if (this.isMultiValued) {
487+
gridCol.sortComparator = multiValueComparator
488+
gridCol.filterOperators = getGridNumericColumnOperators().map(op => multiModalOperator(op))
489+
}
490+
491+
return gridCol
492+
}
438493
}
439494

440495
export async function fetchFieldTypeInfo(sessionId: string, trackId: string, successCallback: (res: FieldModel[]) => void, failureCallback) {
@@ -466,6 +521,13 @@ export function truncateToValidGUID(str: string) {
466521
return str;
467522
}
468523

524+
// TODO: we should have a class for this
525+
export declare type Filter = {
526+
field: string,
527+
value: any,
528+
operator: string
529+
}
530+
469531
export function searchStringToInitialFilters(operators) : any[] {
470532
const queryParam = new URLSearchParams(window.location.search)
471533
const searchString = queryParam.get("searchString")
@@ -486,7 +548,7 @@ export function searchStringToInitialFilters(operators) : any[] {
486548
return initialFilters
487549
}
488550

489-
export function fieldTypeInfoToOperators(fieldTypeInfo): any {
551+
export function fieldTypeInfoToOperators(fieldTypeInfo: FieldModel[]): any {
490552
const stringType = ["equals", "does not equal", "contains", "does not contain", "starts with", "ends with", "is empty", "is not empty"];
491553
const variableSamplesType = ["in set", "variable in", "not variable in", "variable in all of", "variable in any of", "not variable in any of", "not variable in one of", "is empty", "is not empty"];
492554
const numericType = ["=", "!=", ">", ">=", "<", "<=", "is empty", "is not empty"];

jbrowse/src/org/labkey/jbrowse/JBrowseLuceneSearch.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,10 @@ public void customizeField(JBrowseFieldDescriptor field, Container c, User u)
276276
{
277277
switch (field.getFieldName())
278278
{
279-
case "AF" -> field.label("Allele Frequency");
279+
case "AF" -> field.label("Allele Frequency").inDefaultColumns(true);
280280
case "AC" -> field.label("Allele Count");
281281
case "MAF" -> field.label("Minor Allele Frequency");
282-
case "IMPACT" ->
283-
field.label("Impact on Protein Coding").allowableValues(Arrays.asList("HIGH", "LOW", "MODERATE", "MODIFIER"));
282+
case "IMPACT" -> field.label("Impact on Protein Coding").allowableValues(Arrays.asList("HIGH", "LOW", "MODERATE", "MODIFIER")).inDefaultColumns(true);
284283
}
285284
}
286285
}

jbrowse/test/src/org/labkey/test/tests/external/labModules/JBrowseTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,4 +1522,9 @@ private void testVariantTableComparators() throws Exception {
15221522
"0.008258, 0.44, 0.17, 0.036, 0.005367, 0.019, 0", "intron_variant", "", "NTNG1", "");
15231523
waitAndClick(Locator.tagWithAttributeContaining("button", "aria-label", "Show filters"));
15241524
}
1525+
1526+
private void testLuceneSearchUI()
1527+
{
1528+
//TODO: actually test the grid UI
1529+
}
15251530
}

0 commit comments

Comments
 (0)