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]
|
## 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
|
- #573: Provide more control over layout rendering when needed with
|
||||||
"autolayout" flag on templates.
|
"autolayout" flag on templates.
|
||||||
- #586: Allow telegraf database in non-default locations
|
- #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 newHosts = Object.assign({}, hosts);
|
||||||
const allSeries = _.get(resp, ['data', 'results', '0', 'series', '0', 'values'], []);
|
const allSeries = _.get(resp, ['data', 'results', '0', 'series', '0', 'values'], []);
|
||||||
allSeries.forEach(([series]) => {
|
allSeries.forEach(([series]) => {
|
||||||
const matches = series.match(/(\w*).*,host=([^,]*)/);
|
const seriesObj = parseSeries(series);
|
||||||
if (!matches || matches.length !== 3) { // eslint-disable-line no-magic-numbers
|
const measurement = seriesObj.measurement;
|
||||||
return;
|
const host = seriesObj.tags.host;
|
||||||
}
|
|
||||||
const measurement = matches[1];
|
|
||||||
const host = matches[2];
|
|
||||||
|
|
||||||
if (!newHosts[host]) {
|
if (!newHosts[host]) {
|
||||||
return;
|
return;
|
||||||
|
@ -75,7 +72,11 @@ export function getAppsForHosts(proxyLink, hosts, appMappings, telegrafDB) {
|
||||||
if (!newHosts[host].apps) {
|
if (!newHosts[host].apps) {
|
||||||
newHosts[host].apps = [];
|
newHosts[host].apps = [];
|
||||||
}
|
}
|
||||||
|
if (!newHosts[host].tags) {
|
||||||
|
newHosts[host].tags = {};
|
||||||
|
}
|
||||||
newHosts[host].apps = _.uniq(newHosts[host].apps.concat(measurementsToApps[measurement]));
|
newHosts[host].apps = _.uniq(newHosts[host].apps.concat(measurementsToApps[measurement]));
|
||||||
|
_.assign(newHosts[host].tags, seriesObj.tags);
|
||||||
});
|
});
|
||||||
|
|
||||||
return newHosts;
|
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) {
|
function _isEmpty(resp) {
|
||||||
return !resp.results[0].series;
|
return !resp.results[0].series;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,40 +19,29 @@ const HostsTable = React.createClass({
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
filteredHosts: this.props.hosts,
|
|
||||||
sortDirection: null,
|
sortDirection: null,
|
||||||
sortKey: null,
|
sortKey: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
filter(allHosts, searchTerm) {
|
||||||
this.filterHosts(newProps.hosts, this.state.searchTerm);
|
return allHosts.filter((h) => {
|
||||||
},
|
const apps = h.apps ? h.apps.join(', ') : '';
|
||||||
|
// search each tag for the presence of the search term
|
||||||
filterHosts(allHosts, searchTerm) {
|
let tagResult = false;
|
||||||
const hosts = allHosts.filter((h) => {
|
if (h.tags) {
|
||||||
let apps = null;
|
tagResult = Object.keys(h.tags).reduce((acc, key) => {
|
||||||
if (h.apps) {
|
return acc || h.tags[key].search(searchTerm) !== -1;
|
||||||
apps = h.apps.join(', ');
|
}, false);
|
||||||
} else {
|
} else {
|
||||||
apps = '';
|
tagResult = false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
h.name.search(searchTerm) !== -1 ||
|
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) {
|
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) {
|
sortableClasses(key) {
|
||||||
if (this.state.sortKey === key) {
|
if (this.state.sortKey === key) {
|
||||||
if (this.state.sortDirection === 'asc') {
|
if (this.state.sortDirection === 'asc') {
|
||||||
|
@ -77,9 +80,10 @@ const HostsTable = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hosts = this.sort(this.state.filteredHosts, this.state.sortKey, this.state.sortDirection);
|
const {searchTerm, sortKey, sortDirection} = this.state;
|
||||||
const hostCount = hosts.length;
|
const {hosts, source} = this.props;
|
||||||
const {source} = this.props;
|
const sortedHosts = this.sort(this.filter(hosts, searchTerm), sortKey, sortDirection);
|
||||||
|
const hostCount = sortedHosts.length;
|
||||||
|
|
||||||
let hostsTitle;
|
let hostsTitle;
|
||||||
if (hostCount === 1) {
|
if (hostCount === 1) {
|
||||||
|
@ -94,44 +98,23 @@ const HostsTable = React.createClass({
|
||||||
<div className="panel panel-minimal">
|
<div className="panel panel-minimal">
|
||||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||||
<h2 className="panel-title">{hostsTitle}</h2>
|
<h2 className="panel-title">{hostsTitle}</h2>
|
||||||
<SearchBar onSearch={_.wrap(this.props.hosts, this.filterHosts)} />
|
<SearchBar onSearch={this.updateSearchTerm} />
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
<table className="table v-center">
|
<table className="table v-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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 className="text-center">Status</th>
|
||||||
<th onClick={() => this.changeSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
|
<th onClick={() => this.updateSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
|
||||||
<th onClick={() => this.changeSort('load')} className={this.sortableClasses('load')}>Load</th>
|
<th onClick={() => this.updateSort('load')} className={this.sortableClasses('load')}>Load</th>
|
||||||
<th>Apps</th>
|
<th>Apps</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{
|
||||||
hosts.map(({name, cpu, load, apps = []}) => {
|
sortedHosts.map((h) => {
|
||||||
return (
|
return <HostRow key={h.name} host={h} source={source} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</tbody>
|
</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({
|
const SearchBar = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onSearch: PropTypes.func.isRequired,
|
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() {
|
handleChange() {
|
||||||
this.props.onSearch(this.refs.searchInput.value);
|
this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch);
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
Loading…
Reference in New Issue