<commit_before>'use strict';

require('./base');
const React = require('react');
var {uniq, flatten, sortBy, groupBy, map, flatmap, partitionN, mapObject,
	pluck, concat, where, contains, get, updateIn, range, Let, pick,
	zip, identity, getIn, sum, keys, values, mmap} = require('./underscore_ext');
var {Observable: {from}, Scheduler: {animationFrame}} = require('./rx');
var {parseDsID, testStatus} = require('./xenaQuery');
import Link from 'react-toolbox/lib/link';
var styles = require('./Datapages.module.css');
var nav = require('./nav');
import {Checkbox} from 'react-toolbox/lib/checkbox';
import {Button} from 'react-toolbox/lib/button';
var showdown = require('showdown');
var {stripHTML} = require('./dom_helper');
var treehouseImg = require('../images/Treehouse.jpg');
var {rxEvents} = require('./react-utils');
var {servers: {localHub}, serverNames} = require('./defaultServers');
import Dialog from 'react-toolbox/lib/dialog';
var {encodeObject, urlParams} = require('./util');
import {ThemeProvider} from 'react-css-themr';
var appTheme = require('./appTheme');
var {getHubParams} = require('./hubParams');
var Rx = require('./rx');
var platform = require('platform');
import Drawer from 'react-toolbox/lib/drawer';
import PureComponent from './PureComponent';
var spinner = require('./ajax-loader.gif');

var getHubName = host => get(serverNames, host, host);

var pluralize = (str, count) => count === 1 ? `1 ${str}` : `${count} ${str}s`;

// Get params from the anchor href. With RT Link, the anchor is parentElement.
var paramFromHref = ev => mapObject(urlParams(ev.target.parentElement.href), a => a[0]);

//
// event handler for navigating within datapages. Prevents page load, and
// sets url params.
//

function navHandler(ev) {
	ev.preventDefault();
	this.props.callback(['navigate', 'datapages', paramFromHref(ev, this.props.state)]);
}

var getUserServers = servers => keys(servers).filter(k => servers[k].user);

//
// Data hubs sidebar
//

var hubLink = (host, onClick, hubParams) => (
	<Link
		className={styles.link}
		href={'?' + encodeObject({host, ...hubParams})}
		label={getHubName(host)}
		onClick={onClick}/>);

class DataHubs extends React.Component {
	onHub = (ev) => { navHandler.call(this, ev); };

	onSelect = (isOn, ev) => {
		var {checked} = ev.target,
			host = ev.target.getAttribute('data-host');
		this.props.callback([checked ? 'enable-host' : 'disable-host', host, 'user']);
	};

	render() {
		var {hubParams, state: {spreadsheet: {servers}}} = this.props;
		return (
			<div className={styles.dataHubs}>
				<h2>Active Data Hubs</h2>
				<ul>
					{map(servers,
						({user}, host) => (
							<li key={host}>
								<Checkbox
									label={hubLink(host, this.onHub, hubParams)}
									onChange={this.onSelect}
									checked={user}
									data-host={host}/>
							</li>))}
				</ul>
			</div>);
	}
}


//
// Cohort Summary Page
//

var treehouse = cohort =>
	cohort.search(/^Treehouse/gi) === -1 ? null :
	<img src={treehouseImg} height='40px'/>;

var cohortLink = (cohort, onClick, hubParams) => (
	<Link
		className={styles.link}
		href={'?' + encodeObject({cohort, ...hubParams})}
		label={cohort}
		onClick={onClick}/>);

var collateCohorts = hubCohorts =>
	concat(...pluck(hubCohorts, 'cohorts')).reduce(
		(acc, cohort) => updateIn(acc, [cohort.cohort],
			(v = 0) => cohort.count + v),
		{});

var CohortSummary = ({cohorts, onCohort, hubParams, action}) => {
	var names = sortBy(keys(cohorts), c => c.toLowerCase()),
		nCohorts = names.length,
		nDatasets = sum(values(cohorts));
	return (
		<div>
			<h2>{pluralize('Cohort', nCohorts)}, {pluralize('Dataset', nDatasets)} {action}</h2>
			<ul className={styles.list}>
				{map(names, name =>
					<li key={name}>
						{treehouse(name)}
						{cohortLink(name, onCohort, hubParams)}
						{` (${pluralize('dataset', cohorts[name])})`}
					</li>)}
			</ul>
		</div>);
};

class CohortSummaryPage extends React.Component {
	onCohort = (ev) => { navHandler.call(this, ev); };

	render() {
		var {hubParams, state} = this.props,
			{spreadsheet: {servers}} = state,
			userServers = getUserServers(servers),
			cohorts = getIn(this.props.state, ['datapages', 'cohorts'], []),
			activeCohorts = cohorts.filter(c => contains(userServers, c.server)),
			combined = collateCohorts(activeCohorts);
		return (
			<div className={styles.datapages}>
				<DataHubs {...this.props}/>
				<CohortSummary hubParams={hubParams} cohorts={combined} onCohort={this.onCohort}/>
			</div>);
	}
}

//
// Dataset delete button
//

class DeleteButton extends React.Component {
	state = {active: false};

	onDelete = () => {
		this.setState({active: !this.state.active});
	};

	onReally = () => {
		var {name} = this.props;
		this.props.callback(['navigate', 'datapages', {host: localHub}]);
		this.props.callback(['delete-dataset', localHub, name]);
	};

	actions = () => {
		return [
			{label: 'Cancel', onClick: this.onDelete},
			{label: 'Really Delete', onClick: this.onReally}];
	};

	render() {
		var {active} = this.state,
			{label} = this.props;
		return (
			<div className={styles.deleteButton}>
				<Dialog actions={this.actions()} active={active}
						onEscKeyDown={this.onDelete} onOverlayClick={this.onDelete}
						title='Delete dataset'>
					{label}
				</Dialog>
				<Button onClick={this.onDelete} accent>Delete</Button>
			</div>);
	}
}

var canDelete = ({status}, host) =>
	host === localHub && contains(['loaded', 'error'], status);

var markdownValue = value => {
	if (value) {
		var converter = new showdown.Converter();
		return (<div className={styles.header}
			dangerouslySetInnerHTML={{__html: converter.makeHtml(value)}}/>);
	}
};

//
// Cohort Datasets Page
//

var datasetLink = (callback, preferred, onClick, hubParams) => ds => {
	var [host] = parseDsID(ds.dsID);
	return (
		<li key={ds.name}>
			<Link
				className={styles.link}
				href={'?' + encodeObject({dataset: ds.name, host, ...hubParams})}
				label={ds.label}
				onClick={onClick}/>
			{preferred.has(ds.dsID) ? <span className={styles.star}>*</span> : null}
			{ds.status !== 'loaded' ?
				<span className={styles.count}> [{ds.status}]</span> : null}
			{ds.status === 'loaded' ?
				<span className={styles.count}> (n={(ds.count || 0).toLocaleString()})</span> :
				null}
			<span> {getHubName(host)}</span>
			{canDelete(ds, host) ?
				<DeleteButton callback={callback} name={ds.name} label={ds.label || ds.name}/> : null}
			<div className={styles.lineClamp}>
				<span className={styles.description}>{stripHTML(ds.description)}</span>
			</div>
		</li>);
};

var drawGroup = (callback, groups, preferred, onClick, hubParams) => dataSubType => {
	var list = sortBy(groups[dataSubType], g => g.label.toLowerCase());
	return (
		<div key={dataSubType}>
			<h3>{dataSubType}</h3>
			<ul className={styles.groupList}>
				{map(list, datasetLink(callback, preferred, onClick, hubParams))}
			</ul>
		</div>);
};

var COHORT_NULL = '(unassigned)';

var getPreferred = (wizard, cohort) =>
	new Set(values(getIn(wizard, ['cohortPreferred', cohort], {})));

class CohortPage extends React.Component {
	onViz = () => {
		var {datapages, spreadsheet: {cohort: currentCohort}} = this.props.state,
			cohort = getIn(datapages, ['cohort', 'cohort'], COHORT_NULL);

		if (cohort !== get(currentCohort, 'name')) {
			this.props.callback(['cohort', cohort]);
		}
		this.props.callback(['navigate', 'heatmap']);
	};

	onDataset = (ev) => { navHandler.call(this, ev); };

	render() {
		var {hubParams, state: {datapages, params, wizard}} = this.props,
			cohort = getIn(datapages, ['cohort', 'cohort']) === params.cohort ?
				datapages.cohort : {cohort: '...', datasets: []},
			{callback} = this.props,
			dsGroups = groupBy(values(cohort.datasets), 'dataSubType'),
			dataSubTypes = sortBy(keys(dsGroups), g => g.toLowerCase()),
			preferred = getPreferred(wizard, params.cohort);

		return (
			<div className={styles.datapages}>
				{markdownValue(cohort.meta)}
				<div className={styles.sidebar}>
					<Button onClick={this.onViz} accent>Visualize</Button>
				</div>
				<h2>cohort: {treehouse(cohort.cohort)}{cohort.cohort}</h2>
				{dataSubTypes.map(drawGroup(callback, dsGroups, preferred, this.onDataset, hubParams))}
				{preferred.size === 0 ? null : (
					<span>
						<span className={styles.star}>*</span>
						<span>default dataset in visualization</span>
					</span>)}
			</div>);
	}
}

//
// Dataset Page
//

var TYPE_NULL = 'genomicMatrix',
	FORMAT_MAPPING = {
		'clinicalMatrix': "ROWs (samples)  x  COLUMNs (identifiers) (i.e. clinicalMatrix)",
		'genomicMatrix': "ROWs (identifiers)  x  COLUMNs (samples) (i.e. genomicMatrix)",
		'mutationVector': "Variant by Position (i.e. mutationVector)",
		'genomicSegment': 'Genomic Segment (i.e. genomicSegment)',
		'unknown': "unknown"
	};

var dataPair = (key, value, method = identity) => value == null ? [] : [
	<span className={styles.key}>{key}</span>,
	<span className={styles.value}>{method(value)}</span>,
	<br/>];

var toLink = value => <a href={value}>{value}</a>;
var toCohortLink = (onClick, hubParams) => cohort => (
	<Link
		href={'?' + encodeObject({cohort, ...hubParams})}
		onClick={onClick}
		label={cohort}/>);
var toPMIDLink = value => (
	<a href={`http://www.ncbi.nlm.nih.gov/pubmed/?term=${value.toString()}`}>
		{value}
	</a>);

var jsonLink = link => link.replace(/(.gz)?$/, '.json');
var toDownloadLink = value => (
	<span>
		<a href={value}>{value}</a>
		{'; '}
		<a href={jsonLink(value)}>Full metadata</a>
	</span>);
var toHTML = value => <span dangerouslySetInnerHTML={{__html: value}}/>;

var headerValue = value => value && <p className={styles.header}>{value}</p>;
var htmlValue = value => value &&
	<p className={styles.header} dangerouslySetInnerHTML={{__html: value}}/>;

// XXX Does not display load warning in pop-up
// XXX Does not use red/blue coloring for error vs. other load status (e.g. 'loading')
var getStatus = (status, loaderWarning) =>
	status === 'loaded' && !loaderWarning ? [] :
	status === 'loaded' ? dataPair('status', 'loaded with warning') :
	dataPair('status', status);

// split, handling falsey values
var split = (str, pat) => str ? str.split(pat) : [];

var resolveCodes = (probes, codes, data) =>
	mmap(probes, data, (probe, row) =>
			codes[probe] ? map(row, v => isNaN(v) ? v : codes[probe][v]) :
			row);

// not the most efficient algorithm :-/
var addHeaders = (fields, samples, data) =>
	mmap(['', ...fields], [samples, ...data], (header, row) => [header, ...row]);

var transpose = data => zip.apply(null, data);

// Transpose matrix if it's clinical
var transposeClinical = (meta, data) =>
	get(meta, 'type') === 'clinicalMatrix' ? transpose(data) :
	data;

var table = data => (
		<table className={styles.dataSnippetTable}>
			<tbody>
				{map(data, (row, i) => <tr key={i}>{map(row, (c, j) => <td key={j}>{c}</td>)}</tr>)}
			</tbody>
		</table>);

var matrixTable = (meta, {fields, samples, codes, data}) =>
	table(transposeClinical(meta, addHeaders(fields, samples, resolveCodes(fields, codes, data))));

// swap chromend & chromstart when sorting fields
var cmpSparseFields = (a, b) =>
	a === 'chromend' && b === 'chromstart' ? 1 :
	a === 'chromstart' && b === 'chromend' ? -1 :
	a.localeCompare(b);

var sparseTable = (meta, data) => {
	var fields = keys(data).sort(cmpSparseFields),
		rows = fields.map(field => data[field]),
		samples = data.sampleID;
	return table(transpose(addHeaders(fields, samples, rows)));
};

var noTable = () => null;

var dataMethod = ({type = 'genomicMatrix', status} = {}) =>
	status !== 'loaded' ? noTable :
	type === 'genomicMatrix' ? matrixTable :
	type === 'clinicalMatrix' ? matrixTable :
	type === 'mutationVector' ? sparseTable :
	type === 'genomicSegment' ? sparseTable :
	noTable;

var setKey = arr => arr.map((el, i) => React.cloneElement(el, {key: i}));

class DatasetPage extends React.Component {
	onCohort = (ev) => { navHandler.call(this, ev); };

	onViz = () => {
		var {datapages, spreadsheet: {cohort: currentCohort}} = this.props.state,
			cohort = getIn(datapages, ['dataset', 'meta', 'cohort'], COHORT_NULL);

		if (cohort !== get(currentCohort, 'name')) {
			this.props.callback(['cohort', cohort]);
		}
		this.props.callback(['navigate', 'heatmap']);
	};

	onIdentifiers = (ev) => { navHandler.call(this, ev); };
	onSamples = (ev) => { navHandler.call(this, ev); };

	render() {
		var {callback, state, hubParams} = this.props,
			{params: {host, dataset}, datapages} = state,
			{meta, probeCount = 0, data, downloadLink, probemapLink, dataset: currentDataset,
				host: currentHost} = get(datapages, 'dataset', {});

		if (!meta || currentHost !== host || currentDataset !== dataset) {
			return (
				<div className={styles.datapages}>
					<h2>dataset: ...</h2>
				</div>);
		}
		var {name, label = name, description, longTitle,
			cohort = COHORT_NULL, dataSubType, platform, unit,
			assembly, version, url, articletitle, citation, pmid,
			dataproducer, author = dataproducer,
			'wrangling_procedure': wranglingProcedure,
			type = TYPE_NULL, status, loader, count} = meta;

		return (
			<div className={styles.datapages}>
				<div className={styles.sidebar}>
					<Button onClick={this.onViz} accent>Visualize</Button>
					{canDelete(meta, host) ?
						<DeleteButton callback={callback} name={name} label={label}/> : null}
				</div>
				<h2>dataset: {(dataSubType ? dataSubType + ' - ' : '') + label}</h2>
				{headerValue(longTitle)}
				{htmlValue(description)}
				{setKey(flatten([
					dataPair('cohort', cohort, toCohortLink(this.onCohort, hubParams)),
					dataPair('dataset ID', name),
					getStatus(status, loader),
					dataPair('download', downloadLink, toDownloadLink),
					dataPair('samples', count),
					dataPair('version', version),
					dataPair('hub', host),
					dataPair('type of data', dataSubType),
					dataPair('assembly', assembly),
					dataPair('unit', unit),
					dataPair('platform', platform),
					dataPair('ID/Gene Mapping', probemapLink, toDownloadLink),
					dataPair('publication', articletitle),
					dataPair('citation', citation),
					dataPair('author', author),
					dataPair('PMID', pmid, toPMIDLink),
					flatmap(uniq(split(url, /,/)), url =>
						dataPair('raw data', url, toLink)),
					dataPair('wrangling', wranglingProcedure, toHTML),
					dataPair('input data format', FORMAT_MAPPING[type])]))}
				{status === 'loaded' ?
					<span className={styles.tableControls}>
						{type === 'genomicMatrix' ?
							`${probeCount.toLocaleString()} identifiers X ${count} samples` : null}
						{type === 'clinicalMatrix' ?
							`${count} samples X ${probeCount.toLocaleString()} identifiers` : null}
						<Link
							href={'?' + encodeObject({host, dataset, allIdentifiers: true, ...hubParams})}
							onClick={this.onIdentifiers} label='All Identifiers'/>
						<Link
							href={'?' + encodeObject({host, dataset, allSamples: true, ...hubParams})}
							onClick={this.onSamples} label='All Samples'/>
					</span> : null}
				{dataMethod(meta)(meta, data)}
			</div>);
	}
}

// Our handling of parameters 'hub' and 'host', is somewhat confusing. 'host'
// means "show the hub page for this url". 'hub' means "add this url to the
// active hub list, and, if in /datapages/ show the hub page for this url".
// The 'hub' parameter can be repeated, which adds each hub to the active hub
// list. Only the first one will be displayed when linking to /datapages/.
// Needs refactor.
var defaultHost = params =>
	Let(({host, hubs} = params) =>
			!host && hubs ? {...params, host: hubs[0]} : params);

var osFiles = {
		osxJre: {
			pattern: "ucsc_xena_macos_[_0-9]*_with_jre.dmg",
			description: "OSX installer, bundled JRE",
			help: "Recommended for OSX 10.7 and above"
		},
		osxNoJre: {
			pattern: "ucsc_xena_macos_[_0-9]*.dmg",
			description: "OSX installer, no JRE",
			help: "Recommended for OSX 10.6"
		},
		win32: {
			pattern: "ucsc_xena_windows_[_0-9]*.exe",
			description: "Windows 32 bit installer, bundled JRE",
			help: "Recommended for all 32 bit versions of Windows"
		},
		win64: {
			pattern: "ucsc_xena_windows-x64_[_0-9]*.exe",
			description: "Windows 64 bit installer, bundled JRE",
			help: "Recommended for all 64 bit versions of Windows"
		},
		tar: {
			pattern: "ucsc_xena_[_0-9]*.tar.gz",
			description: "Tar archive, no updater or JRE",
			help: "Recommended for linux server deployments"
		}
	}, defaults = {
		'OS X': {32: 'osxNoJre', 64: 'osxJre'},
		'Windows': {32: 'win32', 64: 'win64'},
		'Linux': {32: 'tar', 64: 'tar'}
	};

function findMatch(pattern, list) {
	var matches = list.filter(s => s.match(pattern));
	return matches.length > 0 ? matches[0] : undefined;
}

var matchPaths = serverFiles =>
	mapObject(osFiles, ({pattern}, key) =>
			  findMatch(osFiles[key].pattern, serverFiles));

var parseInt10 = s => parseInt(s, 10);

function osxArch() {
	var version = getIn(platform, ['os', 'version']),
		parts = version && version.match(/(\d*)\.(\d*)/).slice(1).map(parseInt10); // [maj, min, patch]
	return (parts && parts[0] === 10 && parts[1] <= 6) ? 32 : 64;
}

function getOs() {
	var family = getIn(platform, ['os', 'family']);
	if (family) {
		if (family.indexOf('Windows') !== -1) {
			return 'Windows';
		}
		if (family.indexOf('OS X') !== -1) {
			return 'OS X';
		}
		if (family.indexOf('Linux') !== -1) {
			return 'Linux';
		}
	}
}

var downloadDir = 'https://genome-cancer.ucsc.edu/download/public/get-xena';
var updatesPath = `${downloadDir}/updates.xml`;
var downloadPath = name => `${downloadDir}/${name}`;
var getFileName = e => downloadPath(e.getAttribute('fileName'));

class XenaDownload extends React.Component {
	state = {};

	componentDidMount() {
		this.sub = Rx.Observable.ajax({method: 'GET', responseType: 'xml', url: updatesPath, crossDomain: true}).subscribe(xml => {
			var files = matchPaths([...xml.response.getElementsByTagName('entry')]
								   .map(getFileName));

			this.setState({files}); //eslint-disable-line react/no-did-mount-set-state
		});
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	render() {
		var {files} = this.state,
			os = getOs(),
			arch = os === 'OS X' ? osxArch() : getIn(platform, ['os', 'architecture']),
			defaultTarget = getIn(defaults, [os, arch]),
			defaultInstall = get(files, defaultTarget);

		return (
			<div className={styles.download}>
				{defaultInstall ? <Link className={styles.downloadLink} href={defaultInstall} label="Install UCSC Xena hub"/> : null}
				{defaultInstall ? <br/> : null}
				{defaultInstall ? 'Other installers' : 'Please download an installer'}
				{files ? map(pick(osFiles, (_, k) => files[k]), (info, key) =>
					 <Link className={styles.downloadLink} href={files[key]} title={info.help} label={info.description}/>) : null}
				<br/>
				<Link className={styles.helpLink} href='http://xena.ucsc.edu/private-hubs/'>Help</Link>
			</div>);
	}
}

//
// Hub page
//

var refresh = 2000;
var localTimeout = 500; // use a short timeout for localhost

class HubPage extends PureComponent {
	state = {status: undefined, launched: undefined, showDownloads: false};

	onCohort = (ev) => {navHandler.call(this, ev);};

	launch = () => {
		var i = document.getElementById('xenaLauncher');
		if (i) {
			document.body.removeChild(i);
		}
		i = document.createElement('iframe');
		i.id = 'xenaLauncher';
		document.body.appendChild(i);
		i.src = 'ucscxena://';
		this.setState({launched: true});
	}

	onRaise = () => {
		Rx.Observable.ajax({method: 'GET', url: `${localHub}/raise/`,
				crossDomain: true}).subscribe(() => {});
	}

	componentDidMount() {
		var {state} = this.props,
			{host} = defaultHost(state.params),
			isLocal = host === localHub;

		if (isLocal) {
			this.sub = Rx.Observable.of(true).merge(Rx.Observable.interval(refresh))
				.flatMap(() => testStatus(host, localTimeout))
				.takeWhile(({status}) => status !== 'up')
				.concat(Rx.Observable.of({status: 'up'}))
				.subscribe(this.updatePing);

			// Try to launch if we don't get a response.
			this.timeoutID = setTimeout(() => {
				if (this.state.status !== 'up' && !this.state.launched) {
					this.launch();
				}
			}, 2000);
		}
	}

	componentWillUnmount() {
		if (this.timeoutID) {
			clearTimeout(this.timeoutID);
		}
		if (this.sub) {
			this.sub.unsubscribe();
		}
	}

	updatePing = resp => {
		if (resp.status === 'up' && this.state.status !== 'up') {
			this.props.callback(['cohort-summary-clear', {server: localHub}]);
		}
		this.setState(resp);
	}

	onShowDownloads = () => {
		this.setState({showDownloads: true});
	}

	onHideDownloads = () => {
		this.setState({showDownloads: false});
	}

	render() {
		var {status, showDownloads, launched} = this.state,
			{state, hubParams} = this.props,
			{spreadsheet: {servers}} = state,
			userServers = getUserServers(servers),
			{host} = defaultHost(state.params),
			isLocal = host === localHub,
			cohorts = getIn(state, ['datapages', 'cohorts'], []),
			hubCohorts = where(cohorts, {server: host}),
			coll = collateCohorts(hubCohorts),
			inHubs = contains(userServers, host) ?
				'' : ' (not in my data hubs)';

		return (
			<div className={styles.datapages}>
				<i className={`material-icons ${styles.open}`} onClick={this.onShowDownloads} title={'Install'}>chevron_left</i>
				<Drawer onOverlayClick={this.onHideDownloads} className={styles.drawer} active={showDownloads} type='right'>
					<i className={`material-icons ${styles.close}`} onClick={this.onHideDownloads} title={'Install'}>chevron_right</i>
					<XenaDownload/>
				</Drawer>
				<div className={isLocal && status !== 'up' ? styles.noticeVisible : styles.noticeHidden}>

					<div className={styles.localHubNotice}>
						<p>{status === 'starting' ? 'Local hub is starting.' :
							launched ? 'Trying to start local hub.' :
							'Trying to contact local hub.'}
							<img className={styles.spinner} src={spinner}/></p>
							{status !== 'starting' && launched ? (
								<div>
									<p>If you have previously installed a local
									hub, it should start momentarly. If it does not, please start it manually.</p>
									<br/>
									<p>If you would like to install a hub, open the links at right.</p>
								</div>) : null}
					</div>
				</div>

				{markdownValue(getIn(hubCohorts, [0, 'meta']))}
				<h2>{getHubName(host)}{inHubs}</h2>
				<p>Host address: {host}</p>
				<CohortSummary
					hubParams={hubParams}
					cohorts={coll}
					onCohort={this.onCohort}
					action={isLocal && status === 'up' ?
						<Button onClick={this.onRaise} accent>Import</Button> : null}/>
			</div>);
	}
}

//
// Samples / Identifiers page
//


var binSize = 1000;

// The point of ListPage is to render a potentially very long list
// incrementally, so we get a fast first render, and don't lock up the UI.
// We do that by rendering binSize rows at a time, on a requestAnimationFrame
// timeout.
class ListPage extends React.Component {
	state = {};

	componentWillMount() {
		var {state, path} = this.props,
			list = getIn(state, ['datapages', path, 'list']);
		var events = rxEvents(this, 'list');
		var chunks = events.list
			.startWith(list)
			.distinctUntilChanged()
			.filter(identity)
			.switchMap(ids => {
				var chunks = partitionN(ids, binSize).map(a => a.join('\n'));
				return from(range(chunks.length), animationFrame)
					.map(i => ({chunks: chunks.slice(0, i + 1), total: chunks.length}));
			});

		this.sub = chunks.subscribe(chunks => {
			this.setState(chunks);
		});
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	componentWillReceiveProps(props) {
		var {state, path} = props,
			list = getIn(state, ['datapages', path, 'list']);

		this.on.list(list);
	}

	render() {
		var {state, path, title} = this.props,
			{params: {host, dataset}, datapages} = state,
			{dataset: currentDataset, host: currentHost}
				= getIn(datapages, [path], {}),
			{chunks, total} = currentHost !== host || currentDataset !== dataset ?
				{} : this.state,
			percent = !chunks ? ' 0%' :
				chunks.length === total ? '' :
				` ${Math.floor(chunks.length / total * 100)}%`;

		return (
			<div className={styles.datapages}>
				<h3>dataset: {dataset}</h3>
				<h4>{title}{percent}</h4>
				{chunks ? chunks.map((c, i) => (
					<pre key={i} className={styles.list}>
						{c}
					</pre>
				)) : 'Loading...'}
			</div>);
	}
}

var IdentifiersPage = props =>
	<ListPage title='Identifiers' path='identifiers' {...props}/>;

var SamplesPage = props =>
	<ListPage title='Samples' path='samples' {...props}/>;

//
// Top-level dispatch to sub-pages
//

var getPage = ({dataset, host, cohort, allIdentifiers, allSamples}) =>
	allSamples ? SamplesPage :
	allIdentifiers ? IdentifiersPage :
	dataset && host ? DatasetPage :
	host ? HubPage :
	cohort ? CohortPage :
	CohortSummaryPage;

class Datapages extends React.Component {
	componentDidMount() {
		nav({activeLink: 'datapages', onNavigate: this.onNavigate});
	}

	onNavigate = (page, params) => {
		this.props.callback(['navigate', page, params]);
	};

	render() {
		var {state} = this.props, // XXX
			{params} = state,
			hubParams = getHubParams(state),
			Page = getPage(defaultHost(params));

		return <Page {...this.props} hubParams={hubParams} />;
	}
}

class ThemedDatapages extends React.Component {
	render() {
		return (
		<ThemeProvider theme={appTheme}>
			<Datapages {...this.props}/>
		</ThemeProvider>);
	}
}

var selector = state => state;

module.exports = props => <ThemedDatapages {...props} selector={selector}/>;
<commit_msg>Add i4j logo to download drawer.<commit_after>'use strict';

require('./base');
const React = require('react');
var {uniq, flatten, sortBy, groupBy, map, flatmap, partitionN, mapObject,
	pluck, concat, where, contains, get, updateIn, range, Let, pick,
	zip, identity, getIn, sum, keys, values, mmap} = require('./underscore_ext');
var {Observable: {from}, Scheduler: {animationFrame}} = require('./rx');
var {parseDsID, testStatus} = require('./xenaQuery');
import Link from 'react-toolbox/lib/link';
var styles = require('./Datapages.module.css');
var nav = require('./nav');
import {Checkbox} from 'react-toolbox/lib/checkbox';
import {Button} from 'react-toolbox/lib/button';
var showdown = require('showdown');
var {stripHTML} = require('./dom_helper');
var treehouseImg = require('../images/Treehouse.jpg');
var {rxEvents} = require('./react-utils');
var {servers: {localHub}, serverNames} = require('./defaultServers');
import Dialog from 'react-toolbox/lib/dialog';
var {encodeObject, urlParams} = require('./util');
import {ThemeProvider} from 'react-css-themr';
var appTheme = require('./appTheme');
var {getHubParams} = require('./hubParams');
var Rx = require('./rx');
var platform = require('platform');
import Drawer from 'react-toolbox/lib/drawer';
import PureComponent from './PureComponent';
var spinner = require('./ajax-loader.gif');

var getHubName = host => get(serverNames, host, host);

var pluralize = (str, count) => count === 1 ? `1 ${str}` : `${count} ${str}s`;

// Get params from the anchor href. With RT Link, the anchor is parentElement.
var paramFromHref = ev => mapObject(urlParams(ev.target.parentElement.href), a => a[0]);

//
// event handler for navigating within datapages. Prevents page load, and
// sets url params.
//

function navHandler(ev) {
	ev.preventDefault();
	this.props.callback(['navigate', 'datapages', paramFromHref(ev, this.props.state)]);
}

var getUserServers = servers => keys(servers).filter(k => servers[k].user);

//
// Data hubs sidebar
//

var hubLink = (host, onClick, hubParams) => (
	<Link
		className={styles.link}
		href={'?' + encodeObject({host, ...hubParams})}
		label={getHubName(host)}
		onClick={onClick}/>);

class DataHubs extends React.Component {
	onHub = (ev) => { navHandler.call(this, ev); };

	onSelect = (isOn, ev) => {
		var {checked} = ev.target,
			host = ev.target.getAttribute('data-host');
		this.props.callback([checked ? 'enable-host' : 'disable-host', host, 'user']);
	};

	render() {
		var {hubParams, state: {spreadsheet: {servers}}} = this.props;
		return (
			<div className={styles.dataHubs}>
				<h2>Active Data Hubs</h2>
				<ul>
					{map(servers,
						({user}, host) => (
							<li key={host}>
								<Checkbox
									label={hubLink(host, this.onHub, hubParams)}
									onChange={this.onSelect}
									checked={user}
									data-host={host}/>
							</li>))}
				</ul>
			</div>);
	}
}


//
// Cohort Summary Page
//

var treehouse = cohort =>
	cohort.search(/^Treehouse/gi) === -1 ? null :
	<img src={treehouseImg} height='40px'/>;

var cohortLink = (cohort, onClick, hubParams) => (
	<Link
		className={styles.link}
		href={'?' + encodeObject({cohort, ...hubParams})}
		label={cohort}
		onClick={onClick}/>);

var collateCohorts = hubCohorts =>
	concat(...pluck(hubCohorts, 'cohorts')).reduce(
		(acc, cohort) => updateIn(acc, [cohort.cohort],
			(v = 0) => cohort.count + v),
		{});

var CohortSummary = ({cohorts, onCohort, hubParams, action}) => {
	var names = sortBy(keys(cohorts), c => c.toLowerCase()),
		nCohorts = names.length,
		nDatasets = sum(values(cohorts));
	return (
		<div>
			<h2>{pluralize('Cohort', nCohorts)}, {pluralize('Dataset', nDatasets)} {action}</h2>
			<ul className={styles.list}>
				{map(names, name =>
					<li key={name}>
						{treehouse(name)}
						{cohortLink(name, onCohort, hubParams)}
						{` (${pluralize('dataset', cohorts[name])})`}
					</li>)}
			</ul>
		</div>);
};

class CohortSummaryPage extends React.Component {
	onCohort = (ev) => { navHandler.call(this, ev); };

	render() {
		var {hubParams, state} = this.props,
			{spreadsheet: {servers}} = state,
			userServers = getUserServers(servers),
			cohorts = getIn(this.props.state, ['datapages', 'cohorts'], []),
			activeCohorts = cohorts.filter(c => contains(userServers, c.server)),
			combined = collateCohorts(activeCohorts);
		return (
			<div className={styles.datapages}>
				<DataHubs {...this.props}/>
				<CohortSummary hubParams={hubParams} cohorts={combined} onCohort={this.onCohort}/>
			</div>);
	}
}

//
// Dataset delete button
//

class DeleteButton extends React.Component {
	state = {active: false};

	onDelete = () => {
		this.setState({active: !this.state.active});
	};

	onReally = () => {
		var {name} = this.props;
		this.props.callback(['navigate', 'datapages', {host: localHub}]);
		this.props.callback(['delete-dataset', localHub, name]);
	};

	actions = () => {
		return [
			{label: 'Cancel', onClick: this.onDelete},
			{label: 'Really Delete', onClick: this.onReally}];
	};

	render() {
		var {active} = this.state,
			{label} = this.props;
		return (
			<div className={styles.deleteButton}>
				<Dialog actions={this.actions()} active={active}
						onEscKeyDown={this.onDelete} onOverlayClick={this.onDelete}
						title='Delete dataset'>
					{label}
				</Dialog>
				<Button onClick={this.onDelete} accent>Delete</Button>
			</div>);
	}
}

var canDelete = ({status}, host) =>
	host === localHub && contains(['loaded', 'error'], status);

var markdownValue = value => {
	if (value) {
		var converter = new showdown.Converter();
		return (<div className={styles.header}
			dangerouslySetInnerHTML={{__html: converter.makeHtml(value)}}/>);
	}
};

//
// Cohort Datasets Page
//

var datasetLink = (callback, preferred, onClick, hubParams) => ds => {
	var [host] = parseDsID(ds.dsID);
	return (
		<li key={ds.name}>
			<Link
				className={styles.link}
				href={'?' + encodeObject({dataset: ds.name, host, ...hubParams})}
				label={ds.label}
				onClick={onClick}/>
			{preferred.has(ds.dsID) ? <span className={styles.star}>*</span> : null}
			{ds.status !== 'loaded' ?
				<span className={styles.count}> [{ds.status}]</span> : null}
			{ds.status === 'loaded' ?
				<span className={styles.count}> (n={(ds.count || 0).toLocaleString()})</span> :
				null}
			<span> {getHubName(host)}</span>
			{canDelete(ds, host) ?
				<DeleteButton callback={callback} name={ds.name} label={ds.label || ds.name}/> : null}
			<div className={styles.lineClamp}>
				<span className={styles.description}>{stripHTML(ds.description)}</span>
			</div>
		</li>);
};

var drawGroup = (callback, groups, preferred, onClick, hubParams) => dataSubType => {
	var list = sortBy(groups[dataSubType], g => g.label.toLowerCase());
	return (
		<div key={dataSubType}>
			<h3>{dataSubType}</h3>
			<ul className={styles.groupList}>
				{map(list, datasetLink(callback, preferred, onClick, hubParams))}
			</ul>
		</div>);
};

var COHORT_NULL = '(unassigned)';

var getPreferred = (wizard, cohort) =>
	new Set(values(getIn(wizard, ['cohortPreferred', cohort], {})));

class CohortPage extends React.Component {
	onViz = () => {
		var {datapages, spreadsheet: {cohort: currentCohort}} = this.props.state,
			cohort = getIn(datapages, ['cohort', 'cohort'], COHORT_NULL);

		if (cohort !== get(currentCohort, 'name')) {
			this.props.callback(['cohort', cohort]);
		}
		this.props.callback(['navigate', 'heatmap']);
	};

	onDataset = (ev) => { navHandler.call(this, ev); };

	render() {
		var {hubParams, state: {datapages, params, wizard}} = this.props,
			cohort = getIn(datapages, ['cohort', 'cohort']) === params.cohort ?
				datapages.cohort : {cohort: '...', datasets: []},
			{callback} = this.props,
			dsGroups = groupBy(values(cohort.datasets), 'dataSubType'),
			dataSubTypes = sortBy(keys(dsGroups), g => g.toLowerCase()),
			preferred = getPreferred(wizard, params.cohort);

		return (
			<div className={styles.datapages}>
				{markdownValue(cohort.meta)}
				<div className={styles.sidebar}>
					<Button onClick={this.onViz} accent>Visualize</Button>
				</div>
				<h2>cohort: {treehouse(cohort.cohort)}{cohort.cohort}</h2>
				{dataSubTypes.map(drawGroup(callback, dsGroups, preferred, this.onDataset, hubParams))}
				{preferred.size === 0 ? null : (
					<span>
						<span className={styles.star}>*</span>
						<span>default dataset in visualization</span>
					</span>)}
			</div>);
	}
}

//
// Dataset Page
//

var TYPE_NULL = 'genomicMatrix',
	FORMAT_MAPPING = {
		'clinicalMatrix': "ROWs (samples)  x  COLUMNs (identifiers) (i.e. clinicalMatrix)",
		'genomicMatrix': "ROWs (identifiers)  x  COLUMNs (samples) (i.e. genomicMatrix)",
		'mutationVector': "Variant by Position (i.e. mutationVector)",
		'genomicSegment': 'Genomic Segment (i.e. genomicSegment)',
		'unknown': "unknown"
	};

var dataPair = (key, value, method = identity) => value == null ? [] : [
	<span className={styles.key}>{key}</span>,
	<span className={styles.value}>{method(value)}</span>,
	<br/>];

var toLink = value => <a href={value}>{value}</a>;
var toCohortLink = (onClick, hubParams) => cohort => (
	<Link
		href={'?' + encodeObject({cohort, ...hubParams})}
		onClick={onClick}
		label={cohort}/>);
var toPMIDLink = value => (
	<a href={`http://www.ncbi.nlm.nih.gov/pubmed/?term=${value.toString()}`}>
		{value}
	</a>);

var jsonLink = link => link.replace(/(.gz)?$/, '.json');
var toDownloadLink = value => (
	<span>
		<a href={value}>{value}</a>
		{'; '}
		<a href={jsonLink(value)}>Full metadata</a>
	</span>);
var toHTML = value => <span dangerouslySetInnerHTML={{__html: value}}/>;

var headerValue = value => value && <p className={styles.header}>{value}</p>;
var htmlValue = value => value &&
	<p className={styles.header} dangerouslySetInnerHTML={{__html: value}}/>;

// XXX Does not display load warning in pop-up
// XXX Does not use red/blue coloring for error vs. other load status (e.g. 'loading')
var getStatus = (status, loaderWarning) =>
	status === 'loaded' && !loaderWarning ? [] :
	status === 'loaded' ? dataPair('status', 'loaded with warning') :
	dataPair('status', status);

// split, handling falsey values
var split = (str, pat) => str ? str.split(pat) : [];

var resolveCodes = (probes, codes, data) =>
	mmap(probes, data, (probe, row) =>
			codes[probe] ? map(row, v => isNaN(v) ? v : codes[probe][v]) :
			row);

// not the most efficient algorithm :-/
var addHeaders = (fields, samples, data) =>
	mmap(['', ...fields], [samples, ...data], (header, row) => [header, ...row]);

var transpose = data => zip.apply(null, data);

// Transpose matrix if it's clinical
var transposeClinical = (meta, data) =>
	get(meta, 'type') === 'clinicalMatrix' ? transpose(data) :
	data;

var table = data => (
		<table className={styles.dataSnippetTable}>
			<tbody>
				{map(data, (row, i) => <tr key={i}>{map(row, (c, j) => <td key={j}>{c}</td>)}</tr>)}
			</tbody>
		</table>);

var matrixTable = (meta, {fields, samples, codes, data}) =>
	table(transposeClinical(meta, addHeaders(fields, samples, resolveCodes(fields, codes, data))));

// swap chromend & chromstart when sorting fields
var cmpSparseFields = (a, b) =>
	a === 'chromend' && b === 'chromstart' ? 1 :
	a === 'chromstart' && b === 'chromend' ? -1 :
	a.localeCompare(b);

var sparseTable = (meta, data) => {
	var fields = keys(data).sort(cmpSparseFields),
		rows = fields.map(field => data[field]),
		samples = data.sampleID;
	return table(transpose(addHeaders(fields, samples, rows)));
};

var noTable = () => null;

var dataMethod = ({type = 'genomicMatrix', status} = {}) =>
	status !== 'loaded' ? noTable :
	type === 'genomicMatrix' ? matrixTable :
	type === 'clinicalMatrix' ? matrixTable :
	type === 'mutationVector' ? sparseTable :
	type === 'genomicSegment' ? sparseTable :
	noTable;

var setKey = arr => arr.map((el, i) => React.cloneElement(el, {key: i}));

class DatasetPage extends React.Component {
	onCohort = (ev) => { navHandler.call(this, ev); };

	onViz = () => {
		var {datapages, spreadsheet: {cohort: currentCohort}} = this.props.state,
			cohort = getIn(datapages, ['dataset', 'meta', 'cohort'], COHORT_NULL);

		if (cohort !== get(currentCohort, 'name')) {
			this.props.callback(['cohort', cohort]);
		}
		this.props.callback(['navigate', 'heatmap']);
	};

	onIdentifiers = (ev) => { navHandler.call(this, ev); };
	onSamples = (ev) => { navHandler.call(this, ev); };

	render() {
		var {callback, state, hubParams} = this.props,
			{params: {host, dataset}, datapages} = state,
			{meta, probeCount = 0, data, downloadLink, probemapLink, dataset: currentDataset,
				host: currentHost} = get(datapages, 'dataset', {});

		if (!meta || currentHost !== host || currentDataset !== dataset) {
			return (
				<div className={styles.datapages}>
					<h2>dataset: ...</h2>
				</div>);
		}
		var {name, label = name, description, longTitle,
			cohort = COHORT_NULL, dataSubType, platform, unit,
			assembly, version, url, articletitle, citation, pmid,
			dataproducer, author = dataproducer,
			'wrangling_procedure': wranglingProcedure,
			type = TYPE_NULL, status, loader, count} = meta;

		return (
			<div className={styles.datapages}>
				<div className={styles.sidebar}>
					<Button onClick={this.onViz} accent>Visualize</Button>
					{canDelete(meta, host) ?
						<DeleteButton callback={callback} name={name} label={label}/> : null}
				</div>
				<h2>dataset: {(dataSubType ? dataSubType + ' - ' : '') + label}</h2>
				{headerValue(longTitle)}
				{htmlValue(description)}
				{setKey(flatten([
					dataPair('cohort', cohort, toCohortLink(this.onCohort, hubParams)),
					dataPair('dataset ID', name),
					getStatus(status, loader),
					dataPair('download', downloadLink, toDownloadLink),
					dataPair('samples', count),
					dataPair('version', version),
					dataPair('hub', host),
					dataPair('type of data', dataSubType),
					dataPair('assembly', assembly),
					dataPair('unit', unit),
					dataPair('platform', platform),
					dataPair('ID/Gene Mapping', probemapLink, toDownloadLink),
					dataPair('publication', articletitle),
					dataPair('citation', citation),
					dataPair('author', author),
					dataPair('PMID', pmid, toPMIDLink),
					flatmap(uniq(split(url, /,/)), url =>
						dataPair('raw data', url, toLink)),
					dataPair('wrangling', wranglingProcedure, toHTML),
					dataPair('input data format', FORMAT_MAPPING[type])]))}
				{status === 'loaded' ?
					<span className={styles.tableControls}>
						{type === 'genomicMatrix' ?
							`${probeCount.toLocaleString()} identifiers X ${count} samples` : null}
						{type === 'clinicalMatrix' ?
							`${count} samples X ${probeCount.toLocaleString()} identifiers` : null}
						<Link
							href={'?' + encodeObject({host, dataset, allIdentifiers: true, ...hubParams})}
							onClick={this.onIdentifiers} label='All Identifiers'/>
						<Link
							href={'?' + encodeObject({host, dataset, allSamples: true, ...hubParams})}
							onClick={this.onSamples} label='All Samples'/>
					</span> : null}
				{dataMethod(meta)(meta, data)}
			</div>);
	}
}

// Our handling of parameters 'hub' and 'host', is somewhat confusing. 'host'
// means "show the hub page for this url". 'hub' means "add this url to the
// active hub list, and, if in /datapages/ show the hub page for this url".
// The 'hub' parameter can be repeated, which adds each hub to the active hub
// list. Only the first one will be displayed when linking to /datapages/.
// Needs refactor.
var defaultHost = params =>
	Let(({host, hubs} = params) =>
			!host && hubs ? {...params, host: hubs[0]} : params);

var osFiles = {
		osxJre: {
			pattern: "ucsc_xena_macos_[_0-9]*_with_jre.dmg",
			description: "OSX installer, bundled JRE",
			help: "Recommended for OSX 10.7 and above"
		},
		osxNoJre: {
			pattern: "ucsc_xena_macos_[_0-9]*.dmg",
			description: "OSX installer, no JRE",
			help: "Recommended for OSX 10.6"
		},
		win32: {
			pattern: "ucsc_xena_windows_[_0-9]*.exe",
			description: "Windows 32 bit installer, bundled JRE",
			help: "Recommended for all 32 bit versions of Windows"
		},
		win64: {
			pattern: "ucsc_xena_windows-x64_[_0-9]*.exe",
			description: "Windows 64 bit installer, bundled JRE",
			help: "Recommended for all 64 bit versions of Windows"
		},
		tar: {
			pattern: "ucsc_xena_[_0-9]*.tar.gz",
			description: "Tar archive, no updater or JRE",
			help: "Recommended for linux server deployments"
		}
	}, defaults = {
		'OS X': {32: 'osxNoJre', 64: 'osxJre'},
		'Windows': {32: 'win32', 64: 'win64'},
		'Linux': {32: 'tar', 64: 'tar'}
	};

function findMatch(pattern, list) {
	var matches = list.filter(s => s.match(pattern));
	return matches.length > 0 ? matches[0] : undefined;
}

var matchPaths = serverFiles =>
	mapObject(osFiles, ({pattern}, key) =>
			  findMatch(osFiles[key].pattern, serverFiles));

var parseInt10 = s => parseInt(s, 10);

function osxArch() {
	var version = getIn(platform, ['os', 'version']),
		parts = version && version.match(/(\d*)\.(\d*)/).slice(1).map(parseInt10); // [maj, min, patch]
	return (parts && parts[0] === 10 && parts[1] <= 6) ? 32 : 64;
}

function getOs() {
	var family = getIn(platform, ['os', 'family']);
	if (family) {
		if (family.indexOf('Windows') !== -1) {
			return 'Windows';
		}
		if (family.indexOf('OS X') !== -1) {
			return 'OS X';
		}
		if (family.indexOf('Linux') !== -1) {
			return 'Linux';
		}
	}
}

var downloadDir = 'https://genome-cancer.ucsc.edu/download/public/get-xena';
var i4jLogo = `${downloadDir}/install4j_small.png`;
var updatesPath = `${downloadDir}/updates.xml`;
var downloadPath = name => `${downloadDir}/${name}`;
var getFileName = e => downloadPath(e.getAttribute('fileName'));

class XenaDownload extends React.Component {
	state = {};

	componentDidMount() {
		this.sub = Rx.Observable.ajax({method: 'GET', responseType: 'xml', url: updatesPath, crossDomain: true}).subscribe(xml => {
			var files = matchPaths([...xml.response.getElementsByTagName('entry')]
								   .map(getFileName));

			this.setState({files}); //eslint-disable-line react/no-did-mount-set-state
		});
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	render() {
		var {files} = this.state,
			os = getOs(),
			arch = os === 'OS X' ? osxArch() : getIn(platform, ['os', 'architecture']),
			defaultTarget = getIn(defaults, [os, arch]),
			defaultInstall = get(files, defaultTarget);

		return (
			<div className={styles.download}>
				{defaultInstall ? <Link className={styles.downloadLink} href={defaultInstall} label="Install UCSC Xena hub"/> : null}
				{defaultInstall ? <br/> : null}
				{defaultInstall ? 'Other installers' : 'Please download an installer'}
				{files ? map(pick(osFiles, (_, k) => files[k]), (info, key) =>
					 <Link className={styles.downloadLink} href={files[key]} title={info.help} label={info.description}/>) : null}
				<br/>
				<Link className={styles.helpLink} href='http://xena.ucsc.edu/private-hubs/'>Help</Link>
				<br/>
				<img src={i4jLogo}/>
			</div>);
	}
}

//
// Hub page
//

var refresh = 2000;
var localTimeout = 500; // use a short timeout for localhost

class HubPage extends PureComponent {
	state = {status: undefined, launched: undefined, showDownloads: false};

	onCohort = (ev) => {navHandler.call(this, ev);};

	launch = () => {
		var i = document.getElementById('xenaLauncher');
		if (i) {
			document.body.removeChild(i);
		}
		i = document.createElement('iframe');
		i.id = 'xenaLauncher';
		document.body.appendChild(i);
		i.src = 'ucscxena://';
		this.setState({launched: true});
	}

	onRaise = () => {
		Rx.Observable.ajax({method: 'GET', url: `${localHub}/raise/`,
				crossDomain: true}).subscribe(() => {});
	}

	componentDidMount() {
		var {state} = this.props,
			{host} = defaultHost(state.params),
			isLocal = host === localHub;

		if (isLocal) {
			this.sub = Rx.Observable.of(true).merge(Rx.Observable.interval(refresh))
				.flatMap(() => testStatus(host, localTimeout))
				.takeWhile(({status}) => status !== 'up')
				.concat(Rx.Observable.of({status: 'up'}))
				.subscribe(this.updatePing);

			// Try to launch if we don't get a response.
			this.timeoutID = setTimeout(() => {
				if (this.state.status !== 'up' && !this.state.launched) {
					this.launch();
				}
			}, 2000);
		}
	}

	componentWillUnmount() {
		if (this.timeoutID) {
			clearTimeout(this.timeoutID);
		}
		if (this.sub) {
			this.sub.unsubscribe();
		}
	}

	updatePing = resp => {
		if (resp.status === 'up' && this.state.status !== 'up') {
			this.props.callback(['cohort-summary-clear', {server: localHub}]);
		}
		this.setState(resp);
	}

	onShowDownloads = () => {
		this.setState({showDownloads: true});
	}

	onHideDownloads = () => {
		this.setState({showDownloads: false});
	}

	render() {
		var {status, showDownloads, launched} = this.state,
			{state, hubParams} = this.props,
			{spreadsheet: {servers}} = state,
			userServers = getUserServers(servers),
			{host} = defaultHost(state.params),
			isLocal = host === localHub,
			cohorts = getIn(state, ['datapages', 'cohorts'], []),
			hubCohorts = where(cohorts, {server: host}),
			coll = collateCohorts(hubCohorts),
			inHubs = contains(userServers, host) ?
				'' : ' (not in my data hubs)';

		return (
			<div className={styles.datapages}>
				<i className={`material-icons ${styles.open}`} onClick={this.onShowDownloads} title={'Install'}>chevron_left</i>
				<Drawer onOverlayClick={this.onHideDownloads} className={styles.drawer} active={showDownloads} type='right'>
					<i className={`material-icons ${styles.close}`} onClick={this.onHideDownloads} title={'Install'}>chevron_right</i>
					<XenaDownload/>
				</Drawer>
				<div className={isLocal && status !== 'up' ? styles.noticeVisible : styles.noticeHidden}>

					<div className={styles.localHubNotice}>
						<p>{status === 'starting' ? 'Local hub is starting.' :
							launched ? 'Trying to start local hub.' :
							'Trying to contact local hub.'}
							<img className={styles.spinner} src={spinner}/></p>
							{status !== 'starting' && launched ? (
								<div>
									<p>If you have previously installed a local
									hub, it should start momentarly. If it does not, please start it manually.</p>
									<br/>
									<p>If you would like to install a hub, open the links at right.</p>
								</div>) : null}
					</div>
				</div>

				{markdownValue(getIn(hubCohorts, [0, 'meta']))}
				<h2>{getHubName(host)}{inHubs}</h2>
				<p>Host address: {host}</p>
				<CohortSummary
					hubParams={hubParams}
					cohorts={coll}
					onCohort={this.onCohort}
					action={isLocal && status === 'up' ?
						<Button onClick={this.onRaise} accent>Import</Button> : null}/>
			</div>);
	}
}

//
// Samples / Identifiers page
//


var binSize = 1000;

// The point of ListPage is to render a potentially very long list
// incrementally, so we get a fast first render, and don't lock up the UI.
// We do that by rendering binSize rows at a time, on a requestAnimationFrame
// timeout.
class ListPage extends React.Component {
	state = {};

	componentWillMount() {
		var {state, path} = this.props,
			list = getIn(state, ['datapages', path, 'list']);
		var events = rxEvents(this, 'list');
		var chunks = events.list
			.startWith(list)
			.distinctUntilChanged()
			.filter(identity)
			.switchMap(ids => {
				var chunks = partitionN(ids, binSize).map(a => a.join('\n'));
				return from(range(chunks.length), animationFrame)
					.map(i => ({chunks: chunks.slice(0, i + 1), total: chunks.length}));
			});

		this.sub = chunks.subscribe(chunks => {
			this.setState(chunks);
		});
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	componentWillReceiveProps(props) {
		var {state, path} = props,
			list = getIn(state, ['datapages', path, 'list']);

		this.on.list(list);
	}

	render() {
		var {state, path, title} = this.props,
			{params: {host, dataset}, datapages} = state,
			{dataset: currentDataset, host: currentHost}
				= getIn(datapages, [path], {}),
			{chunks, total} = currentHost !== host || currentDataset !== dataset ?
				{} : this.state,
			percent = !chunks ? ' 0%' :
				chunks.length === total ? '' :
				` ${Math.floor(chunks.length / total * 100)}%`;

		return (
			<div className={styles.datapages}>
				<h3>dataset: {dataset}</h3>
				<h4>{title}{percent}</h4>
				{chunks ? chunks.map((c, i) => (
					<pre key={i} className={styles.list}>
						{c}
					</pre>
				)) : 'Loading...'}
			</div>);
	}
}

var IdentifiersPage = props =>
	<ListPage title='Identifiers' path='identifiers' {...props}/>;

var SamplesPage = props =>
	<ListPage title='Samples' path='samples' {...props}/>;

//
// Top-level dispatch to sub-pages
//

var getPage = ({dataset, host, cohort, allIdentifiers, allSamples}) =>
	allSamples ? SamplesPage :
	allIdentifiers ? IdentifiersPage :
	dataset && host ? DatasetPage :
	host ? HubPage :
	cohort ? CohortPage :
	CohortSummaryPage;

class Datapages extends React.Component {
	componentDidMount() {
		nav({activeLink: 'datapages', onNavigate: this.onNavigate});
	}

	onNavigate = (page, params) => {
		this.props.callback(['navigate', page, params]);
	};

	render() {
		var {state} = this.props, // XXX
			{params} = state,
			hubParams = getHubParams(state),
			Page = getPage(defaultHost(params));

		return <Page {...this.props} hubParams={hubParams} />;
	}
}

class ThemedDatapages extends React.Component {
	render() {
		return (
		<ThemeProvider theme={appTheme}>
			<Datapages {...this.props}/>
		</ThemeProvider>);
	}
}

var selector = state => state;

module.exports = props => <ThemedDatapages {...props} selector={selector}/>;

{'fraction_non_alphanumeric': 0.18697800258793082, 'fraction_numerical': 0.004783750931262988, 'mean_word_length': 3.9473326867119303, 'pattern_counts': {'":': 0, '<': 337, '<?xml version=': 0, '>': 539, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 58, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1596615', 'n_tokens_mistral': 19869, 'n_tokens_neox': 17333, 'n_words': 3983}