Typescriptify Status Page and children

pull/3721/head
ebb-tide 2018-06-18 16:57:56 -07:00
parent 9b51924921
commit 5d7958988f
25 changed files with 294 additions and 1066 deletions

View File

@ -48,7 +48,7 @@ interface State {
@ErrorHandling
export class AllUsersPage extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props)
this.state = {

View File

@ -1,5 +1,5 @@
import {proxy} from 'src/utils/queryUrlGenerator'
import {TimeRange} from '../../types'
import {TimeRange} from 'src/types'
export const getAlerts = (
source: string,

View File

@ -3,15 +3,38 @@ import {noop} from 'src/shared/actions/app'
import _ from 'lodash'
import {errorThrown} from 'src/shared/actions/errors'
import {TimeSeriesResponse} from 'src/types/series'
export const handleLoading = (query, editQueryStatus) => {
interface Query {
host: string | string[]
text: string
id: string
database?: string
db?: string
rp?: string
}
interface Payload {
source: string
query: Query
tempVars: any[]
db?: string
rp?: string
resolution?: number
}
export const handleLoading = (query: Query, editQueryStatus) => {
editQueryStatus(query.id, {
loading: true,
})
}
// {results: [{}]}
export const handleSuccess = (data, query, editQueryStatus) => {
export const handleSuccess = (
data: TimeSeriesResponse,
query: Query,
editQueryStatus
) => {
const {results} = data
const error = _.get(results, ['0', 'error'], false)
const series = _.get(results, ['0', 'series'], false)
@ -51,24 +74,6 @@ export const handleError = (error, query, editQueryStatus) => {
})
}
interface Query {
host: string | string[]
text: string
id: string
database?: string
db?: string
rp?: string
}
interface Payload {
source: string
query: Query
tempVars: any[]
db?: string
rp?: string
resolution?: number
}
export const fetchTimeSeriesAsync = async (
{source, db, rp, query, tempVars, resolution}: Payload,
editQueryStatus = noop

View File

@ -175,8 +175,10 @@ class Dygraph extends Component<Props, State> {
}
public componentWillUnmount() {
this.dygraph.destroy()
delete this.dygraph
if (this.dygraph) {
this.dygraph.destroy()
delete this.dygraph
}
}
public shouldComponentUpdate(nextProps: Props, nextState: State) {

View File

@ -427,7 +427,23 @@ export const DYGRAPH_CONTAINER_H_MARGIN = 16
export const DYGRAPH_CONTAINER_V_MARGIN = 8
export const DYGRAPH_CONTAINER_XLABEL_MARGIN = 20
export const DEFAULT_SOURCE_LINKS = {
self: '',
kapacitors: '',
proxy: '',
queries: '',
write: '',
permissions: '',
users: '',
roles: '',
databases: '',
annotations: '',
health: '',
services: '',
}
export const DEFAULT_SOURCE = {
id: '',
url: 'http://localhost:8086',
name: 'Influx 1',
username: '',
@ -436,6 +452,11 @@ export const DEFAULT_SOURCE = {
telegraf: 'telegraf',
insecureSkipVerify: false,
metaUrl: '',
organization: '',
role: '',
defaultRP: '',
links: DEFAULT_SOURCE_LINKS,
type: '',
}
export const defaultIntervalValue = '333'

View File

@ -6,6 +6,8 @@ interface TimeRangeOption extends TimeRange {
menuOption: string
}
const nowminus30d = 'now() - 30d'
export const timeRanges: TimeRangeOption[] = [
{
defaultGroupBy: '10s',
@ -75,7 +77,7 @@ export const timeRanges: TimeRangeOption[] = [
defaultGroupBy: '6h',
seconds: 2592000,
inputValue: 'Past 30d',
lower: 'now() - 30d',
lower: nowminus30d,
upper: null,
menuOption: 'Past 30d',
},
@ -89,3 +91,7 @@ export const defaultTimeRange = {
seconds: 900,
format: FORMAT_INFLUXQL,
}
export const STATUS_PAGE_TIME_RANGE = timeRanges.find(
tr => tr.lower === nowminus30d
)

View File

@ -14,12 +14,12 @@ import {
notifySourceDeleteFailed,
} from 'src/shared/copy/notifications'
import {Source, NotificationFunc} from 'src/types'
import {Source, Notification} from 'src/types'
interface Props {
source: Source
sources: Source[]
notify: (n: NotificationFunc) => void
notify: (n: Notification) => void
deleteKapacitor: actions.DeleteKapacitorAsync
fetchKapacitors: actions.FetchKapacitorsAsync
removeAndLoadSources: actions.RemoveAndLoadSources

View File

@ -1,278 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router'
import _ from 'lodash'
import {getSource} from 'shared/apis'
import {createSource, updateSource} from 'shared/apis'
import {
addSource as addSourceAction,
updateSource as updateSourceAction,
} from 'shared/actions/sources'
import {notify as notifyAction} from 'shared/actions/notifications'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import Notifications from 'shared/components/Notifications'
import SourceForm from 'src/sources/components/SourceForm'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import SourceIndicator from 'shared/components/SourceIndicator'
import {DEFAULT_SOURCE} from 'shared/constants'
const initialPath = '/sources/new'
import {
notifyErrorConnectingToSource,
notifySourceCreationSucceeded,
notifySourceCreationFailed,
notifySourceUdpated,
notifySourceUdpateFailed,
} from 'shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class SourcePage extends Component {
constructor(props) {
super(props)
this.state = {
isLoading: true,
source: DEFAULT_SOURCE,
editMode: props.params.id !== undefined,
isInitialSource: props.router.location.pathname === initialPath,
}
}
componentDidMount() {
const {editMode} = this.state
const {params, notify} = this.props
if (!editMode) {
return this.setState({isLoading: false})
}
getSource(params.id)
.then(({data: source}) => {
this.setState({
source: {...DEFAULT_SOURCE, ...source},
isLoading: false,
})
})
.catch(error => {
notify(notifyErrorConnectingToSource(this._parseError(error)))
this.setState({isLoading: false})
})
}
handleInputChange = e => {
let val = e.target.value
const name = e.target.name
if (e.target.type === 'checkbox') {
val = e.target.checked
}
this.setState(prevState => {
const source = {
...prevState.source,
[name]: val,
}
return {...prevState, source}
})
}
handleBlurSourceURL = () => {
const {source, editMode} = this.state
if (editMode) {
this.setState(this._normalizeSource)
return
}
if (!source.url) {
return
}
this.setState(this._normalizeSource, this._createSourceOnBlur)
}
handleSubmit = e => {
e.preventDefault()
const {isCreated, editMode} = this.state
const isNewSource = !editMode
if (!isCreated && isNewSource) {
return this.setState(this._normalizeSource, this._createSource)
}
this.setState(this._normalizeSource, this._updateSource)
}
gotoPurgatory = () => {
const {router} = this.props
router.push('/purgatory')
}
_normalizeSource({source}) {
const url = source.url.trim()
if (source.url.startsWith('http')) {
return {source: {...source, url}}
}
return {source: {...source, url: `http://${url}`}}
}
_createSourceOnBlur = () => {
const {source} = this.state
// if there is a type on source it has already been created
if (source.type) {
return
}
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.setState({
source: {...DEFAULT_SOURCE, ...sourceFromServer},
isCreated: true,
})
})
.catch(err => {
// dont want to flash this until they submit
const error = this._parseError(err)
console.error('Error creating InfluxDB connection: ', error)
})
}
_createSource = () => {
const {source} = this.state
const {notify} = this.props
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this._redirect(sourceFromServer)
notify(notifySourceCreationSucceeded(source.name))
})
.catch(error => {
notify(notifySourceCreationFailed(source.name, this._parseError(error)))
})
}
_updateSource = () => {
const {source} = this.state
const {notify} = this.props
updateSource(source)
.then(({data: sourceFromServer}) => {
this.props.updateSource(sourceFromServer)
this._redirect(sourceFromServer)
notify(notifySourceUdpated(source.name))
})
.catch(error => {
notify(notifySourceUdpateFailed(source.name, this._parseError(error)))
})
}
_redirect = source => {
const {isInitialSource} = this.state
const {params, router} = this.props
if (isInitialSource) {
return this._redirectToApp(source)
}
router.push(`/sources/${params.sourceID}/manage-sources`)
}
_redirectToApp = source => {
const {location, router} = this.props
const {redirectPath} = location.query
if (!redirectPath) {
return router.push(`/sources/${source.id}/hosts`)
}
const fixedPath = redirectPath.replace(
/\/sources\/[^/]*/,
`/sources/${source.id}`
)
return router.push(fixedPath)
}
_parseError = error => {
return _.get(error, ['data', 'message'], error)
}
render() {
const {isLoading, source, editMode, isInitialSource} = this.state
if (isLoading) {
return <div className="page-spinner" />
}
return (
<div className={`${isInitialSource ? '' : 'page'}`}>
<Notifications />
<div className="page-header">
<div className="page-header__container page-header__source-page">
<div className="page-header__col-md-8">
<div className="page-header__left">
<h1 className="page-header__title">
{editMode
? 'Configure InfluxDB Connection'
: 'Add a New InfluxDB Connection'}
</h1>
</div>
{isInitialSource ? null : (
<div className="page-header__right">
<SourceIndicator />
</div>
)}
</div>
</div>
</div>
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel">
<SourceForm
source={source}
editMode={editMode}
onInputChange={this.handleInputChange}
onSubmit={this.handleSubmit}
onBlurSourceURL={this.handleBlurSourceURL}
isInitialSource={isInitialSource}
gotoPurgatory={this.gotoPurgatory}
/>
</div>
</div>
</div>
</div>
</FancyScrollbar>
</div>
)
}
}
const {func, shape, string} = PropTypes
SourcePage.propTypes = {
params: shape({
id: string,
sourceID: string,
}),
router: shape({
push: func.isRequired,
}).isRequired,
location: shape({
query: shape({
redirectPath: string,
}).isRequired,
}).isRequired,
notify: func.isRequired,
addSource: func.isRequired,
updateSource: func.isRequired,
}
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
addSource: bindActionCreators(addSourceAction, dispatch),
updateSource: bindActionCreators(updateSourceAction, dispatch),
})
export default connect(null, mapDispatchToProps)(withRouter(SourcePage))

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router'
import _ from 'lodash'
import React, {Component, ChangeEvent, FormEvent} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
import {Location} from 'history'
import {getSource} from 'src/shared/apis'
import {createSource, updateSource} from 'src/shared/apis'
import {
@ -27,45 +26,47 @@ import {
notifySourceUdpateFailed,
} from 'src/shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source} from 'src/types'
import {Source, Notification, NotificationFunc} from 'src/types'
import {getDeep} from 'src/utils/wrappers'
interface Params {
id: string
hash: string
sourceID: string
}
interface Props {
params: shape({
id: string,
sourceID: string,
}),
router: shape({
push: func.isRequired,
}).isRequired,
location: shape({
query: shape({
redirectPath: string,
}).isRequired,
}).isRequired,
notify: func.isRequired,
addSource: func.isRequired,
updateSource: func.isRequired,}
location: Location
router: InjectedRouter
params: Params
notify: (notification: Notification | NotificationFunc) => void
addSource: (s: Source) => void
updateSource: (s: Source) => void
}
interface State {
isLoading: boolean
isCreated: boolean
source: Source
editMode: boolean
isInitialSource: boolean}
isInitialSource: boolean
}
@ErrorHandling
class SourcePage extends Component<Props, State> {
constructor(props:Props) {
constructor(props: Props) {
super(props)
this.state = {
isLoading: true,
isCreated: false,
source: DEFAULT_SOURCE,
editMode: props.params.id !== undefined,
isInitialSource: props.router.location.pathname === initialPath,
isInitialSource: props.location.pathname === initialPath,
}
}
componentDidMount() {
public componentDidMount() {
const {editMode} = this.state
const {params, notify} = this.props
@ -81,148 +82,12 @@ class SourcePage extends Component<Props, State> {
})
})
.catch(error => {
notify(notifyErrorConnectingToSource(this._parseError(error)))
notify(notifyErrorConnectingToSource(this.parseError(error)))
this.setState({isLoading: false})
})
}
handleInputChange = e => {
let val = e.target.value
const name = e.target.name
if (e.target.type === 'checkbox') {
val = e.target.checked
}
this.setState(prevState => {
const source = {
...prevState.source,
[name]: val,
}
return {...prevState, source}
})
}
handleBlurSourceURL = () => {
const {source, editMode} = this.state
if (editMode) {
this.setState(this._normalizeSource)
return
}
if (!source.url) {
return
}
this.setState(this._normalizeSource, this._createSourceOnBlur)
}
handleSubmit = e => {
e.preventDefault()
const {isCreated, editMode} = this.state
const isNewSource = !editMode
if (!isCreated && isNewSource) {
return this.setState(this._normalizeSource, this._createSource)
}
this.setState(this._normalizeSource, this._updateSource)
}
gotoPurgatory = () => {
const {router} = this.props
router.push('/purgatory')
}
_normalizeSource({source}) {
const url = source.url.trim()
if (source.url.startsWith('http')) {
return {source: {...source, url}}
}
return {source: {...source, url: `http://${url}`}}
}
_createSourceOnBlur = () => {
const {source} = this.state
// if there is a type on source it has already been created
if (source.type) {
return
}
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.setState({
source: {...DEFAULT_SOURCE, ...sourceFromServer},
isCreated: true,
})
})
.catch(err => {
// dont want to flash this until they submit
const error = this._parseError(err)
console.error('Error creating InfluxDB connection: ', error)
})
}
_createSource = () => {
const {source} = this.state
const {notify} = this.props
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this._redirect(sourceFromServer)
notify(notifySourceCreationSucceeded(source.name))
})
.catch(error => {
notify(notifySourceCreationFailed(source.name, this._parseError(error)))
})
}
_updateSource = () => {
const {source} = this.state
const {notify} = this.props
updateSource(source)
.then(({data: sourceFromServer}) => {
this.props.updateSource(sourceFromServer)
this._redirect(sourceFromServer)
notify(notifySourceUdpated(source.name))
})
.catch(error => {
notify(notifySourceUdpateFailed(source.name, this._parseError(error)))
})
}
_redirect = source => {
const {isInitialSource} = this.state
const {params, router} = this.props
if (isInitialSource) {
return this._redirectToApp(source)
}
router.push(`/sources/${params.sourceID}/manage-sources`)
}
_redirectToApp = source => {
const {location, router} = this.props
const {redirectPath} = location.query
if (!redirectPath) {
return router.push(`/sources/${source.id}/hosts`)
}
const fixedPath = redirectPath.replace(
/\/sources\/[^/]*/,
`/sources/${source.id}`
)
return router.push(fixedPath)
}
_parseError = error => {
return _.get(error, ['data', 'message'], error)
}
render() {
public render() {
const {isLoading, source, editMode, isInitialSource} = this.state
if (isLoading) {
@ -272,26 +137,143 @@ class SourcePage extends Component<Props, State> {
</div>
)
}
}
const {func, shape, string} = PropTypes
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
let val: string | boolean = e.target.value
const name = e.target.name
SourcePage.propTypes = {
params: shape({
id: string,
sourceID: string,
}),
router: shape({
push: func.isRequired,
}).isRequired,
location: shape({
query: shape({
redirectPath: string,
}).isRequired,
}).isRequired,
notify: func.isRequired,
addSource: func.isRequired,
updateSource: func.isRequired,
if (e.target.type === 'checkbox') {
val = e.target.checked
}
this.setState(prevState => {
const source = {
...prevState.source,
[name]: val,
}
return {...prevState, source}
})
}
private handleBlurSourceURL = () => {
const {source, editMode} = this.state
if (editMode) {
this.setState(this.normalizeSource)
return
}
if (!source.url) {
return
}
this.setState(this.normalizeSource, this.createSourceOnBlur)
}
private handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const {isCreated, editMode} = this.state
const isNewSource = !editMode
if (!isCreated && isNewSource) {
return this.setState(this.normalizeSource, this.createSource)
}
this.setState(this.normalizeSource, this.updateSource)
}
private gotoPurgatory = () => {
const {router} = this.props
router.push('/purgatory')
}
private normalizeSource() {
const {source} = this.state
const url = source.url.trim()
if (source.url.startsWith('http')) {
return {source: {...source, url}}
}
return {source: {...source, url: `http://${url}`}}
}
private createSourceOnBlur = () => {
const {source} = this.state
// if there is a type on source it has already been created
if (source.type) {
return
}
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.setState({
source: {...DEFAULT_SOURCE, ...sourceFromServer},
isCreated: true,
})
})
.catch(err => {
// dont want to flash this until they submit
const error = this.parseError(err)
console.error('Error creating InfluxDB connection: ', error)
})
}
private createSource = () => {
const {source} = this.state
const {notify} = this.props
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceCreationSucceeded(source.name))
})
.catch(error => {
notify(notifySourceCreationFailed(source.name, this.parseError(error)))
})
}
private updateSource = () => {
const {source} = this.state
const {notify} = this.props
updateSource(source)
.then(({data: sourceFromServer}) => {
this.props.updateSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceUdpated(source.name))
})
.catch(error => {
notify(notifySourceUdpateFailed(source.name, this.parseError(error)))
})
}
private redirect = (source: Source) => {
const {isInitialSource} = this.state
const {params, router} = this.props
if (isInitialSource) {
return this.redirectToApp(source)
}
router.push(`/sources/${params.sourceID}/manage-sources`)
}
private redirectToApp = (source: Source) => {
const {location, router} = this.props
const {redirectPath} = location.query
if (!redirectPath) {
return router.push(`/sources/${source.id}/hosts`)
}
const fixedPath = redirectPath.replace(
/\/sources\/[^/]*/,
`/sources/${source.id}`
)
return router.push(fixedPath)
}
private parseError = error => {
return getDeep<string>(error, 'data.message', '')
}
}
const mapDispatchToProps = dispatch => ({
@ -299,4 +281,4 @@ const mapDispatchToProps = dispatch => ({
addSource: bindActionCreators(addSourceAction, dispatch),
updateSource: bindActionCreators(updateSourceAction, dispatch),
})
export default connect(null, mapDispatchToProps)(withRouter(SourcePage))
export default withRouter(connect(null, mapDispatchToProps)(SourcePage))

View File

@ -1,49 +0,0 @@
// he is a library for safely encoding and decoding HTML Entities
import he from 'he'
import {fetchJSONFeed as fetchJSONFeedAJAX} from 'src/status/apis'
import {notify} from 'src/shared/actions/notifications'
import {notifyJSONFeedFailed} from 'src/shared/copy/notifications'
import * as actionTypes from 'src/status/constants/actionTypes'
const fetchJSONFeedRequested = () => ({
type: actionTypes.FETCH_JSON_FEED_REQUESTED,
})
const fetchJSONFeedCompleted = data => ({
type: actionTypes.FETCH_JSON_FEED_COMPLETED,
payload: {data},
})
const fetchJSONFeedFailed = () => ({
type: actionTypes.FETCH_JSON_FEED_FAILED,
})
export const fetchJSONFeedAsync = url => async dispatch => {
dispatch(fetchJSONFeedRequested())
try {
const {data} = await fetchJSONFeedAJAX(url)
// data could be from a webpage, and thus would be HTML
if (typeof data === 'string' || !data) {
dispatch(fetchJSONFeedFailed())
} else {
// decode HTML entities from response text
const decodedData = {
...data,
items: data.items.map(item => {
item.title = he.decode(item.title)
item.content_text = he.decode(item.content_text)
return item
}),
}
dispatch(fetchJSONFeedCompleted(decodedData))
}
} catch (error) {
console.error(error)
dispatch(fetchJSONFeedFailed())
dispatch(notify(notifyJSONFeedFailed(url)))
}
}

View File

@ -7,6 +7,7 @@ import {notify} from 'src/shared/actions/notifications'
import {notifyJSONFeedFailed} from 'src/shared/copy/notifications'
import {JSONFeedData} from 'src/types'
import {AxiosResponse} from 'axios'
export enum ActionTypes {
FETCH_JSON_FEED_REQUESTED = 'FETCH_JSON_FEED_REQUESTED',
@ -52,7 +53,7 @@ export const fetchJSONFeedAsync = (url: string) => async (
): Promise<void> => {
dispatch(fetchJSONFeedRequested())
try {
const {data} = await fetchJSONFeedAJAX(url)
const {data} = (await fetchJSONFeedAJAX(url)) as AxiosResponse<JSONFeedData>
// data could be from a webpage, and thus would be HTML
if (typeof data === 'string' || !data) {
dispatch(fetchJSONFeedFailed())

View File

@ -1,9 +1,10 @@
import AJAX from 'src/utils/ajax'
import {JSONFeedData} from 'src/types'
const excludeBasepath = true // don't prefix route of external link with basepath/
export const fetchJSONFeed = url =>
AJAX(
export const fetchJSONFeed = (url: string) =>
AJAX<JSONFeedData>(
{
method: 'GET',
url,

View File

@ -1,101 +0,0 @@
import React, {Component} from 'react'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class GettingStarted extends Component {
constructor(props) {
super(props)
}
render() {
return (
<FancyScrollbar className="getting-started--container">
<div className="getting-started">
<div className="getting-started--cell intro">
<h5>
<span className="icon cubo-uniform" /> Welcome to Chronograf!
</h5>
<p>Follow the links below to explore Chronografs features.</p>
</div>
<div className="getting-started--cell">
<p>
<strong>Install the TICK Stack</strong>
<br />Save some time and use this handy tool to install the rest
of the stack:
</p>
<p>
<a href="https://github.com/influxdata/sandbox" target="_blank">
<span className="icon github" /> TICK Sandbox
</a>
</p>
</div>
<div className="getting-started--cell">
<p>
<strong>Guides</strong>
</p>
<p>
<a
href="https://docs.influxdata.com/chronograf/latest/guides/create-a-dashboard/"
target="_blank"
>
Create a Dashboard
</a>
<br />
<a
href="https://docs.influxdata.com/chronograf/latest/guides/create-a-kapacitor-alert/"
target="_blank"
>
Create a Kapacitor Alert
</a>
<br />
<a
href="https://docs.influxdata.com/chronograf/latest/guides/configure-kapacitor-event-handlers/"
target="_blank"
>
Configure Kapacitor Event Handlers
</a>
<br />
<a
href="https://docs.influxdata.com/chronograf/latest/guides/transition-web-admin-interface/"
target="_blank"
>
Transition from InfluxDB's Web Admin Interface
</a>
<br />
<a
href="https://docs.influxdata.com/chronograf/latest/guides/dashboard-template-variables/"
target="_blank"
>
Dashboard Template Variables
</a>
<br />
<a
href="https://docs.influxdata.com/chronograf/latest/guides/advanced-kapacitor/"
target="_blank"
>
Advanced Kapacitor Usage
</a>
</p>
</div>
<div className="getting-started--cell">
<p>
<strong>Questions & Comments</strong>
</p>
<p>
If you have any product feedback please open a GitHub issue and
we'll take a look. For any questions or other issues try posting
on our&nbsp;
<a href="https://community.influxdata.com/" target="_blank">
Community Forum
</a>.
</p>
</div>
</div>
</FancyScrollbar>
)
}
}
export default GettingStarted

View File

@ -1,61 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
const JSONFeedReader = ({data}) =>
data && data.items ? (
<div className="newsfeed">
{data.items
? data.items.map(
({
id,
date_published: datePublished,
url,
title,
author: {name},
image,
content_text: contentText,
}) => (
<div key={id} className="newsfeed--post">
<div className="newsfeed--date">
{`${moment(datePublished).format('MMM DD')}`}
</div>
<div className="newsfeed--post-title">
<a href={url} target="_blank">
<h6>{title}</h6>
</a>
<span>by {name}</span>
</div>
<div className="newsfeed--content">
{image ? <img src={image} /> : null}
<p>{contentText}</p>
</div>
</div>
)
)
: null}
</div>
) : null
const {arrayOf, shape, string} = PropTypes
JSONFeedReader.propTypes = {
data: shape({
items: arrayOf(
shape({
author: shape({
name: string.isRequired,
}).isRequired,
content_text: string.isRequired,
date_published: string.isRequired,
id: string.isRequired,
image: string,
title: string.isRequired,
url: string.isRequired,
})
),
}).isRequired,
}
export default JSONFeedReader

View File

@ -1,92 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {fetchJSONFeedAsync} from 'src/status/actions'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import JSONFeedReader from 'src/status/components/JSONFeedReader'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class NewsFeed extends Component {
constructor(props) {
super(props)
}
// TODO: implement shouldComponentUpdate based on fetching conditions
render() {
const {hasCompletedFetchOnce, isFetching, isFailed, data} = this.props
if (!hasCompletedFetchOnce) {
return isFailed ? (
<div className="graph-empty">
<p>Failed to load News Feed</p>
</div>
) : (
// TODO: Factor this out of here and AutoRefresh
<div className="graph-fetching">
<div className="graph-spinner" />
</div>
)
}
return (
<FancyScrollbar autoHide={false} className="newsfeed--container">
{isFetching ? (
// TODO: Factor this out of here and AutoRefresh
<div className="graph-panel__refreshing">
<div />
<div />
<div />
</div>
) : null}
{isFailed ? (
<div className="graph-empty">
<p>Failed to refresh News Feed</p>
</div>
) : null}
<JSONFeedReader data={data} />
</FancyScrollbar>
)
}
// TODO: implement interval polling a la AutoRefresh
componentDidMount() {
const {statusFeedURL, fetchJSONFeed} = this.props
fetchJSONFeed(statusFeedURL)
}
}
const {bool, func, shape, string} = PropTypes
NewsFeed.propTypes = {
hasCompletedFetchOnce: bool.isRequired,
isFetching: bool.isRequired,
isFailed: bool.isRequired,
data: shape(),
fetchJSONFeed: func.isRequired,
statusFeedURL: string,
}
const mapStateToProps = ({
links: {
external: {statusFeed: statusFeedURL},
},
JSONFeed: {hasCompletedFetchOnce, isFetching, isFailed, data},
}) => ({
hasCompletedFetchOnce,
isFetching,
isFailed,
data,
statusFeedURL,
})
const mapDispatchToProps = dispatch => ({
fetchJSONFeed: bindActionCreators(fetchJSONFeedAsync, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(NewsFeed)

View File

@ -1,6 +0,0 @@
export const SET_STATUS_PAGE_AUTOREFRESH = 'SET_STATUS_PAGE_AUTOREFRESH'
export const SET_STATUS_PAGE_TIME_RANGE = 'SET_STATUS_PAGE_TIME_RANGE'
export const FETCH_JSON_FEED_REQUESTED = 'FETCH_JSON_FEED_REQUESTED'
export const FETCH_JSON_FEED_COMPLETED = 'FETCH_JSON_FEED_COMPLETED'
export const FETCH_JSON_FEED_FAILED = 'FETCH_JSON_FEED_FAILED'

View File

@ -1 +0,0 @@
export const RECENT_ALERTS_LIMIT = 30

View File

@ -1,113 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import SourceIndicator from 'shared/components/SourceIndicator'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import LayoutRenderer from 'shared/components/LayoutRenderer'
import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
@ErrorHandling
class StatusPage extends Component {
constructor(props) {
super(props)
this.state = {
cells: fixtureStatusPageCells,
}
}
render() {
const {source, autoRefresh, timeRange} = this.props
const {cells} = this.state
const dashboardTime = {
id: 'dashtime',
tempVar: TEMP_VAR_DASHBOARD_TIME,
type: 'constant',
values: [
{
value: timeRange.lower,
type: 'constant',
selected: true,
},
],
}
const upperDashboardTime = {
id: 'upperdashtime',
tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: 'constant',
values: [
{
value: 'now()',
type: 'constant',
selected: true,
},
],
}
const templates = [dashboardTime, upperDashboardTime]
return (
<div className="page">
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Status</h1>
</div>
<div className="page-header__right">
<SourceIndicator />
</div>
</div>
</div>
<FancyScrollbar className="page-contents">
<div className="dashboard container-fluid full-width">
{cells.length ? (
<LayoutRenderer
autoRefresh={autoRefresh}
timeRange={timeRange}
cells={cells}
templates={templates}
source={source}
shouldNotBeEditable={true}
isStatusPage={true}
isEditable={false}
/>
) : (
<span>Loading Status Page...</span>
)}
</div>
</FancyScrollbar>
</div>
)
}
}
const {number, shape, string} = PropTypes
StatusPage.propTypes = {
source: shape({
name: string.isRequired,
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
autoRefresh: number.isRequired,
timeRange: shape({
lower: string.isRequired,
}).isRequired,
}
const mapStateToProps = ({statusUI: {autoRefresh, timeRange}}) => ({
autoRefresh,
timeRange,
})
export default connect(mapStateToProps, null)(StatusPage)

View File

@ -1,9 +1,10 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import SourceIndicator from 'src/shared/components/SourceIndicator'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import LayoutRenderer from 'src/shared/components/LayoutRenderer'
import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges'
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -11,7 +12,7 @@ import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
import {Source, TimeRange, Cell} from 'src/types'
import {Source, Cell} from 'src/types'
interface State {
cells: Cell[]
@ -19,10 +20,11 @@ interface State {
interface Props {
source: Source
autoRefresh: number
timeRange: TimeRange
}
const autoRefresh = AUTOREFRESH_DEFAULT
const timeRange = STATUS_PAGE_TIME_RANGE
@ErrorHandling
class StatusPage extends Component<Props, State> {
constructor(props: Props) {
@ -34,7 +36,7 @@ class StatusPage extends Component<Props, State> {
}
public render() {
const {source, autoRefresh, timeRange} = this.props
const {source} = this.props
const {cells} = this.state
const dashboardTime = {
@ -100,9 +102,4 @@ class StatusPage extends Component<Props, State> {
}
}
const mstp = ({statusUI: {autoRefresh, timeRange}}) => ({
autoRefresh,
timeRange,
})
export default connect(mstp, null)(StatusPage)
export default StatusPage

View File

@ -1,9 +1,40 @@
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants/index'
import {DEFAULT_AXIS} from 'src/dashboards/constants/cellEditor'
import {Cell, CellQuery} from 'src/types'
import {CellType} from 'src/types/dashboard'
export const fixtureStatusPageCells = [
const emptyQuery: CellQuery = {
query: '',
source: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
}
const emptyAxes = {
axes: {
x: DEFAULT_AXIS,
y: DEFAULT_AXIS,
y2: DEFAULT_AXIS,
},
}
export const fixtureStatusPageCells: Cell[] = [
{
...NEW_DEFAULT_DASHBOARD_CELL,
...emptyAxes,
i: 'alerts-bar-graph',
type: CellType.Bar,
isWidget: false,
x: 0,
y: 0,
@ -15,7 +46,7 @@ export const fixtureStatusPageCells = [
queries: [
{
query: `SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > ${TEMP_VAR_DASHBOARD_TIME} GROUP BY time(1d)`,
label: 'Events',
source: '',
queryConfig: {
database: 'chronograf',
measurement: 'alerts',
@ -44,90 +75,56 @@ export const fixtureStatusPageCells = [
},
},
],
type: 'bar',
links: {
self: '/chronograf/v1/status/23/cells/c-bar-graphs-fly',
},
},
{
...NEW_DEFAULT_DASHBOARD_CELL,
...emptyAxes,
i: 'recent-alerts',
type: CellType.Alerts,
isWidget: true,
name: 'Alerts Last 30 Days',
type: 'alerts',
x: 0,
y: 5,
w: 6.5,
h: 6,
legend: {},
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
queries: [emptyQuery],
colors: DEFAULT_LINE_COLORS,
links: {self: ''},
},
{
...NEW_DEFAULT_DASHBOARD_CELL,
...emptyAxes,
i: 'news-feed',
type: CellType.News,
isWidget: true,
name: 'News Feed',
type: 'news',
x: 6.5,
y: 5,
w: 3,
h: 6,
legend: {},
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
queries: [emptyQuery],
colors: DEFAULT_LINE_COLORS,
links: {self: ''},
},
{
...NEW_DEFAULT_DASHBOARD_CELL,
...emptyAxes,
i: 'getting-started',
type: CellType.Guide,
isWidget: true,
name: 'Getting Started',
type: 'guide',
x: 9.5,
y: 5,
w: 2.5,
h: 6,
legend: {},
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
queries: [emptyQuery],
colors: DEFAULT_LINE_COLORS,
links: {self: ''},
},
]

View File

@ -1,43 +0,0 @@
import * as actionTypes from 'src/status/constants/actionTypes'
const initialState = {
hasCompletedFetchOnce: false,
isFetching: false,
isFailed: false,
data: null,
}
const JSONFeedReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_JSON_FEED_REQUESTED: {
return {...state, isFetching: true, isFailed: false}
}
case actionTypes.FETCH_JSON_FEED_COMPLETED: {
const {data} = action.payload
return {
...state,
hasCompletedFetchOnce: true,
isFetching: false,
isFailed: false,
data,
}
}
case actionTypes.FETCH_JSON_FEED_FAILED: {
return {
...state,
isFetching: false,
isFailed: true,
data: null,
}
}
default: {
return state
}
}
}
export default JSONFeedReducer

View File

@ -1,7 +0,0 @@
import statusUI from './ui'
import JSONFeed from './JSONFeed'
export default {
statusUI,
JSONFeed,
}

View File

@ -1,36 +0,0 @@
import {AUTOREFRESH_DEFAULT} from 'shared/constants'
import {timeRanges} from 'shared/data/timeRanges'
import * as actionTypes from 'src/status/constants/actionTypes'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 30d')
const initialState = {
autoRefresh: AUTOREFRESH_DEFAULT,
timeRange: {lower, upper},
}
const statusUI = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_STATUS_PAGE_AUTOREFRESH: {
const {milliseconds} = action.payload
return {
...state,
autoRefresh: milliseconds,
}
}
case actionTypes.SET_STATUS_PAGE_TIME_RANGE: {
const {timeRange} = action.payload
return {...state, timeRange}
}
default: {
return state
}
}
}
export default statusUI

View File

@ -3,12 +3,12 @@ import {ColorString} from 'src/types/colors'
import {Template} from 'src/types/tempVars'
export interface Axis {
bounds: [string, string]
label: string
prefix: string
suffix: string
base: string
scale: string
bounds?: [string, string]
}
export type TimeSeriesValue = string | number | null | undefined
@ -76,6 +76,7 @@ export interface Cell {
decimalPlaces: DecimalPlaces
links: CellLinks
legend: Legend
isWidget?: boolean
}
export enum CellType {

View File

@ -40,6 +40,7 @@ import {
DygraphClass,
DygraphData,
} from './dygraphs'
import {JSONFeedData} from './status'
export {
Me,
@ -96,4 +97,5 @@ export {
SchemaFilter,
RemoteDataState,
URLQueryParams,
JSONFeedData,
}