higlass
Version:
HiGlass Hi-C / genomic / large data viewer
428 lines (366 loc) • 11.3 kB
JSX
// @ts-nocheck
import PropTypes from 'prop-types';
import React from 'react';
import CheckboxTree from 'react-checkbox-tree';
import slugid from 'slugid';
import { tileProxy } from './services';
import '../styles/TilesetFinder.css';
import withPubSub from './hocs/with-pub-sub';
// Configs
import { TRACKS_INFO } from './configs';
export class TilesetFinder extends React.Component {
constructor(props) {
super(props);
// this.localTracks = TRACKS_INFO.filter
// local tracks are ones that don't have a filetype associated with them
this.localTracks = TRACKS_INFO.filter((x) => x.local && !x.hidden).map(
(x) => {
const y = { ...x };
y.datatype = x.datatype[0];
return y;
},
);
this.augmentedTracksInfo = TRACKS_INFO;
if (window.higlassTracksByType) {
Object.keys(window.higlassTracksByType).forEach((pluginTrackType) => {
this.augmentedTracksInfo.push(
window.higlassTracksByType[pluginTrackType].config,
);
});
}
if (props.datatype) {
this.localTracks = this.localTracks.filter(
(x) => x.datatype[0] === props.datatype,
);
} else {
this.localTracks = this.localTracks.filter(
(x) => x.orientation === this.props.orientation,
);
}
this.localTracks.forEach((x) => {
x.uuid = slugid.nice();
});
const newOptions = this.prepareNewEntries('', this.localTracks, {});
const availableTilesetKeys = Object.keys(newOptions);
const selectedUuid = availableTilesetKeys.length
? [availableTilesetKeys[0]]
: null;
this.mounted = false;
this.state = {
selectedUuid,
options: newOptions,
filter: '',
checked: [],
expanded: [],
};
this.requestTilesetLists();
}
componentDidMount() {
// we want to query for a list of tracks that are compatible with this
// track orientation
this.mounted = true;
this.requestTilesetLists();
this.searchBox.focus();
}
componentWillUnmount() {
this.mounted = false;
}
prepareNewEntries(sourceServer, newEntries, existingOptions) {
/**
* Add meta data to new tileset entries before adding
* them to the list of available options.
*/
const newOptions = existingOptions;
const entries = newEntries.map((ne) => {
const ane = {
...ne,
server: sourceServer,
tilesetUid: ne.uuid,
serverUidKey: this.serverUidKey(sourceServer, ne.uuid),
datatype: ne.datatype,
name: ne.name,
uid: slugid.nice(),
};
return ane;
});
entries.forEach((ne) => {
newOptions[ne.serverUidKey] = ne;
});
return newOptions;
}
serverUidKey(server, uid) {
/**
* Create a key for a server and uid
*/
return `${server}/${uid}`;
}
requestTilesetLists() {
let datatypesQuery = null;
if (this.props.datatype) {
datatypesQuery = `dt=${this.props.datatype}`;
} else {
const datatypes = new Set(
[].concat(
...this.augmentedTracksInfo
.filter((x) => x.datatype)
.filter((x) => {
return (
x.orientation === this.props.orientation ||
(this.props.orientation === '1d-vertical' &&
x.orientation === '1d-horizontal')
);
})
.map((x) => x.datatype),
),
);
datatypesQuery = [...datatypes].map((x) => `dt=${x}`).join('&');
}
if (!this.props.trackSourceServers) {
console.warn('No track source servers specified in the viewconf');
return;
}
this.props.trackSourceServers.forEach((sourceServer) => {
tileProxy.json(
`${sourceServer}/tilesets/?limit=10000&${datatypesQuery}`,
(error, data) => {
if (error) {
console.error('ERROR:', error);
} else {
const newOptions = this.prepareNewEntries(
sourceServer,
data.results,
this.state.options,
);
const availableTilesetKeys = Object.keys(newOptions);
let { selectedUuid } = this.state;
// if there isn't a selected tileset, select the first received one
if (!selectedUuid) {
selectedUuid = availableTilesetKeys.length
? [availableTilesetKeys[0]]
: null;
const selectedTileset = this.state.options[selectedUuid[0]];
this.props.selectedTilesetChanged([selectedTileset]);
}
if (this.mounted) {
this.setState({
selectedUuid,
options: newOptions,
});
}
}
},
this.props.pubSub,
);
});
}
/**
* Double clicked on an element. Should be selected
* and this window will be closed.
*/
handleOptionDoubleClick(x /* , y */) {
const value = this.state.options[x.target.value];
this.props.onDoubleClick(value);
}
handleSelectedOptions(selectedOptions) {
const selectedValues = [];
const selectedTilesets = [];
// I don't know why selectedOptions.map doesn't work
for (let i = 0; i < selectedOptions.length; i++) {
selectedValues.push(selectedOptions[i]);
selectedTilesets.push(this.state.options[selectedOptions[i]]);
}
this.props.selectedTilesetChanged(selectedTilesets);
this.setState({ selectedUuid: selectedValues });
}
handleSelect() {
const { selectedOptions } = this.multiSelect;
const selectedOptionsList = [];
for (let i = 0; i < selectedOptions.length; i++) {
const selectedOption = selectedOptions[i];
selectedOptionsList.push(selectedOption.value);
}
this.handleSelectedOptions(selectedOptionsList);
}
handleSearchChange() {
const domElement = this.searchBox;
this.setState({ filter: domElement.value });
}
/*
* Create the nested checkbox tree by partitioning the
* list of available datasets according to their group
*
* @param {object} datasetsDict A dictionary of id -> tileset
* def mappings
* @param {string} filter A string to filter the results by
* @returns {array} A list of items that define the nested checkbox
* structure
* {
* 'name': 'blah',
* 'value': 'blah',
* 'children': [],
* }
*/
partitionByGroup(datasetsDict, filter) {
const itemsByGroup = {
'': {
name: '',
value: '',
children: [],
},
};
for (const uuid of Object.keys(datasetsDict)) {
const item = datasetsDict[uuid];
if (!item.name.toLowerCase().includes(filter)) {
continue;
}
if ('project_name' in item) {
const group = item.project_name;
if (!(group in itemsByGroup)) {
itemsByGroup[group] = {
value: group,
label: group,
children: [],
};
}
itemsByGroup[group].children.push({
label: item.name,
value: uuid,
});
} else {
itemsByGroup[''].children.push({
label: item.name,
value: uuid,
});
}
}
const allItems = itemsByGroup[''].children;
// coollapse the group lists into one list of objects
for (const group of Object.keys(itemsByGroup)) {
if (group !== '') {
itemsByGroup[group].children.sort((a, b) =>
a.label.toLowerCase().localeCompare(b.label.toLowerCase(), 'en'),
);
allItems.push(itemsByGroup[group]);
}
}
allItems.sort((a, b) =>
a.label.toLowerCase().localeCompare(b.label.toLowerCase(), 'en'),
);
return allItems;
}
handleChecked(checked) {
this.handleSelectedOptions(checked);
this.setState({ checked });
}
handleExpanded(expanded) {
this.setState({ expanded });
}
render() {
const optionsList = [];
for (const key in this.state.options) {
optionsList.push(this.state.options[key]);
}
const nestedItems = this.partitionByGroup(
this.state.options,
this.state.filter,
);
const svgStyle = {
width: 15,
height: 15,
top: 2,
right: 2,
position: 'relative',
};
const halfSvgStyle = JSON.parse(JSON.stringify(svgStyle));
halfSvgStyle.opacity = 0.5;
const form = (
<form
onSubmit={(evt) => {
evt.preventDefault();
}}
>
<div className="tileset-finder-search-bar">
<span className="tileset-finder-label">Select tileset:</span>
<input
ref={(c) => {
this.searchBox = c;
}}
className="tileset-finder-search-box"
onChange={this.handleSearchChange.bind(this)}
placeholder="Search Term"
type="text"
/>
</div>
<div className="tileset-finder-checkbox-tree">
<CheckboxTree
checked={this.state.checked}
expanded={this.state.expanded}
icons={{
uncheck: (
<svg style={svgStyle}>
<title>Uncheck</title>
<use xlinkHref="#square_o" />
</svg>
),
check: (
<svg style={svgStyle}>
<title>Check</title>
<use xlinkHref="#check_square_o" />
</svg>
),
halfCheck: (
<svg style={halfSvgStyle}>
<title>Half Check</title>
<use xlinkHref="#check_square_o" />
</svg>
),
leaf: (
<svg style={svgStyle}>
<title>Leaf</title>
<use xlinkHref="#file_o" />
</svg>
),
expandClose: (
<svg style={svgStyle}>
<title>Expand Close</title>
<use xlinkHref="#chevron_right" />
</svg>
),
expandOpen: (
<svg style={svgStyle}>
<title>Expand Open </title>
<use xlinkHref="#chevron_down" />
</svg>
),
parentClose: (
<svg style={svgStyle}>
<title>Parent Close</title>
<use xlinkHref="#folder_o" />
</svg>
),
parentOpen: (
<svg style={svgStyle}>
<title>Parent Open</title>
<use xlinkHref="#folder_open_o" />
</svg>
),
}}
nodes={nestedItems}
onCheck={this.handleChecked.bind(this)}
onExpand={this.handleExpanded.bind(this)}
/>
</div>
</form>
);
return <div>{form}</div>;
}
}
TilesetFinder.propTypes = {
datatype: PropTypes.string,
orientation: PropTypes.string,
onDoubleClick: PropTypes.func,
pubSub: PropTypes.object.isRequired,
selectedTilesetChanged: PropTypes.func,
trackSourceServers: PropTypes.array,
};
export default withPubSub(TilesetFinder);