blob: fb0491258070f6a69b95b9f40db3807eb71e9f33 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import TreeView from 'react-treeview';
import { Link } from 'react-router';
import 'react-treeview/react-treeview.css';
import ClassNames from 'classnames';
import TableStore from '../stores/TableStore';
import AdhocQueryActions from '../actions/AdhocQueryActions';
import UserStore from '../stores/UserStore';
import Loader from './LoaderComponent';
import '../styles/css/tree.css';
let filterString = '';
function getState (page, filterString, database) {
let state = getTables(page, filterString, database);
state.page = page;
state.loading = false;
return state;
}
function getTables (page, filterString, database) {
// get all the native tables
// so that Object.keys does not throw up
let tables = TableStore.getTables(database) || {};
let pageSize = 10;
let allTables;
let startIndex;
let relevantIndexes;
let pageTables;
if (!filterString) {
// no need for filtering
allTables = Object.keys(tables);
} else {
// filter
allTables = Object.keys(tables).map(name => {
if (name.match(filterString)) return name;
}).filter(name => { return !!name; });
}
startIndex = (page - 1) * pageSize;
relevantIndexes = allTables.slice(startIndex, startIndex + pageSize);
pageTables = relevantIndexes.map(name => {
return tables[name];
});
return {
totalPages: Math.ceil(allTables.length / pageSize),
tables: pageTables,
database: database,
};
}
class TableTree extends React.Component {
constructor (props) {
super(props);
this.state = {
tables: [],
totalPages: 0,
page: 0,
loading: true,
isCollapsed: false,
};
this._onChange = this._onChange.bind(this);
this.prevPage = this.prevPage.bind(this);
this.nextPage = this.nextPage.bind(this);
this.toggle = this.toggle.bind(this);
this.validateClickEvent = this.validateClickEvent.bind(this);
if (!TableStore.getTables(UserStore.currentDatabase())) {
AdhocQueryActions
.getTables(UserStore.getUserDetails().secretToken, UserStore.currentDatabase());
} else {
let state = getState(1, '', UserStore.currentDatabase());
this.state = state;
// on page refresh only a single table is fetched, and hence we need to
// fetch others too.
if (!TableStore.areTablesCompletelyFetched(UserStore.currentDatabase())) {
AdhocQueryActions
.getTables(UserStore.getUserDetails().secretToken, UserStore.currentDatabase());
}
}
}
componentDidMount () {
TableStore.addChangeListener(this._onChange);
// listen for opening tree
this.refs.tableTree.getDOMNode()
.addEventListener('click', this.validateClickEvent);
}
componentWillUnmount () {
this.refs.tableTree.getDOMNode()
.removeEventListener('click', this.validateClickEvent);
TableStore.removeChangeListener(this._onChange);
}
render() {
// construct tree
let tableTreeInternal = this.state.tables.map(table => {
let label = (<Link to='tableschema' params={{databaseName: this.state.database, tableName: table.name}}
title={table.name} query={{database: UserStore.currentDatabase()}}>
{table.name}</Link>);
return (
<TreeView key={table.name} nodeLabel={label}
defaultCollapsed={!table.isLoaded}>
{table.isLoaded ? table.columns.map(col => {
return (
<div className='treeNode' key={table.name + '|' + col.name}>
{col.name} ({col._type})
</div>
);
}) : <Loader size='4px' margin='2px' />}
</TreeView>
);
});
// show a loader when tree is loading
if (this.state.loading) {
tableTreeInternal = <Loader size='4px' margin='2px'/>;
} else if (!this.state.tables.length) {
tableTreeInternal = (<div className='alert-danger' style={{padding: '8px 5px'}}>
<strong>Sorry, we couldn&#39;t find any.</strong>
</div>);
}
let pagination = this.state.tables.length ?
(
<div>
<div className='text-center'>
<button className='btn btn-link glyphicon glyphicon-triangle-left page-back'
onClick={this.prevPage}>
</button>
<span>{this.state.page} of {this.state.totalPages}</span>
<button className='btn btn-link glyphicon glyphicon-triangle-right page-next'
onClick={this.nextPage}>
</button>
</div>
</div>
) : null;
let tableTree = (
<div>
{ !this.state.loading &&
<div className='form-group'>
<input type='search' className='form-control'
placeholder='Type to filter tables'
onChange={this._filter.bind(this)}/>
</div>
}
{pagination}
<div ref='tableTree' style={{maxHeight: '350px', overflowY: 'auto'}}>
{tableTreeInternal}
</div>
</div>
);
let collapseClass = ClassNames({
'pull-right': true,
'glyphicon': true,
'glyphicon-chevron-up': !this.state.isCollapsed,
'glyphicon-chevron-down': this.state.isCollapsed
});
let panelBodyClassName = ClassNames({
'panel-body': true,
'hide': this.state.isCollapsed
});
return (
<div className='panel panel-default'>
<div className='panel-heading'>
<h3 className='panel-title'>
Tables
<span className={collapseClass} onClick={this.toggle}></span>
</h3>
</div>
<div className={panelBodyClassName} style={{maxHeight: '350px', overflowY: 'auto'}}>
{tableTree}
</div>
</div>
);
}
_onChange (page) {
// so that page doesn't reset to beginning
page = page || this.state.page || 1;
this.setState(getState(page, filterString, UserStore.currentDatabase()));
}
getDetails (tableName, database) {
// find the table
let table = this.state.tables.filter(table => {
return tableName === table.name;
});
if (table.length && table[0].isLoaded) return;
AdhocQueryActions
.getTableDetails(UserStore.getUserDetails().secretToken, tableName,
database);
}
_filter (event) {
filterString = event.target.value;
this._onChange();
}
prevPage () {
if (this.state.page - 1) this._onChange(this.state.page - 1);
}
nextPage () {
if (this.state.page < this.state.totalPages) {
this._onChange(this.state.page + 1);
}
}
toggle () {
this.setState({ isCollapsed: !this.state.isCollapsed });
}
validateClickEvent (e) {
if (e.target && e.target.nodeName === 'DIV' &&
e.target.nextElementSibling.nodeName === 'A') {
this.getDetails(e.target.nextElementSibling.textContent, UserStore.currentDatabase());
}
}
}
TableTree.propTypes = {
database: React.PropTypes.string.isRequired
};
export default TableTree;