Merge pull request #635 from influxdata/feature/tr-search-by-tag
Add ability to filter by tags on hostspull/637/head
commit
8423e29575
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue