Merge pull request #4404 from influxdata/feature/host-page-loading-status
Feature/host page loading statuspull/4399/head
commit
285f2843b5
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
export const HOSTS_TABLE = {
|
||||
export const HOSTS_TABLE_SIZING = {
|
||||
colName: '40%',
|
||||
colStatus: '74px',
|
||||
colCPU: '70px',
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
|
@ -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))
|
|
@ -10,7 +10,6 @@ export const getEnv = async url => {
|
|||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Error retrieving envs: ', error)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue