fix(chronograf): connecting to a source no longer takes you to host page
Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com> Co-authored-by: Michael Desa <mjdesa@gmail.com>pull/10616/head
parent
30fc5282f6
commit
2af8a38777
|
@ -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";`,
|
||||
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}/`,
|
||||
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
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
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 {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class HostRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return shallowCompare(this, nextProps)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {host, source} = this.props
|
||||
const {name, cpu, load, apps = []} = host
|
||||
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
|
||||
|
||||
return (
|
||||
<div className="hosts-table--tr">
|
||||
<div className="hosts-table--td" style={{width: colName}}>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link>
|
||||
</div>
|
||||
<div className="hosts-table--td" style={{width: colStatus}}>
|
||||
<div
|
||||
className={classnames(
|
||||
'table-dot',
|
||||
Math.max(host.deltaUptime || 0, host.winDeltaUptime || 0) > 0
|
||||
? 'dot-success'
|
||||
: 'dot-critical'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{width: colCPU}} className="monotype hosts-table--td">
|
||||
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}
|
||||
</div>
|
||||
<div style={{width: colLoad}} className="monotype hosts-table--td">
|
||||
{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}
|
||||
</div>
|
||||
<div className="hosts-table--td">
|
||||
{apps.map((app, index) => {
|
||||
return (
|
||||
<span key={app}>
|
||||
<Link
|
||||
style={{marginLeft: '2px'}}
|
||||
to={{
|
||||
pathname: `/sources/${source.id}/hosts/${name}`,
|
||||
query: {app},
|
||||
}}
|
||||
>
|
||||
{app}
|
||||
</Link>
|
||||
{index === apps.length - 1 ? null : ', '}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HostRow.propTypes = {
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
host: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
cpu: PropTypes.number,
|
||||
load: PropTypes.number,
|
||||
deltaUptime: PropTypes.number.required,
|
||||
apps: PropTypes.arrayOf(PropTypes.string.isRequired),
|
||||
}),
|
||||
}
|
||||
|
||||
export default HostRow
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
export const HOSTS_TABLE = {
|
||||
colName: '40%',
|
||||
colStatus: '74px',
|
||||
colCPU: '70px',
|
||||
colLoad: '68px',
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer'
|
||||
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
||||
import {generateForHosts} from 'src/utils/tempVars'
|
||||
|
||||
import {timeRanges} from 'shared/data/timeRanges'
|
||||
import {
|
||||
getLayouts,
|
||||
getAppsForHost,
|
||||
getMeasurementsForHost,
|
||||
loadHostsLinks,
|
||||
} from 'src/hosts/apis'
|
||||
import {EMPTY_LINKS} from 'src/dashboards/constants/dashboardHeader'
|
||||
|
||||
import {setAutoRefresh, delayEnablePresentationMode} from 'shared/actions/app'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import AutoRefresh from 'src/utils/AutoRefresh'
|
||||
|
||||
@ErrorHandling
|
||||
class HostPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
layouts: [],
|
||||
hostLinks: EMPTY_LINKS,
|
||||
timeRange: timeRanges.find(tr => tr.lower === 'now() - 1h'),
|
||||
dygraphs: [],
|
||||
}
|
||||
}
|
||||
|
||||
async fetchHostsAndMeasurements(layouts) {
|
||||
const {source, params} = this.props
|
||||
|
||||
const host = await getAppsForHost(
|
||||
source.links.proxy,
|
||||
params.hostID,
|
||||
layouts,
|
||||
source.telegraf
|
||||
)
|
||||
|
||||
const measurements = await getMeasurementsForHost(source, params.hostID)
|
||||
|
||||
return {host, measurements}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
data: {layouts},
|
||||
} = await getLayouts()
|
||||
const {location, autoRefresh} = this.props
|
||||
|
||||
AutoRefresh.poll(autoRefresh)
|
||||
|
||||
// fetching layouts and mappings can be done at the same time
|
||||
const {host, measurements} = await this.fetchHostsAndMeasurements(layouts)
|
||||
|
||||
const focusedApp = location.query.app
|
||||
|
||||
const filteredLayouts = layouts.filter(layout => {
|
||||
if (focusedApp) {
|
||||
return layout.app === focusedApp
|
||||
}
|
||||
|
||||
return (
|
||||
host.apps &&
|
||||
host.apps.includes(layout.app) &&
|
||||
measurements.includes(layout.measurement)
|
||||
)
|
||||
})
|
||||
|
||||
const hostLinks = await this.getHostLinks()
|
||||
|
||||
this.setState({layouts: filteredLayouts, hostLinks}) // eslint-disable-line react/no-did-mount-set-state
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {autoRefresh} = this.props
|
||||
if (prevProps.autoRefresh !== autoRefresh) {
|
||||
AutoRefresh.poll(autoRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AutoRefresh.stopPolling()
|
||||
}
|
||||
|
||||
handleChooseTimeRange = ({lower, upper}) => {
|
||||
if (upper) {
|
||||
this.setState({timeRange: {lower, upper}})
|
||||
} else {
|
||||
const timeRange = timeRanges.find(range => range.lower === lower)
|
||||
this.setState({timeRange})
|
||||
}
|
||||
}
|
||||
|
||||
get layouts() {
|
||||
const {timeRange, layouts} = this.state
|
||||
|
||||
if (layouts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const {source, autoRefresh, manualRefresh} = this.props
|
||||
|
||||
const autoflowLayouts = layouts.filter(layout => !!layout.autoflow)
|
||||
|
||||
const cellWidth = 4
|
||||
const cellHeight = 4
|
||||
const pageWidth = 12
|
||||
|
||||
let cellCount = 0
|
||||
const autoflowCells = autoflowLayouts.reduce((allCells, layout) => {
|
||||
return allCells.concat(
|
||||
layout.cells.map(cell => {
|
||||
const x = (cellCount * cellWidth) % pageWidth
|
||||
const y = Math.floor(cellCount * cellWidth / pageWidth) * cellHeight
|
||||
cellCount += 1
|
||||
return Object.assign(cell, {
|
||||
w: cellWidth,
|
||||
h: cellHeight,
|
||||
x,
|
||||
y,
|
||||
})
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
const staticLayouts = layouts.filter(layout => !layout.autoflow)
|
||||
staticLayouts.unshift({cells: autoflowCells})
|
||||
|
||||
let translateY = 0
|
||||
const layoutCells = staticLayouts.reduce((allCells, layout) => {
|
||||
let maxY = 0
|
||||
layout.cells.forEach(cell => {
|
||||
cell.y += translateY
|
||||
if (cell.y > translateY) {
|
||||
maxY = cell.y
|
||||
}
|
||||
cell.queries.forEach(q => {
|
||||
q.text = q.query
|
||||
q.db = source.telegraf
|
||||
q.rp = source.defaultRP
|
||||
})
|
||||
})
|
||||
translateY = maxY
|
||||
|
||||
return allCells.concat(layout.cells)
|
||||
}, [])
|
||||
|
||||
const tempVars = generateForHosts(source)
|
||||
|
||||
return (
|
||||
<LayoutRenderer
|
||||
source={source}
|
||||
isEditable={false}
|
||||
cells={layoutCells}
|
||||
templates={tempVars}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
host={this.props.params.hostID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
autoRefresh,
|
||||
onManualRefresh,
|
||||
params: {hostID},
|
||||
inPresentationMode,
|
||||
handleChooseAutoRefresh,
|
||||
handleClickPresentationButton,
|
||||
} = this.props
|
||||
const {timeRange, hostLinks} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<DashboardHeader
|
||||
timeRange={timeRange}
|
||||
activeDashboard={hostID}
|
||||
autoRefresh={autoRefresh}
|
||||
isHidden={inPresentationMode}
|
||||
onManualRefresh={onManualRefresh}
|
||||
handleChooseAutoRefresh={handleChooseAutoRefresh}
|
||||
handleChooseTimeRange={this.handleChooseTimeRange}
|
||||
handleClickPresentationButton={handleClickPresentationButton}
|
||||
dashboardLinks={hostLinks}
|
||||
/>
|
||||
<FancyScrollbar
|
||||
className={classnames({
|
||||
'page-contents': true,
|
||||
'presentation-mode': inPresentationMode,
|
||||
})}
|
||||
>
|
||||
<div className="container-fluid full-width dashboard">
|
||||
{this.layouts}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getHostLinks = async () => {
|
||||
const {
|
||||
source,
|
||||
params: {hostID},
|
||||
} = this.props
|
||||
|
||||
const activeHost = {name: hostID}
|
||||
const links = await loadHostsLinks(source, {activeHost})
|
||||
|
||||
return links
|
||||
}
|
||||
}
|
||||
|
||||
const {shape, string, bool, func, number} = PropTypes
|
||||
|
||||
HostPage.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
params: shape({
|
||||
hostID: string.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({
|
||||
query: shape({
|
||||
app: string,
|
||||
}),
|
||||
}),
|
||||
inPresentationMode: bool,
|
||||
autoRefresh: number.isRequired,
|
||||
manualRefresh: number.isRequired,
|
||||
onManualRefresh: func.isRequired,
|
||||
handleChooseAutoRefresh: func.isRequired,
|
||||
handleClickPresentationButton: func,
|
||||
}
|
||||
|
||||
const mstp = ({
|
||||
app: {
|
||||
ephemeral: {inPresentationMode},
|
||||
persisted: {autoRefresh},
|
||||
},
|
||||
}) => ({
|
||||
inPresentationMode,
|
||||
autoRefresh,
|
||||
})
|
||||
|
||||
const mdtp = {
|
||||
handleChooseAutoRefresh: setAutoRefresh,
|
||||
handleClickPresentationButton: delayEnablePresentationMode,
|
||||
}
|
||||
|
||||
export default connect(mstp, mdtp)(ManualRefresh(HostPage))
|
|
@ -1,207 +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/AutoRefreshDropdown'
|
||||
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
||||
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
|
||||
|
||||
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} = this.props
|
||||
const {hosts, hostsLoading, hostsError} = this.state
|
||||
return (
|
||||
<div className="page hosts-list-page">
|
||||
<PageHeader
|
||||
titleText="Host List"
|
||||
optionsComponents={this.optionsComponents}
|
||||
sourceIndicator={true}
|
||||
/>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<HostsTable
|
||||
source={source}
|
||||
hosts={_.values(hosts)}
|
||||
hostsLoading={hostsLoading}
|
||||
hostsError={hostsError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
get optionsComponents() {
|
||||
const {autoRefresh, onChooseAutoRefresh, onManualRefresh} = this.props
|
||||
|
||||
return (
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
import HostsPage from './containers/HostsPage'
|
||||
import HostPage from './containers/HostPage'
|
||||
export {HostsPage, HostPage}
|
|
@ -1,32 +0,0 @@
|
|||
import {Source} from 'src/types/sources'
|
||||
import {HostNames, HostName} from 'src/types/hosts'
|
||||
import {DashboardSwitcherLinks} from 'src/types/dashboards'
|
||||
|
||||
export const EMPTY_LINKS = {
|
||||
links: [],
|
||||
active: null,
|
||||
}
|
||||
|
||||
export const linksFromHosts = (
|
||||
hostNames: HostNames,
|
||||
source: Source
|
||||
): DashboardSwitcherLinks => {
|
||||
const links = Object.values(hostNames).map(h => {
|
||||
return {
|
||||
key: h.name,
|
||||
text: h.name,
|
||||
to: `/sources/${source.id}/hosts/${h.name}`,
|
||||
}
|
||||
})
|
||||
|
||||
return {links, active: null}
|
||||
}
|
||||
|
||||
export const updateActiveHostLink = (
|
||||
hostLinks: DashboardSwitcherLinks,
|
||||
host: HostName
|
||||
): DashboardSwitcherLinks => {
|
||||
const active = hostLinks.links.find(link => link.key === host.name)
|
||||
|
||||
return {...hostLinks, active}
|
||||
}
|
|
@ -17,7 +17,6 @@ import {getBasepath} from 'src/utils/basepath'
|
|||
import App from 'src/App'
|
||||
import CheckSources from 'src/CheckSources'
|
||||
import {StatusPage} from 'src/status'
|
||||
import {HostsPage, HostPage} from 'src/hosts'
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||
import {LogsPage} from 'src/logs'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
|
@ -107,8 +106,6 @@ class Root extends PureComponent<{}, State> {
|
|||
<Route path="/sources/:sourceID" component={App}>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="status" component={StatusPage} />
|
||||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route path="dashboards/:dashboardID" component={DashboardPage} />
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
|
|
|
@ -55,14 +55,6 @@ class SideNav extends PureComponent<Props> {
|
|||
<span className="sidebar--icon icon cubo-uniform" />
|
||||
</Link>
|
||||
</div>
|
||||
<NavBlock
|
||||
highlightWhen={['hosts']}
|
||||
icon="eye"
|
||||
link={`${sourcePrefix}/hosts`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||
</NavBlock>
|
||||
<NavBlock
|
||||
highlightWhen={['delorean']}
|
||||
icon="capacitor2"
|
||||
|
|
|
@ -65,7 +65,7 @@ class InfluxTableRow extends PureComponent<Props> {
|
|||
return (
|
||||
<Link
|
||||
className="btn btn-default btn-xs source-table--connect"
|
||||
to={`/sources/${source.id}/hosts`}
|
||||
to={`/sources/${source.id}/manage-sources`}
|
||||
>
|
||||
Connect
|
||||
</Link>
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import {loadHostsLinks} from 'src/hosts/apis'
|
||||
import {source} from 'test/resources'
|
||||
|
||||
import {HostNames} from 'src/types/hosts'
|
||||
import {DashboardSwitcherLinks} from 'src/types/dashboards'
|
||||
|
||||
describe('hosts.apis.loadHostLinks', () => {
|
||||
const socure = {...source, id: '897'}
|
||||
|
||||
const hostNames: HostNames = {
|
||||
'zelda.local': {
|
||||
name: 'zelda.local',
|
||||
},
|
||||
'gannon.local': {
|
||||
name: 'gannon.local',
|
||||
},
|
||||
'korok.local': {
|
||||
name: 'korok.local',
|
||||
},
|
||||
}
|
||||
|
||||
const hostNamesAJAX = async () => hostNames
|
||||
|
||||
const options = {
|
||||
activeHost: {
|
||||
name: 'korok.local',
|
||||
},
|
||||
getHostNamesAJAX: hostNamesAJAX,
|
||||
}
|
||||
|
||||
it('can load the host links', async () => {
|
||||
const hostLinks = await loadHostsLinks(socure, options)
|
||||
|
||||
const expectedLinks: DashboardSwitcherLinks = {
|
||||
active: {
|
||||
key: 'korok.local',
|
||||
text: 'korok.local',
|
||||
to: '/sources/897/hosts/korok.local',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
key: 'zelda.local',
|
||||
text: 'zelda.local',
|
||||
to: '/sources/897/hosts/zelda.local',
|
||||
},
|
||||
{
|
||||
key: 'gannon.local',
|
||||
text: 'gannon.local',
|
||||
to: '/sources/897/hosts/gannon.local',
|
||||
},
|
||||
{
|
||||
key: 'korok.local',
|
||||
text: 'korok.local',
|
||||
to: '/sources/897/hosts/korok.local',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(hostLinks).toEqual(expectedLinks)
|
||||
})
|
||||
})
|
|
@ -1,66 +0,0 @@
|
|||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
import {HostsPage} from 'src/hosts/containers/HostsPage'
|
||||
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'
|
||||
|
||||
jest.mock('src/hosts/apis', () => require('mocks/hosts/apis'))
|
||||
jest.mock('src/shared/apis/env', () => require('mocks/shared/apis/env'))
|
||||
|
||||
import {getCpuAndLoadForHosts} from 'src/hosts/apis'
|
||||
|
||||
const setup = (override = {}) => {
|
||||
const props = {
|
||||
source,
|
||||
links: {environment: ''},
|
||||
autoRefresh: 0,
|
||||
manualRefresh: 0,
|
||||
onChooseAutoRefresh: () => {},
|
||||
onManualRefresh: () => {},
|
||||
notify: () => {},
|
||||
...override,
|
||||
}
|
||||
|
||||
const wrapper = shallow(<HostsPage {...props} />)
|
||||
return {wrapper, props}
|
||||
}
|
||||
|
||||
describe('Hosts.Containers.HostsPage', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders all children components', () => {
|
||||
const {wrapper} = setup()
|
||||
const hostsTable = wrapper.find(HostsTable)
|
||||
|
||||
expect(hostsTable.exists()).toBe(true)
|
||||
|
||||
const pageTitle = wrapper
|
||||
.find(PageHeader)
|
||||
.dive()
|
||||
.find(Title)
|
||||
.dive()
|
||||
.find('h1')
|
||||
.first()
|
||||
.text()
|
||||
|
||||
expect(pageTitle).toBe('Host List')
|
||||
})
|
||||
|
||||
describe('hosts', () => {
|
||||
it('renders hosts when response has hosts', done => {
|
||||
const {wrapper} = setup()
|
||||
|
||||
process.nextTick(() => {
|
||||
wrapper.update()
|
||||
const hostsTable = wrapper.find(HostsTable)
|
||||
expect(hostsTable.prop('hosts').length).toBe(1)
|
||||
expect(getCpuAndLoadForHosts).toHaveBeenCalledTimes(2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,80 +0,0 @@
|
|||
import {
|
||||
updateActiveHostLink,
|
||||
linksFromHosts,
|
||||
} from 'src/hosts/utils/hostsSwitcherLinks'
|
||||
import {source} from 'test/resources'
|
||||
import {HostNames} from 'src/types/hosts'
|
||||
|
||||
describe('hosts.utils.hostSwitcherLinks', () => {
|
||||
describe('linksFromHosts', () => {
|
||||
const socure = {...source, id: '897'}
|
||||
|
||||
const hostNames: HostNames = {
|
||||
'zelda.local': {
|
||||
name: 'zelda.local',
|
||||
},
|
||||
'gannon.local': {
|
||||
name: 'gannon.local',
|
||||
},
|
||||
}
|
||||
|
||||
it('can build host links for a given source', () => {
|
||||
const actualLinks = linksFromHosts(hostNames, socure)
|
||||
|
||||
const expectedLinks = {
|
||||
links: [
|
||||
{
|
||||
key: 'zelda.local',
|
||||
text: 'zelda.local',
|
||||
to: '/sources/897/hosts/zelda.local',
|
||||
},
|
||||
{
|
||||
key: 'gannon.local',
|
||||
text: 'gannon.local',
|
||||
to: '/sources/897/hosts/gannon.local',
|
||||
},
|
||||
],
|
||||
active: null,
|
||||
}
|
||||
|
||||
expect(actualLinks).toEqual(expectedLinks)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateActiveHostLink', () => {
|
||||
const link1 = {
|
||||
key: 'korok.local',
|
||||
text: 'korok.local',
|
||||
to: '/sources/897/hosts/korok.local',
|
||||
}
|
||||
|
||||
const link2 = {
|
||||
key: 'deku.local',
|
||||
text: 'deku.local',
|
||||
to: '/sources/897/hosts/deku.local',
|
||||
}
|
||||
|
||||
const activeLink = {
|
||||
key: 'robbie.local',
|
||||
text: 'robbie.local',
|
||||
to: '/sources/897/hosts/robbie.local',
|
||||
}
|
||||
|
||||
const activeHostName = {
|
||||
name: 'robbie.local',
|
||||
}
|
||||
|
||||
const links = [link1, activeLink, link2]
|
||||
|
||||
it('can set the active host link', () => {
|
||||
const loadedLinks = {
|
||||
links,
|
||||
active: null,
|
||||
}
|
||||
const actualLinks = updateActiveHostLink(loadedLinks, activeHostName)
|
||||
const expectedLinks = {links, active: activeLink}
|
||||
|
||||
expect(actualLinks).toEqual(expectedLinks)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue