From 786d3100086fcadd089753abc1d24466f9493ad5 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Thu, 6 Sep 2018 00:13:48 -0700 Subject: [PATCH] Add loading status indicator to HostsPage Convert HostsPage and HostsTable to typescript Display page-spinner if hostpage in loading state Add error state to HostsPage Fix tests for HostsPage Update Changelog Add missing type definitions to HostsTable --- CHANGELOG.md | 4 +- ui/src/hosts/apis/index.js | 294 --------------- ui/src/hosts/apis/index.ts | 353 ++++++++++++++++++ ui/src/hosts/components/HostRow.js | 4 +- ui/src/hosts/components/HostsTable.js | 191 ---------- ui/src/hosts/components/HostsTable.tsx | 232 ++++++++++++ .../{tableSizing.js => tableSizing.ts} | 2 +- ui/src/hosts/containers/HostPage.js | 2 +- ui/src/hosts/containers/HostsPage.js | 201 ---------- ui/src/hosts/containers/HostsPage.tsx | 224 +++++++++++ ui/src/shared/apis/{env.js => env.ts} | 1 - ui/src/types/hosts.ts | 44 +++ ui/src/types/index.ts | 3 + ui/test/hosts/apis/loadHostLinks.test.ts | 18 +- ui/test/hosts/containers/HostsPage.test.tsx | 10 +- 15 files changed, 874 insertions(+), 709 deletions(-) delete mode 100644 ui/src/hosts/apis/index.js create mode 100644 ui/src/hosts/apis/index.ts delete mode 100644 ui/src/hosts/components/HostsTable.js create mode 100644 ui/src/hosts/components/HostsTable.tsx rename ui/src/hosts/constants/{tableSizing.js => tableSizing.ts} (68%) delete mode 100644 ui/src/hosts/containers/HostsPage.js create mode 100644 ui/src/hosts/containers/HostsPage.tsx rename ui/src/shared/apis/{env.js => env.ts} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2ee10df..c07b02a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js deleted file mode 100644 index 4ebf110f2..000000000 --- a/ui/src/hosts/apis/index.js +++ /dev/null @@ -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 -} diff --git a/ui/src/hosts/apis/index.ts b/ui/src/hosts/apis/index.ts new file mode 100644 index 000000000..6dfbd9227 --- /dev/null +++ b/ui/src/hosts/apis/index.ts @@ -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 => { + 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(data, 'results.[0].series', []) + const loadSeries = getDeep(data, 'results.[1].series', []) + const uptimeSeries = getDeep(data, 'results.[2].series', []) + const winCPUSeries = getDeep(data, 'results.[3].series', []) + const winLoadSeries = getDeep(data, 'results.[4].series', []) + const winUptimeSeries = getDeep(data, 'results.[5].series', []) + const allHostsSeries = getDeep(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 => { + 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(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 => { + const hostNames = await getAllHosts(source) + return loadHostsLinksFromNames(source, activeHost, hostNames) +} + +export const loadHostsLinksFromNames = async ( + source: Source, + activeHost: HostName, + hostNames: HostNames +): Promise => { + const allLinks = linksFromHosts(hostNames, source) + + return updateActiveHostLink(allLinks, activeHost) +} + +export const getLayouts = () => + AJAX({ + 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(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 => { + 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( + data, + 'results.[0].series.[0].values', + [] + ) + + allSeries.forEach(series => { + const seriesObj = parseSeries(series[0]) + const measurement = seriesObj.measurement + const host = getDeep(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 => { + 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(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 +} diff --git a/ui/src/hosts/components/HostRow.js b/ui/src/hosts/components/HostRow.js index a74819666..93f1bf27e 100644 --- a/ui/src/hosts/components/HostRow.js +++ b/ui/src/hosts/components/HostRow.js @@ -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 (
diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js deleted file mode 100644 index d43008830..000000000 --- a/ui/src/hosts/components/HostsTable.js +++ /dev/null @@ -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 ( -
-
-

{hostsTitle}

- -
-
- {hostCount > 0 && !hostsError.length ? ( -
-
-
-
- Host - -
-
- Status - -
-
- CPU - -
-
- Load - -
-
Apps
-
-
- ( - - ))} - itemHeight={26} - className="hosts-table--tbody" - /> -
- ) : ( -
-

No Hosts found

-
- )} -
-
- ) - } -} - -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 diff --git a/ui/src/hosts/components/HostsTable.tsx b/ui/src/hosts/components/HostsTable.tsx new file mode 100644 index 000000000..db913769e --- /dev/null +++ b/ui/src/hosts/components/HostsTable.tsx @@ -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 { + 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(hosts, e => e[key]) + case SortDirection.DESC: + return _.sortBy(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 ( +
+
+

{this.HostsTitle}

+ +
+
{this.TableContents}
+
+ ) + } + + 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
+ } + + private get ErrorState(): JSX.Element { + return ( +
+

There was a problem loading hosts

+
+ ) + } + + 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 ( +
+ {this.HostsTableHeader} + ( + + ))} + itemHeight={26} + className="hosts-table--tbody" + /> +
+ ) + } + + private get TableWithNoHosts(): JSX.Element { + return ( +
+

No Hosts found

+
+ ) + } + + 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 ( +
+
+
+ Host + +
+
+ Status + +
+
+ CPU + +
+
+ Load + +
+
Apps
+
+
+ ) + } +} + +export default HostsTable diff --git a/ui/src/hosts/constants/tableSizing.js b/ui/src/hosts/constants/tableSizing.ts similarity index 68% rename from ui/src/hosts/constants/tableSizing.js rename to ui/src/hosts/constants/tableSizing.ts index b1721b46a..b91bee26f 100644 --- a/ui/src/hosts/constants/tableSizing.js +++ b/ui/src/hosts/constants/tableSizing.ts @@ -1,4 +1,4 @@ -export const HOSTS_TABLE = { +export const HOSTS_TABLE_SIZING = { colName: '40%', colStatus: '74px', colCPU: '70px', diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index e8e40a80a..76cd74ec4 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -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 } diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js deleted file mode 100644 index 6cffc7e36..000000000 --- a/ui/src/hosts/containers/HostsPage.js +++ /dev/null @@ -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 ( - - - - - - - - - - - - - - ) - } - - 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) -) diff --git a/ui/src/hosts/containers/HostsPage.tsx b/ui/src/hosts/containers/HostsPage.tsx new file mode 100644 index 000000000..61c85d048 --- /dev/null +++ b/ui/src/hosts/containers/HostsPage.tsx @@ -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 { + public static defaultProps: Partial = { + 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(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 ( + + + + + + + + + + + + + + ) + } + + private async fetchHostsData(layouts: Layout[]): Promise { + const {source, links, notify} = this.props + + const envVars = await getEnv(links.environment) + const telegrafSystemInterval = getDeep( + 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(HostsPage)) diff --git a/ui/src/shared/apis/env.js b/ui/src/shared/apis/env.ts similarity index 99% rename from ui/src/shared/apis/env.js rename to ui/src/shared/apis/env.ts index b7c6133e8..2d2e7945d 100644 --- a/ui/src/shared/apis/env.js +++ b/ui/src/shared/apis/env.ts @@ -10,7 +10,6 @@ export const getEnv = async url => { method: 'GET', url, }) - return data } catch (error) { console.error('Error retrieving envs: ', error) diff --git a/ui/src/types/hosts.ts b/ui/src/types/hosts.ts index 1a906f469..d9aa09fc5 100644 --- a/ui/src/types/hosts.ts +++ b/ui/src/types/hosts.ts @@ -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 +} diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 9a5ac3003..5cc764c44 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -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, } diff --git a/ui/test/hosts/apis/loadHostLinks.test.ts b/ui/test/hosts/apis/loadHostLinks.test.ts index 21cb941f2..c75bdd71e 100644 --- a/ui/test/hosts/apis/loadHostLinks.test.ts +++ b/ui/test/hosts/apis/loadHostLinks.test.ts @@ -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: { diff --git a/ui/test/hosts/containers/HostsPage.test.tsx b/ui/test/hosts/containers/HostsPage.test.tsx index 9dd8dc6de..75a47af9d 100644 --- a/ui/test/hosts/containers/HostsPage.test.tsx +++ b/ui/test/hosts/containers/HostsPage.test.tsx @@ -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, }