Merge pull request #635 from influxdata/feature/tr-search-by-tag

Add ability to filter by tags on hosts
pull/637/head
Andrew Watkins 2016-12-02 12:37:37 -08:00 committed by GitHub
commit 8423e29575
3 changed files with 149 additions and 60 deletions

View File

@ -1,5 +1,6 @@
## v1.1.0 [unreleased]
- #635: Add the ability to filter hosts by any tag associated with that host.
- #573: Provide more control over layout rendering when needed with
"autolayout" flag on templates.
- #586: Allow telegraf database in non-default locations

View File

@ -62,12 +62,9 @@ export function getAppsForHosts(proxyLink, hosts, appMappings, telegrafDB) {
const newHosts = Object.assign({}, hosts);
const allSeries = _.get(resp, ['data', 'results', '0', 'series', '0', 'values'], []);
allSeries.forEach(([series]) => {
const matches = series.match(/(\w*).*,host=([^,]*)/);
if (!matches || matches.length !== 3) { // eslint-disable-line no-magic-numbers
return;
}
const measurement = matches[1];
const host = matches[2];
const seriesObj = parseSeries(series);
const measurement = seriesObj.measurement;
const host = seriesObj.tags.host;
if (!newHosts[host]) {
return;
@ -75,7 +72,11 @@ export function getAppsForHosts(proxyLink, hosts, appMappings, telegrafDB) {
if (!newHosts[host].apps) {
newHosts[host].apps = [];
}
if (!newHosts[host].tags) {
newHosts[host].tags = {};
}
newHosts[host].apps = _.uniq(newHosts[host].apps.concat(measurementsToApps[measurement]));
_.assign(newHosts[host].tags, seriesObj.tags);
});
return newHosts;
@ -99,6 +100,49 @@ export function getMeasurementsForHost(source, host) {
});
}
function parseSeries(series) {
const ident = /\w+/;
const tag = /,?([^=]+)=([^,]+)/;
function parseMeasurement(s, obj) {
const match = ident.exec(s);
const measurement = match[0];
if (measurement) {
obj.measurement = measurement;
}
return s.slice(match.index + measurement.length);
}
function parseTag(s, obj) {
const match = tag.exec(s);
const kv = match[0];
const key = match[1];
const value = match[2];
if (key) {
if (!obj.tags) {
obj.tags = {};
}
obj.tags[key] = value;
}
return s.slice(match.index + kv.length);
}
let workStr = series.slice();
const out = {};
// Consume measurement
workStr = parseMeasurement(workStr, out);
// Consume tags
while (workStr.length > 0) {
workStr = parseTag(workStr, out);
}
return out;
}
function _isEmpty(resp) {
return !resp.results[0].series;
}

View File

@ -19,40 +19,29 @@ const HostsTable = React.createClass({
getInitialState() {
return {
searchTerm: '',
filteredHosts: this.props.hosts,
sortDirection: null,
sortKey: null,
};
},
componentWillReceiveProps(newProps) {
this.filterHosts(newProps.hosts, this.state.searchTerm);
},
filterHosts(allHosts, searchTerm) {
const hosts = allHosts.filter((h) => {
let apps = null;
if (h.apps) {
apps = h.apps.join(', ');
filter(allHosts, searchTerm) {
return allHosts.filter((h) => {
const apps = h.apps ? h.apps.join(', ') : '';
// search each tag for the presence of the search term
let tagResult = false;
if (h.tags) {
tagResult = Object.keys(h.tags).reduce((acc, key) => {
return acc || h.tags[key].search(searchTerm) !== -1;
}, false);
} else {
apps = '';
tagResult = false;
}
return (
h.name.search(searchTerm) !== -1 ||
apps.search(searchTerm) !== -1
apps.search(searchTerm) !== -1 ||
tagResult
);
});
this.setState({searchTerm, filteredHosts: hosts});
},
changeSort(key) {
// if we're using the key, reverse order; otherwise, set it with ascending
if (this.state.sortKey === key) {
const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc');
this.setState({sortDirection: reverseDirection});
} else {
this.setState({sortKey: key, sortDirection: 'asc'});
}
},
sort(hosts, key, direction) {
@ -66,6 +55,20 @@ const HostsTable = React.createClass({
}
},
updateSearchTerm(term) {
this.setState({searchTerm: term});
},
updateSort(key) {
// if we're using the key, reverse order; otherwise, set it with ascending
if (this.state.sortKey === key) {
const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc');
this.setState({sortDirection: reverseDirection});
} else {
this.setState({sortKey: key, sortDirection: 'asc'});
}
},
sortableClasses(key) {
if (this.state.sortKey === key) {
if (this.state.sortDirection === 'asc') {
@ -77,9 +80,10 @@ const HostsTable = React.createClass({
},
render() {
const hosts = this.sort(this.state.filteredHosts, this.state.sortKey, this.state.sortDirection);
const hostCount = hosts.length;
const {source} = this.props;
const {searchTerm, sortKey, sortDirection} = this.state;
const {hosts, source} = this.props;
const sortedHosts = this.sort(this.filter(hosts, searchTerm), sortKey, sortDirection);
const hostCount = sortedHosts.length;
let hostsTitle;
if (hostCount === 1) {
@ -94,44 +98,23 @@ const HostsTable = React.createClass({
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">{hostsTitle}</h2>
<SearchBar onSearch={_.wrap(this.props.hosts, this.filterHosts)} />
<SearchBar onSearch={this.updateSearchTerm} />
</div>
<div className="panel-body">
<table className="table v-center">
<thead>
<tr>
<th onClick={() => this.changeSort('name')} className={this.sortableClasses('name')}>Hostname</th>
<th onClick={() => this.updateSort('name')} className={this.sortableClasses('name')}>Hostname</th>
<th className="text-center">Status</th>
<th onClick={() => this.changeSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
<th onClick={() => this.changeSort('load')} className={this.sortableClasses('load')}>Load</th>
<th onClick={() => this.updateSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
<th onClick={() => this.updateSort('load')} className={this.sortableClasses('load')}>Load</th>
<th>Apps</th>
</tr>
</thead>
<tbody>
{
hosts.map(({name, cpu, load, apps = []}) => {
return (
<tr key={name}>
<td className="monotype"><Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link></td>
<td className="text-center"><div className="table-dot dot-success"></div></td>
<td className="monotype">{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}</td>
<td className="monotype">{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}</td>
<td className="monotype">
{apps.map((app, index) => {
return (
<span key={app}>
<Link
style={{marginLeft: "2px"}}
to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
{app}
</Link>
{index === apps.length - 1 ? null : ', '}
</span>
);
})}
</td>
</tr>
);
sortedHosts.map((h) => {
return <HostRow key={h.name} host={h} source={source} />;
})
}
</tbody>
@ -142,13 +125,74 @@ const HostsTable = React.createClass({
},
});
const HostRow = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
host: PropTypes.shape({
name: PropTypes.string,
cpu: PropTypes.number,
load: PropTypes.number,
apps: PropTypes.arrayOf(PropTypes.string.isRequired),
}),
},
shouldComponentUpdate(nextProps) {
return this.props.host !== nextProps.host;
},
render() {
const {host, source} = this.props;
const {name, cpu, load, apps = []} = host;
return (
<tr>
<td className="monotype"><Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link></td>
<td className="text-center"><div className="table-dot dot-success"></div></td>
<td className="monotype">{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}</td>
<td className="monotype">{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}</td>
<td className="monotype">
{apps.map((app, index) => {
return (
<span key={app}>
<Link
style={{marginLeft: "2px"}}
to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
{app}
</Link>
{index === apps.length - 1 ? null : ', '}
</span>
);
})}
</td>
</tr>
);
},
});
const SearchBar = React.createClass({
propTypes: {
onSearch: PropTypes.func.isRequired,
},
getInitialState() {
return {
searchTerm: '',
};
},
componentWillMount() {
const waitPeriod = 300;
this.handleSearch = _.debounce(this.handleSearch, waitPeriod);
},
handleSearch() {
this.props.onSearch(this.state.searchTerm);
},
handleChange() {
this.props.onSearch(this.refs.searchInput.value);
this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch);
},
render() {