Merge pull request #4404 from influxdata/feature/host-page-loading-status

Feature/host page loading status
pull/4399/head
Deniz Kusefoglu 2018-09-10 23:43:09 -07:00 committed by GitHub
commit 285f2843b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 874 additions and 709 deletions

View File

@ -19,6 +19,7 @@
1. [#4389](https://github.com/influxdata/chronograf/pull/4389): Add regexp search for appname in log lines
1. [#4403](https://github.com/influxdata/chronograf/pull/4403): Add ability to toggle between Flux/InfluxQL on dynamic source in CEO
1. [#4404](https://github.com/influxdata/chronograf/pull/4404): Add loading status indicator to hosts page
### UI Improvements
1. [#4227](https://github.com/influxdata/chronograf/pull/4227): Redesign Cell Editor Overlay for reuse in other parts of application
@ -27,9 +28,6 @@
1. [#4363](https://github.com/influxdata/chronograf/pull/4363): Move log message truncation controls into logs filter bar
1. [#4391](https://github.com/influxdata/chronograf/pull/4391): Colorize entire Single Stat cell
1. [#4392](https://github.com/influxdata/chronograf/pull/4392): Add log filters on left side
### UI Improvements
1. [#4236](https://github.com/influxdata/chronograf/pull/4236): Add spinner when loading logs table rows
1. [#4330](https://github.com/influxdata/chronograf/pull/4330): Position cloned cells adjacent to target cell

View File

@ -1,294 +0,0 @@
import {proxy} from 'utils/queryUrlGenerator'
import replaceTemplate from 'src/tempVars/utils/replace'
import AJAX from 'utils/ajax'
import {
linksFromHosts,
updateActiveHostLink,
} from 'src/hosts/utils/hostsSwitcherLinks'
import _ from 'lodash'
export const getCpuAndLoadForHosts = (
proxyLink,
telegrafDB,
telegrafSystemInterval,
tempVars
) => {
const query = replaceTemplate(
`SELECT mean("usage_user") FROM \":db:\".\":rp:\".\"cpu\" WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host;
SELECT mean("load1") FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - 10m GROUP BY host;
SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SELECT mean("Percent_Processor_Time") FROM \":db:\".\":rp:\".\"win_cpu\" WHERE time > now() - 10m GROUP BY host;
SELECT mean("Processor_Queue_Length") FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - 10s GROUP BY host;
SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SHOW TAG VALUES WITH KEY = "host" WHERE TIME > now() - 10m;`,
tempVars
)
return proxy({
source: proxyLink,
query,
db: telegrafDB,
tempVars,
}).then(resp => {
const hosts = {}
const precision = 100
const cpuSeries = _.get(resp, ['data', 'results', '0', 'series'], [])
const loadSeries = _.get(resp, ['data', 'results', '1', 'series'], [])
const uptimeSeries = _.get(resp, ['data', 'results', '2', 'series'], [])
const winCPUSeries = _.get(resp, ['data', 'results', '3', 'series'], [])
const winLoadSeries = _.get(resp, ['data', 'results', '4', 'series'], [])
const winUptimeSeries = _.get(resp, ['data', 'results', '5', 'series'], [])
const allHostsSeries = _.get(resp, ['data', 'results', '6', 'series'], [])
allHostsSeries.forEach(s => {
const hostnameIndex = s.columns.findIndex(col => col === 'value')
s.values.forEach(v => {
const hostname = v[hostnameIndex]
hosts[hostname] = {
name: hostname,
deltaUptime: -1,
cpu: 0.0,
load: 0.0,
}
})
})
cpuSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host] = {
name: s.tags.host,
cpu: Math.round(s.values[0][meanIndex] * precision) / precision,
}
})
loadSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host].load =
Math.round(s.values[0][meanIndex] * precision) / precision
})
uptimeSeries.forEach(s => {
const uptimeIndex = s.columns.findIndex(col => col === 'deltaUptime')
hosts[s.tags.host].deltaUptime =
s.values[s.values.length - 1][uptimeIndex]
})
winCPUSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host] = {
name: s.tags.host,
cpu: Math.round(s.values[0][meanIndex] * precision) / precision,
}
})
winLoadSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host].load =
Math.round(s.values[0][meanIndex] * precision) / precision
})
winUptimeSeries.forEach(s => {
const winUptimeIndex = s.columns.findIndex(
col => col === 'winDeltaUptime'
)
hosts[s.tags.host].winDeltaUptime =
s.values[s.values.length - 1][winUptimeIndex]
})
return hosts
})
}
async function getAllHosts(source) {
const {
telegraf,
links: {proxy: proxyLink},
} = source
try {
const resp = await proxy({
source: proxyLink,
query: 'show tag values with key = "host"',
db: telegraf,
})
const hosts = {}
const allHostsSeries = _.get(resp, ['data', 'results', '0', 'series'], [])
allHostsSeries.forEach(s => {
const hostnameIndex = s.columns.findIndex(col => col === 'value')
s.values.forEach(v => {
const hostname = v[hostnameIndex]
hosts[hostname] = {
name: hostname,
}
})
})
return hosts
} catch (error) {
console.error(error) // eslint-disable-line no-console
throw error
}
}
export const loadHostsLinks = async (
source,
{activeHost = {}, getHostNamesAJAX = getAllHosts} = {}
) => {
const hostNames = await getHostNamesAJAX(source)
const allLinks = linksFromHosts(hostNames, source)
return updateActiveHostLink(allLinks, activeHost)
}
export const getLayouts = () =>
AJAX({
method: 'GET',
resource: 'layouts',
})
export const getAppsForHost = (proxyLink, host, appLayouts, telegrafDB) => {
const measurements = appLayouts.map(m => `^${m.measurement}$`).join('|')
const measurementsToApps = _.zipObject(
appLayouts.map(m => m.measurement),
appLayouts.map(({app}) => app)
)
return proxy({
source: proxyLink,
query: `show series from /${measurements}/ where host = '${host}'`,
db: telegrafDB,
}).then(resp => {
const result = {apps: [], tags: {}}
const allSeries = _.get(resp, 'data.results.0.series.0.values', [])
allSeries.forEach(([series]) => {
const seriesObj = parseSeries(series)
const measurement = seriesObj.measurement
result.apps = _.uniq(result.apps.concat(measurementsToApps[measurement]))
_.assign(result.tags, seriesObj.tags)
})
return result
})
}
export const getAppsForHosts = (proxyLink, hosts, appLayouts, telegrafDB) => {
const measurements = appLayouts.map(m => `^${m.measurement}$`).join('|')
const measurementsToApps = _.zipObject(
appLayouts.map(m => m.measurement),
appLayouts.map(({app}) => app)
)
return proxy({
source: proxyLink,
query: `show series from /${measurements}/ where time > now() - 10m`,
db: telegrafDB,
}).then(resp => {
const newHosts = Object.assign({}, hosts)
const allSeries = _.get(
resp,
['data', 'results', '0', 'series', '0', 'values'],
[]
)
allSeries.forEach(([series]) => {
const seriesObj = parseSeries(series)
const measurement = seriesObj.measurement
const host = _.get(seriesObj, ['tags', 'host'], '')
if (!newHosts[host]) {
return
}
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
})
}
export function getMeasurementsForHost(source, host) {
return proxy({
source: source.links.proxy,
query: `SHOW MEASUREMENTS WHERE "host" = '${host}'`,
db: source.telegraf,
}).then(({data}) => {
if (_isEmpty(data) || _hasError(data)) {
return []
}
const series = _.get(data, ['results', '0', 'series', '0'])
return series.values.map(measurement => {
return measurement[0]
})
})
}
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)
if (match) {
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)
}
return ''
}
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
}
function _hasError(resp) {
return !!resp.results[0].error
}

353
ui/src/hosts/apis/index.ts Normal file
View File

@ -0,0 +1,353 @@
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import {proxy} from 'src/utils/queryUrlGenerator'
import replaceTemplate from 'src/tempVars/utils/replace'
import AJAX from 'src/utils/ajax'
import {
linksFromHosts,
updateActiveHostLink,
} from 'src/hosts/utils/hostsSwitcherLinks'
import {Template, Layout, Source, Host} from 'src/types'
import {HostNames, HostName} from 'src/types/hosts'
import {DashboardSwitcherLinks} from '../../types/dashboards'
interface HostsObject {
[x: string]: Host
}
const EmptyHost: Host = {
name: '',
cpu: 0.0,
load: 0.0,
deltaUptime: -1,
apps: [],
}
interface Series {
name: string
columns: string[]
values: string[]
tags: {
host: string
}
}
interface SeriesObj {
measurement: string
tags: {host: string}
}
interface AppsForHost {
apps: string[]
tags: {host: string}
}
export const getCpuAndLoadForHosts = async (
proxyLink: string,
telegrafDB: string,
telegrafSystemInterval: string,
tempVars: Template[]
): Promise<HostsObject> => {
const query = replaceTemplate(
`SELECT mean("usage_user") FROM \":db:\".\":rp:\".\"cpu\" WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host;
SELECT mean("load1") FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - 10m GROUP BY host;
SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SELECT mean("Percent_Processor_Time") FROM \":db:\".\":rp:\".\"win_cpu\" WHERE time > now() - 10m GROUP BY host;
SELECT mean("Processor_Queue_Length") FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - 10s GROUP BY host;
SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SHOW TAG VALUES WITH KEY = "host" WHERE TIME > now() - 10m;`,
tempVars
)
const {data} = await proxy({
source: proxyLink,
query,
db: telegrafDB,
})
const hosts: HostsObject = {}
const precision = 100
const cpuSeries = getDeep<Series[]>(data, 'results.[0].series', [])
const loadSeries = getDeep<Series[]>(data, 'results.[1].series', [])
const uptimeSeries = getDeep<Series[]>(data, 'results.[2].series', [])
const winCPUSeries = getDeep<Series[]>(data, 'results.[3].series', [])
const winLoadSeries = getDeep<Series[]>(data, 'results.[4].series', [])
const winUptimeSeries = getDeep<Series[]>(data, 'results.[5].series', [])
const allHostsSeries = getDeep<Series[]>(data, 'results.[6].series', [])
allHostsSeries.forEach(s => {
const hostnameIndex = s.columns.findIndex(col => col === 'value')
s.values.forEach(v => {
const hostname = v[hostnameIndex]
hosts[hostname] = {
...EmptyHost,
name: hostname,
}
})
})
cpuSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host] = {
...EmptyHost,
name: s.tags.host,
cpu: Math.round(Number(s.values[0][meanIndex]) * precision) / precision,
}
})
loadSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host].load =
Math.round(Number(s.values[0][meanIndex]) * precision) / precision
})
uptimeSeries.forEach(s => {
const uptimeIndex = s.columns.findIndex(col => col === 'deltaUptime')
hosts[s.tags.host].deltaUptime = Number(
s.values[s.values.length - 1][uptimeIndex]
)
})
winCPUSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host] = {
name: s.tags.host,
cpu: Math.round(Number(s.values[0][meanIndex]) * precision) / precision,
}
})
winLoadSeries.forEach(s => {
const meanIndex = s.columns.findIndex(col => col === 'mean')
hosts[s.tags.host].load =
Math.round(Number(s.values[0][meanIndex]) * precision) / precision
})
winUptimeSeries.forEach(s => {
const winUptimeIndex = s.columns.findIndex(col => col === 'winDeltaUptime')
hosts[s.tags.host].winDeltaUptime = Number(
s.values[s.values.length - 1][winUptimeIndex]
)
})
return hosts
}
const getAllHosts = async (source: Source): Promise<HostNames> => {
const {
telegraf,
links: {proxy: proxyLink},
} = source
try {
const {data} = await proxy({
source: proxyLink,
query: 'show tag values with key = "host"',
db: telegraf,
})
const hosts: HostNames = {}
const allHostsSeries = getDeep<Series[]>(data, 'results[0].series', [])
allHostsSeries.forEach(s => {
const hostnameIndex = s.columns.findIndex(col => col === 'value')
s.values.forEach(v => {
const hostname = v[hostnameIndex]
hosts[hostname] = {
name: hostname,
}
})
})
return hosts
} catch (error) {
console.error(error) // eslint-disable-line no-console
throw error
}
}
export const loadHostsLinks = async (
source: Source,
activeHost: HostName
): Promise<DashboardSwitcherLinks> => {
const hostNames = await getAllHosts(source)
return loadHostsLinksFromNames(source, activeHost, hostNames)
}
export const loadHostsLinksFromNames = async (
source: Source,
activeHost: HostName,
hostNames: HostNames
): Promise<DashboardSwitcherLinks> => {
const allLinks = linksFromHosts(hostNames, source)
return updateActiveHostLink(allLinks, activeHost)
}
export const getLayouts = () =>
AJAX<Layout>({
method: 'GET',
resource: 'layouts',
})
export const getAppsForHost = async (
proxyLink: string,
host: string,
appLayouts: Layout[],
telegrafDB: string
) => {
const measurements = appLayouts.map(m => `^${m.measurement}$`).join('|')
const measurementsToApps = _.zipObject(
appLayouts.map(m => m.measurement),
appLayouts.map(({app}) => app)
)
const {data} = await proxy({
source: proxyLink,
query: `show series from /${measurements}/ where host = '${host}'`,
db: telegrafDB,
})
const appsForHost: AppsForHost = {apps: [], tags: {host: null}}
const allSeries = getDeep<string[][]>(data, 'results.0.series.0.values', [])
allSeries.forEach(series => {
const seriesObj = parseSeries(series[0])
const measurement = seriesObj.measurement
appsForHost.apps = _.uniq(
appsForHost.apps.concat(measurementsToApps[measurement])
)
_.assign(appsForHost.tags, seriesObj.tags)
})
return appsForHost
}
export const getAppsForHosts = async (
proxyLink: string,
hosts: HostsObject,
appLayouts: Layout[],
telegrafDB: string
): Promise<HostsObject> => {
const measurements = appLayouts.map(m => `^${m.measurement}$`).join('|')
const measurementsToApps = _.zipObject(
appLayouts.map(m => m.measurement),
appLayouts.map(({app}) => app)
)
const {data} = await proxy({
source: proxyLink,
query: `show series from /${measurements}/ where time > now() - 10m`,
db: telegrafDB,
})
const newHosts = {...hosts}
const allSeries = getDeep<string[][]>(
data,
'results.[0].series.[0].values',
[]
)
allSeries.forEach(series => {
const seriesObj = parseSeries(series[0])
const measurement = seriesObj.measurement
const host = getDeep<string>(seriesObj, 'tags.host', '')
if (!newHosts[host]) {
return
}
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
}
export const getMeasurementsForHost = async (
source: Source,
host: string
): Promise<string[]> => {
const {data} = await proxy({
source: source.links.proxy,
query: `SHOW MEASUREMENTS WHERE "host" = '${host}'`,
db: source.telegraf,
})
if (isEmpty(data) || hasError(data)) {
return []
}
const values = getDeep<string[][]>(data, 'results.[0].series.[0].values', [])
const measurements = values.map(m => {
return m[0]
})
return measurements
}
const parseSeries = (seriesString: string): SeriesObj => {
const ident = /\w+/
const tag = /,?([^=]+)=([^,]+)/
const parseMeasurement = (s, obj) => {
const match = ident.exec(s)
const measurement = match[0]
if (measurement) {
obj.measurement = measurement
}
return s.slice(match.index + measurement.length)
}
const parseTag = (s, obj) => {
const match = tag.exec(s)
if (match) {
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)
}
return ''
}
let workStr = seriesString.slice()
const out: SeriesObj = {
measurement: null,
tags: {host: null},
}
// Consume measurement
workStr = parseMeasurement(workStr, out)
// Consume tags
while (workStr.length > 0) {
workStr = parseTag(workStr, out)
}
return out
}
const isEmpty = (resp): boolean => {
return !resp.results[0].series
}
const hasError = (resp): boolean => {
return !!resp.results[0].error
}

View File

@ -4,7 +4,7 @@ import shallowCompare from 'react-addons-shallow-compare'
import {Link} from 'react-router'
import classnames from 'classnames'
import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing'
import {HOSTS_TABLE_SIZING} from 'src/hosts/constants/tableSizing'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
@ -20,7 +20,7 @@ class HostRow extends Component {
render() {
const {host, source} = this.props
const {name, cpu, load, apps = []} = host
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE_SIZING
return (
<div className="hosts-table--tr">

View File

@ -1,191 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import SearchBar from 'src/hosts/components/SearchBar'
import HostRow from 'src/hosts/components/HostRow'
import InfiniteScroll from 'shared/components/InfiniteScroll'
import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class HostsTable extends Component {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
sortDirection: null,
sortKey: null,
}
}
filter(allHosts, searchTerm) {
const filterText = searchTerm.toLowerCase()
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].toLowerCase().includes(filterText)
}, false)
} else {
tagResult = false
}
return (
h.name.toLowerCase().includes(filterText) ||
apps.toLowerCase().includes(filterText) ||
tagResult
)
})
}
sort(hosts, key, direction) {
switch (direction) {
case 'asc':
return _.sortBy(hosts, e => e[key])
case 'desc':
return _.sortBy(hosts, e => e[key]).reverse()
default:
return hosts
}
}
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') {
return 'hosts-table--th sortable-header sorting-ascending'
}
return 'hosts-table--th sortable-header sorting-descending'
}
return 'hosts-table--th sortable-header'
}
render() {
const {searchTerm, sortKey, sortDirection} = this.state
const {hosts, hostsLoading, hostsError, source} = this.props
const sortedHosts = this.sort(
this.filter(hosts, searchTerm),
sortKey,
sortDirection
)
const hostCount = sortedHosts.length
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
let hostsTitle
if (hostsLoading) {
hostsTitle = 'Loading Hosts...'
} else if (hostsError.length) {
hostsTitle = 'There was a problem loading hosts'
} else if (hostCount === 1) {
hostsTitle = `${hostCount} Host`
} else {
hostsTitle = `${hostCount} Hosts`
}
return (
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">{hostsTitle}</h2>
<SearchBar
placeholder="Filter by Host..."
onSearch={this.updateSearchTerm}
/>
</div>
<div className="panel-body">
{hostCount > 0 && !hostsError.length ? (
<div className="hosts-table">
<div className="hosts-table--thead">
<div className="hosts-table--tr">
<div
onClick={this.updateSort('name')}
className={this.sortableClasses('name')}
style={{width: colName}}
>
Host
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('deltaUptime')}
className={this.sortableClasses('deltaUptime')}
style={{width: colStatus}}
>
Status
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('cpu')}
className={this.sortableClasses('cpu')}
style={{width: colCPU}}
>
CPU
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('load')}
className={this.sortableClasses('load')}
style={{width: colLoad}}
>
Load
<span className="icon caret-up" />
</div>
<div className="hosts-table--th">Apps</div>
</div>
</div>
<InfiniteScroll
items={sortedHosts.map(h => (
<HostRow key={h.name} host={h} source={source} />
))}
itemHeight={26}
className="hosts-table--tbody"
/>
</div>
) : (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>No Hosts found</h4>
</div>
)}
</div>
</div>
)
}
}
const {arrayOf, bool, number, shape, string} = PropTypes
HostsTable.propTypes = {
hosts: arrayOf(
shape({
name: string,
cpu: number,
load: number,
apps: arrayOf(string.isRequired),
})
),
hostsLoading: bool,
hostsError: string,
source: shape({
id: string.isRequired,
name: string.isRequired,
}).isRequired,
}
export default HostsTable

View File

@ -0,0 +1,232 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import SearchBar from 'src/hosts/components/SearchBar'
import HostRow from 'src/hosts/components/HostRow'
import InfiniteScroll from 'src/shared/components/InfiniteScroll'
import {HOSTS_TABLE_SIZING} from 'src/hosts/constants/tableSizing'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source, RemoteDataState, Host} from 'src/types'
enum SortDirection {
ASC = 'asc',
DESC = 'desc',
}
export interface Props {
hosts: Host[]
hostsPageStatus: RemoteDataState
source: Source
}
interface State {
searchTerm: string
sortDirection: SortDirection
sortKey: string
}
@ErrorHandling
class HostsTable extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
searchTerm: '',
sortDirection: SortDirection.ASC,
sortKey: null,
}
}
public filter(allHosts: Host[], searchTerm: string): Host[] {
const filterText = searchTerm.toLowerCase()
return allHosts.filter(h => {
const apps = h.apps ? h.apps.join(', ') : ''
let tagResult = false
if (h.tags) {
tagResult = Object.keys(h.tags).reduce((acc, key) => {
return acc || h.tags[key].toLowerCase().includes(filterText)
}, false)
} else {
tagResult = false
}
return (
h.name.toLowerCase().includes(filterText) ||
apps.toLowerCase().includes(filterText) ||
tagResult
)
})
}
public sort(hosts: Host[], key: string, direction: SortDirection): Host[] {
switch (direction) {
case SortDirection.ASC:
return _.sortBy<Host>(hosts, e => e[key])
case SortDirection.DESC:
return _.sortBy<Host>(hosts, e => e[key]).reverse()
default:
return hosts
}
}
public updateSearchTerm = (term: string): void => {
this.setState({searchTerm: term})
}
public updateSort = (key: string) => (): void => {
const {sortKey, sortDirection} = this.state
if (sortKey === key) {
const reverseDirection =
sortDirection === SortDirection.ASC
? SortDirection.DESC
: SortDirection.ASC
this.setState({sortDirection: reverseDirection})
} else {
this.setState({sortKey: key, sortDirection: SortDirection.ASC})
}
}
public sortableClasses = (key: string): string => {
const {sortKey, sortDirection} = this.state
if (sortKey === key) {
if (sortDirection === SortDirection.ASC) {
return 'hosts-table--th sortable-header sorting-ascending'
}
return 'hosts-table--th sortable-header sorting-descending'
}
return 'hosts-table--th sortable-header'
}
public render() {
return (
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">{this.HostsTitle}</h2>
<SearchBar
placeholder="Filter by Host..."
onSearch={this.updateSearchTerm}
/>
</div>
<div className="panel-body">{this.TableContents}</div>
</div>
)
}
private get TableContents(): JSX.Element {
const {hosts, hostsPageStatus} = this.props
const hostCount = hosts.length
if (hostsPageStatus === RemoteDataState.Loading) {
return this.LoadingState
}
if (hostsPageStatus === RemoteDataState.Error) {
return this.ErrorState
}
if (hostCount > 0) {
return this.TableWithHosts
}
return this.TableWithNoHosts
}
private get LoadingState(): JSX.Element {
return <div className="page-spinner" />
}
private get ErrorState(): JSX.Element {
return (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>There was a problem loading hosts</h4>
</div>
)
}
private get TableWithHosts(): JSX.Element {
const {source, hosts} = this.props
const {searchTerm, sortKey, sortDirection} = this.state
const sortedHosts = this.sort(
this.filter(hosts, searchTerm),
sortKey,
sortDirection
)
return (
<div className="hosts-table">
{this.HostsTableHeader}
<InfiniteScroll
items={sortedHosts.map(h => (
<HostRow key={h.name} host={h} source={source} />
))}
itemHeight={26}
className="hosts-table--tbody"
/>
</div>
)
}
private get TableWithNoHosts(): JSX.Element {
return (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>No Hosts found</h4>
</div>
)
}
private get HostsTitle(): string {
const {hostsPageStatus, hosts} = this.props
if (hostsPageStatus === RemoteDataState.Loading) {
return 'Loading Hosts...'
}
if (hosts.length === 1) {
return `1 Host`
}
return `${hosts.length} Hosts`
}
private get HostsTableHeader(): JSX.Element {
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE_SIZING
return (
<div className="hosts-table--thead">
<div className="hosts-table--tr">
<div
onClick={this.updateSort('name')}
className={this.sortableClasses('name')}
style={{width: colName}}
>
Host
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('deltaUptime')}
className={this.sortableClasses('deltaUptime')}
style={{width: colStatus}}
>
Status
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('cpu')}
className={this.sortableClasses('cpu')}
style={{width: colCPU}}
>
CPU
<span className="icon caret-up" />
</div>
<div
onClick={this.updateSort('load')}
className={this.sortableClasses('load')}
style={{width: colLoad}}
>
Load
<span className="icon caret-up" />
</div>
<div className="hosts-table--th">Apps</div>
</div>
</div>
)
}
}
export default HostsTable

View File

@ -1,4 +1,4 @@
export const HOSTS_TABLE = {
export const HOSTS_TABLE_SIZING = {
colName: '40%',
colStatus: '74px',
colCPU: '70px',

View File

@ -213,7 +213,7 @@ class HostPage extends Component {
} = this.props
const activeHost = {name: hostID}
const links = await loadHostsLinks(source, {activeHost})
const links = await loadHostsLinks(source, activeHost)
return links
}

View File

@ -1,201 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import HostsTable from 'src/hosts/components/HostsTable'
import AutoRefreshDropdown from 'shared/components/dropdown_auto_refresh/AutoRefreshDropdown'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {Page} from 'src/reusable_ui'
import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis'
import {getEnv} from 'src/shared/apis/env'
import {setAutoRefresh} from 'shared/actions/app'
import {notify as notifyAction} from 'shared/actions/notifications'
import {generateForHosts} from 'src/utils/tempVars'
import {
notifyUnableToGetHosts,
notifyUnableToGetApps,
} from 'shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
export class HostsPage extends Component {
constructor(props) {
super(props)
this.state = {
hosts: {},
hostsLoading: true,
hostsError: '',
}
}
async fetchHostsData() {
const {source, links, notify} = this.props
const {telegrafSystemInterval} = await getEnv(links.environment)
const hostsError = notifyUnableToGetHosts().message
const tempVars = generateForHosts(source)
try {
const hosts = await getCpuAndLoadForHosts(
source.links.proxy,
source.telegraf,
telegrafSystemInterval,
tempVars
)
if (!hosts) {
throw new Error(hostsError)
}
const newHosts = await getAppsForHosts(
source.links.proxy,
hosts,
this.layouts,
source.telegraf
)
this.setState({
hosts: newHosts,
hostsError: '',
hostsLoading: false,
})
} catch (error) {
console.error(error)
notify(notifyUnableToGetHosts())
this.setState({
hostsError,
hostsLoading: false,
})
}
}
async componentDidMount() {
const {notify, autoRefresh} = this.props
this.componentIsMounted = true
this.setState({hostsLoading: true}) // Only print this once
const results = await getLayouts()
const data = _.get(results, 'data')
this.layouts = data && data.layouts
if (!this.layouts) {
const layoutError = notifyUnableToGetApps().message
notify(notifyUnableToGetApps())
this.setState({
hostsError: layoutError,
hostsLoading: false,
})
return
}
await this.fetchHostsData()
if (autoRefresh && this.componentIsMounted) {
this.intervalID = setInterval(() => this.fetchHostsData(), autoRefresh)
}
}
componentWillReceiveProps(nextProps) {
if (this.props.manualRefresh !== nextProps.manualRefresh) {
this.fetchHostsData()
}
if (this.props.autoRefresh !== nextProps.autoRefresh) {
clearInterval(this.intervalID)
if (nextProps.autoRefresh) {
this.intervalID = setInterval(
() => this.fetchHostsData(),
nextProps.autoRefresh
)
}
}
}
render() {
const {
source,
autoRefresh,
onChooseAutoRefresh,
onManualRefresh,
} = this.props
const {hosts, hostsLoading, hostsError} = this.state
return (
<Page className="hosts-list-page">
<Page.Header>
<Page.Header.Left>
<Page.Title title="Host List" />
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
<AutoRefreshDropdown
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
</Page.Header.Right>
</Page.Header>
<Page.Contents scrollable={false}>
<HostsTable
source={source}
hosts={_.values(hosts)}
hostsLoading={hostsLoading}
hostsError={hostsError}
/>
</Page.Contents>
</Page>
)
}
componentWillUnmount() {
this.componentIsMounted = false
clearInterval(this.intervalID)
this.intervalID = false
}
}
const {func, shape, string, number} = PropTypes
const mapStateToProps = state => {
const {
app: {
persisted: {autoRefresh},
},
links,
} = state
return {
links,
autoRefresh,
}
}
HostsPage.propTypes = {
source: shape({
id: string.isRequired,
name: string.isRequired,
type: string, // 'influx-enterprise'
links: shape({
proxy: string.isRequired,
}).isRequired,
telegraf: string.isRequired,
}),
links: shape({
environment: string.isRequired,
}),
autoRefresh: number.isRequired,
manualRefresh: number,
onChooseAutoRefresh: func.isRequired,
onManualRefresh: func.isRequired,
notify: func.isRequired,
}
HostsPage.defaultProps = {
manualRefresh: 0,
}
const mapDispatchToProps = dispatch => ({
onChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(
ManualRefresh(HostsPage)
)

View File

@ -0,0 +1,224 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
// Components
import HostsTable from 'src/hosts/components/HostsTable'
import AutoRefreshDropdown from 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown'
import ManualRefresh, {
ManualRefreshProps,
} from 'src/shared/components/ManualRefresh'
import {Page} from 'src/reusable_ui'
import {ErrorHandling} from 'src/shared/decorators/errors'
// APIs
import {
getCpuAndLoadForHosts,
getLayouts,
getAppsForHosts,
} from 'src/hosts/apis'
import {getEnv} from 'src/shared/apis/env'
// Actions
import {setAutoRefresh} from 'src/shared/actions/app'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Utils
import {generateForHosts} from 'src/utils/tempVars'
// Constants
import {
notifyUnableToGetHosts,
notifyUnableToGetApps,
} from 'src/shared/copy/notifications'
// Types
import {
Source,
Links,
NotificationAction,
RemoteDataState,
Host,
Layout,
} from 'src/types'
interface Props extends ManualRefreshProps {
source: Source
links: Links
autoRefresh: number
onChooseAutoRefresh: () => void
notify: NotificationAction
}
interface State {
hostsObject: {[x: string]: Host}
hostsPageStatus: RemoteDataState
layouts: Layout[]
}
@ErrorHandling
export class HostsPage extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
manualRefresh: 0,
}
public intervalID: number
constructor(props: Props) {
super(props)
this.state = {
hostsObject: {},
hostsPageStatus: RemoteDataState.NotStarted,
layouts: [],
}
}
public async componentDidMount() {
const {notify, autoRefresh} = this.props
this.setState({hostsPageStatus: RemoteDataState.Loading})
const layoutResults = await getLayouts()
const layouts = getDeep<Layout[]>(layoutResults, 'data.layouts', [])
if (!layouts) {
notify(notifyUnableToGetApps())
this.setState({
hostsPageStatus: RemoteDataState.Error,
layouts,
})
return
}
await this.fetchHostsData(layouts)
if (autoRefresh) {
this.intervalID = window.setInterval(
() => this.fetchHostsData(layouts),
autoRefresh
)
}
this.setState({layouts})
}
public componentWillReceiveProps(nextProps: Props) {
const {layouts} = this.state
if (layouts) {
if (this.props.manualRefresh !== nextProps.manualRefresh) {
this.fetchHostsData(layouts)
}
if (this.props.autoRefresh !== nextProps.autoRefresh) {
clearInterval(this.intervalID)
if (nextProps.autoRefresh) {
this.intervalID = window.setInterval(
() => this.fetchHostsData(layouts),
nextProps.autoRefresh
)
}
}
}
}
public componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = null
}
public render() {
const {
source,
autoRefresh,
onChooseAutoRefresh,
onManualRefresh,
} = this.props
const {hostsObject, hostsPageStatus} = this.state
return (
<Page className="hosts-list-page">
<Page.Header>
<Page.Header.Left>
<Page.Title title="Host List" />
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
<AutoRefreshDropdown
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
</Page.Header.Right>
</Page.Header>
<Page.Contents scrollable={false}>
<HostsTable
source={source}
hosts={_.values(hostsObject)}
hostsPageStatus={hostsPageStatus}
/>
</Page.Contents>
</Page>
)
}
private async fetchHostsData(layouts: Layout[]): Promise<void> {
const {source, links, notify} = this.props
const envVars = await getEnv(links.environment)
const telegrafSystemInterval = getDeep<string>(
envVars,
'telegrafSystemInterval',
''
)
const hostsError = notifyUnableToGetHosts().message
const tempVars = generateForHosts(source)
try {
const hostsObject = await getCpuAndLoadForHosts(
source.links.proxy,
source.telegraf,
telegrafSystemInterval,
tempVars
)
if (!hostsObject) {
throw new Error(hostsError)
}
const newHosts = await getAppsForHosts(
source.links.proxy,
hostsObject,
layouts,
source.telegraf
)
this.setState({
hostsObject: newHosts,
hostsPageStatus: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
notify(notifyUnableToGetHosts())
this.setState({
hostsPageStatus: RemoteDataState.Error,
})
}
}
}
const mstp = state => {
const {
app: {
persisted: {autoRefresh},
},
links,
} = state
return {
links,
autoRefresh,
}
}
const mdtp = dispatch => ({
onChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(mstp, mdtp)(ManualRefresh<Props>(HostsPage))

View File

@ -10,7 +10,6 @@ export const getEnv = async url => {
method: 'GET',
url,
})
return data
} catch (error) {
console.error('Error retrieving envs: ', error)

View File

@ -1,3 +1,5 @@
import {Axes} from 'src/types'
export interface HostNames {
[index: string]: HostName
}
@ -5,3 +7,45 @@ export interface HostNames {
export interface HostName {
name: string
}
export interface Host {
name: string
cpu: number
load?: number
apps?: string[]
tags?: {[x: string]: string}
deltaUptime?: number
winDeltaUptime?: number
}
export interface Layout {
id: string
app: string
measurement: string
cells: LayoutCell[]
link: LayoutLink
autoflow: boolean
}
interface LayoutLink {
herf: string
rel: string
}
interface LayoutCell {
x: number
y: number
w: number
h: number
i: string
name: string
type: string
queries: LayoutQuery[]
axes: Axes
colors: string[]
}
interface LayoutQuery {
label: string
query: string
}

View File

@ -67,6 +67,7 @@ import {
import {JSONFeedData} from './status'
import {Annotation} from './annotations'
import {WriteDataMode} from './dataExplorer'
import {Host, Layout} from './hosts'
export {
Me,
@ -137,4 +138,6 @@ export {
TemplateBuilderProps,
WriteDataMode,
QueryStatus,
Host,
Layout,
}

View File

@ -1,4 +1,4 @@
import {loadHostsLinks} from 'src/hosts/apis'
import {loadHostsLinksFromNames} from 'src/hosts/apis'
import {source} from 'test/resources'
import {HostNames} from 'src/types/hosts'
@ -19,17 +19,15 @@ describe('hosts.apis.loadHostLinks', () => {
},
}
const hostNamesAJAX = async () => hostNames
const options = {
activeHost: {
name: 'korok.local',
},
getHostNamesAJAX: hostNamesAJAX,
const activeHost = {
name: 'korok.local',
}
it('can load the host links', async () => {
const hostLinks = await loadHostsLinks(socure, options)
const hostLinks = await loadHostsLinksFromNames(
socure,
activeHost,
hostNames
)
const expectedLinks: DashboardSwitcherLinks = {
active: {

View File

@ -6,7 +6,7 @@ import HostsTable from 'src/hosts/components/HostsTable'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import Title from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import {source} from 'test/resources'
import {source, authLinks} from 'test/resources'
jest.mock('src/hosts/apis', () => require('mocks/hosts/apis'))
jest.mock('src/shared/apis/env', () => require('mocks/shared/apis/env'))
@ -16,12 +16,12 @@ import {getCpuAndLoadForHosts} from 'src/hosts/apis'
const setup = (override = {}) => {
const props = {
source,
links: {environment: ''},
links: authLinks,
autoRefresh: 0,
manualRefresh: 0,
onChooseAutoRefresh: () => {},
onManualRefresh: () => {},
notify: () => {},
onChooseAutoRefresh: jest.fn(),
onManualRefresh: jest.fn(),
notify: jest.fn(),
...override,
}