Merge pull request #1608 from influxdata/feature/status_page-1556
Add Status Page (incl. Alert Events, Recent Alerts, News Feed, and Getting Started guide)pull/1627/head
commit
7f54f987d3
|
@ -7,6 +7,7 @@
|
|||
|
||||
### Features
|
||||
1. [#1512](https://github.com/influxdata/chronograf/pull/1512): Synchronize vertical crosshair at same time across all graphs in a dashboard
|
||||
1. [#1608](https://github.com/influxdata/chronograf/pull/1608): Add a Status Page with Recent Alerts bar graph, Recent Alerts table, News Feed, and Getting Started widgets
|
||||
|
||||
### UI Improvements
|
||||
1. [#1512](https://github.com/influxdata/chronograf/pull/1512): When dashboard time range is changed, reset graphs that are zoomed in
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"he": "^1.1.1"
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ type MuxOpts struct {
|
|||
UseAuth bool // UseAuth turns on Github OAuth and JWT
|
||||
Auth oauth2.Authenticator // Auth is used to authenticate and authorize
|
||||
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
|
||||
StatusFeedURL string // JSON Feed URL for the client Status page News Feed
|
||||
}
|
||||
|
||||
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
||||
|
@ -181,7 +182,8 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.DropRetentionPolicy)
|
||||
|
||||
allRoutes := &AllRoutes{
|
||||
Logger: opts.Logger,
|
||||
Logger: opts.Logger,
|
||||
StatusFeed: opts.StatusFeedURL,
|
||||
}
|
||||
|
||||
router.Handler("GET", "/chronograf/v1/", allRoutes)
|
||||
|
|
|
@ -29,20 +29,27 @@ func (r *AuthRoutes) Lookup(provider string) (AuthRoute, bool) {
|
|||
}
|
||||
|
||||
type getRoutesResponse struct {
|
||||
Layouts string `json:"layouts"` // Location of the layouts endpoint
|
||||
Mappings string `json:"mappings"` // Location of the application mappings endpoint
|
||||
Sources string `json:"sources"` // Location of the sources endpoint
|
||||
Me string `json:"me"` // Location of the me endpoint
|
||||
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
|
||||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
Layouts string `json:"layouts"` // Location of the layouts endpoint
|
||||
Mappings string `json:"mappings"` // Location of the application mappings endpoint
|
||||
Sources string `json:"sources"` // Location of the sources endpoint
|
||||
Me string `json:"me"` // Location of the me endpoint
|
||||
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
|
||||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
|
||||
}
|
||||
|
||||
// AllRoutes is a handler that returns all links to resources in Chronograf server.
|
||||
type getExternalLinksResponse struct {
|
||||
StatusFeed *string `json:"statusFeed,omitempty"` // Location of the a JSON Feed for client's Status page News Feed
|
||||
}
|
||||
|
||||
// AllRoutes is a handler that returns all links to resources in Chronograf server, as well as
|
||||
// external links for the client to know about, such as for JSON feeds or custom side nav buttons.
|
||||
// Optionally, routes for authentication can be returned.
|
||||
type AllRoutes struct {
|
||||
AuthRoutes []AuthRoute // Location of all auth routes. If no auth, this can be empty.
|
||||
LogoutLink string // Location of the logout route for all auth routes. If no auth, this can be empty.
|
||||
StatusFeed string // External link to the JSON Feed for the News Feed on the client's Status Page
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
|
@ -55,6 +62,9 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
Mappings: "/chronograf/v1/mappings",
|
||||
Dashboards: "/chronograf/v1/dashboards",
|
||||
Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
|
||||
ExternalLinks: getExternalLinksResponse{
|
||||
StatusFeed: &a.StatusFeed,
|
||||
},
|
||||
}
|
||||
|
||||
// The JSON response will have no field present for the LogoutLink if there is no logout link.
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[]}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -67,9 +67,38 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout"}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllRoutesWithExternalLinks(t *testing.T) {
|
||||
statusFeedURL := "http://pineapple.life/feed.json"
|
||||
logger := log.New(log.DebugLevel)
|
||||
handler := &AllRoutes{
|
||||
StatusFeed: statusFeedURL,
|
||||
Logger: logger,
|
||||
}
|
||||
req := httptest.NewRequest("GET", "http://docbrowns-inventions.com", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to retrieve body")
|
||||
}
|
||||
var routes getRoutesResponse
|
||||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ type Server struct {
|
|||
GenericTokenURL string `long:"generic-token-url" description:"OAuth 2.0 provider's token endpoint URL" env:"GENERIC_TOKEN_URL"`
|
||||
GenericAPIURL string `long:"generic-api-url" description:"URL that returns OpenID UserInfo compatible information." env:"GENERIC_API_URL"`
|
||||
|
||||
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"`
|
||||
|
||||
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
|
||||
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
|
||||
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
|
||||
|
@ -258,6 +260,7 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
ProviderFuncs: providerFuncs,
|
||||
Basepath: basepath,
|
||||
PrefixRoutes: s.PrefixRoutes,
|
||||
StatusFeedURL: s.StatusFeedURL,
|
||||
}, service)
|
||||
|
||||
// Add chronograf's version header to all requests
|
||||
|
|
|
@ -4106,6 +4106,17 @@
|
|||
"description": "location of the dashboards endpoint",
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
},
|
||||
"external": {
|
||||
"description": "external links provided to client, ex. status feed URL",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"statusFeed": {
|
||||
"description": "link to a JSON Feed for the News Feed on client's Status Page",
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
@ -4113,7 +4124,10 @@
|
|||
"mappings": "/chronograf/v1/mappings",
|
||||
"sources": "/chronograf/v1/sources",
|
||||
"me": "/chronograf/v1/me",
|
||||
"dashboards": "/chronograf/v1/dashboards"
|
||||
"dashboards": "/chronograf/v1/dashboards",
|
||||
"external": {
|
||||
"statusFeed": "http://news.influxdata.com/feed.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Link": {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import linksReducer from 'shared/reducers/links'
|
||||
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
import {noop} from 'shared/actions/app'
|
||||
|
||||
const links = {
|
||||
layouts: '/chronograf/v1/layouts',
|
||||
mappings: '/chronograf/v1/mappings',
|
||||
sources: '/chronograf/v1/sources',
|
||||
me: '/chronograf/v1/me',
|
||||
dashboards: '/chronograf/v1/dashboards',
|
||||
auth: [
|
||||
{
|
||||
name: 'github',
|
||||
label: 'Github',
|
||||
login: '/oauth/github/login',
|
||||
logout: '/oauth/github/logout',
|
||||
callback: '/oauth/github/callback',
|
||||
},
|
||||
],
|
||||
logout: '/oauth/logout',
|
||||
external: {statusFeed: 'http://pineapple.life'},
|
||||
}
|
||||
|
||||
describe('Shared.Reducers.linksReducer', () => {
|
||||
it('can handle LINKS_RECEIVED', () => {
|
||||
const initial = linksReducer(undefined, noop())
|
||||
const actual = linksReducer(initial, linksReceived(links))
|
||||
const expected = links
|
||||
|
||||
expect(_.isEqual(actual, expected)).to.equal(true)
|
||||
})
|
||||
})
|
|
@ -9,6 +9,8 @@ import {showDatabases} from 'shared/apis/metaQuery'
|
|||
import {loadSources as loadSourcesAction} from 'shared/actions/sources'
|
||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
|
||||
import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
||||
|
||||
// Acts as a 'router middleware'. The main `App` component is responsible for
|
||||
// getting the list of data nodes, but not every page requires them to function.
|
||||
// Routes that do require data nodes can be nested under this component.
|
||||
|
@ -88,7 +90,7 @@ const CheckSources = React.createClass({
|
|||
|
||||
if (!isFetching && !source) {
|
||||
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
||||
const restString = rest === null ? 'hosts' : rest[1]
|
||||
const restString = rest === null ? DEFAULT_HOME_PAGE : rest[1]
|
||||
|
||||
if (defaultSource) {
|
||||
return router.push(`/sources/${defaultSource.id}/${restString}`)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
export function getAlerts(source, timeRange) {
|
||||
return proxy({
|
||||
export const getAlerts = (source, timeRange, limit) =>
|
||||
proxy({
|
||||
source,
|
||||
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc`,
|
||||
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc ${limit
|
||||
? `LIMIT ${limit}`
|
||||
: ''}`,
|
||||
db: 'chronograf',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,36 +1,27 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
const AlertsTable = React.createClass({
|
||||
propTypes: {
|
||||
alerts: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
time: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
host: PropTypes.string,
|
||||
level: PropTypes.string,
|
||||
})
|
||||
),
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
class AlertsTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
filteredAlerts: this.props.alerts,
|
||||
sortDirection: null,
|
||||
sortKey: null,
|
||||
}
|
||||
},
|
||||
|
||||
this.filterAlerts = ::this.filterAlerts
|
||||
this.changeSort = ::this.changeSort
|
||||
this.sortableClasses = ::this.sortableClasses
|
||||
this.sort = ::this.sort
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.filterAlerts(this.state.searchTerm, newProps.alerts)
|
||||
},
|
||||
}
|
||||
|
||||
filterAlerts(searchTerm, newAlerts) {
|
||||
const alerts = newAlerts || this.props.alerts
|
||||
|
@ -47,7 +38,7 @@ const AlertsTable = React.createClass({
|
|||
)
|
||||
})
|
||||
this.setState({searchTerm, filteredAlerts})
|
||||
},
|
||||
}
|
||||
|
||||
changeSort(key) {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
|
@ -59,7 +50,17 @@ const AlertsTable = React.createClass({
|
|||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
sortableClasses(key) {
|
||||
if (this.state.sortKey === key) {
|
||||
if (this.state.sortDirection === 'asc') {
|
||||
return 'sortable-header sorting-ascending'
|
||||
}
|
||||
return 'sortable-header sorting-descending'
|
||||
}
|
||||
return 'sortable-header'
|
||||
}
|
||||
|
||||
sort(alerts, key, direction) {
|
||||
switch (direction) {
|
||||
|
@ -70,125 +71,167 @@ const AlertsTable = React.createClass({
|
|||
default:
|
||||
return alerts
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id} = this.props.source
|
||||
renderTable() {
|
||||
const {source: {id}} = this.props
|
||||
const alerts = this.sort(
|
||||
this.state.filteredAlerts,
|
||||
this.state.sortKey,
|
||||
this.state.sortDirection
|
||||
)
|
||||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
{this.props.alerts.length
|
||||
? <SearchBar onSearch={this.filterAlerts} />
|
||||
return this.props.alerts.length
|
||||
? <table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
onClick={() => this.changeSort('name')}
|
||||
className={this.sortableClasses('name')}
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('level')}
|
||||
className={this.sortableClasses('level')}
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('time')}
|
||||
className={this.sortableClasses('time')}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('host')}
|
||||
className={this.sortableClasses('host')}
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('value')}
|
||||
className={this.sortableClasses('value')}
|
||||
>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alerts.map(({name, level, time, host, value}) => {
|
||||
return (
|
||||
<tr key={`${name}-${level}-${time}-${host}-${value}`}>
|
||||
<td className="monotype">{name}</td>
|
||||
<td className={`monotype alert-level-${level.toLowerCase()}`}>
|
||||
{level}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
{new Date(Number(time)).toISOString()}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<Link to={`/sources/${id}/hosts/${host}`}>
|
||||
{host}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="monotype">{value}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
: this.renderTableEmpty()
|
||||
}
|
||||
|
||||
renderTableEmpty() {
|
||||
const {source: {id}, shouldNotBeFilterable} = this.props
|
||||
|
||||
return shouldNotBeFilterable
|
||||
? <div className="graph-empty">
|
||||
<p>
|
||||
Learn how to configure your first <strong>Rule</strong> in<br />
|
||||
the <em>Getting Started</em> guide
|
||||
</p>
|
||||
</div>
|
||||
: <div className="generic-empty-state">
|
||||
<h4 className="no-user-select">
|
||||
There are no Alerts to display
|
||||
</h4>
|
||||
<br />
|
||||
<h6 className="no-user-select">
|
||||
Try changing the Time Range or
|
||||
<Link
|
||||
style={{marginLeft: '10px'}}
|
||||
to={`/sources/${id}/alert-rules/new`}
|
||||
className="btn btn-primary btn-sm"
|
||||
>
|
||||
Create an Alert Rule
|
||||
</Link>
|
||||
</h6>
|
||||
</div>
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
shouldNotBeFilterable,
|
||||
limit,
|
||||
onGetMoreAlerts,
|
||||
isAlertsMaxedOut,
|
||||
alertsCount,
|
||||
} = this.props
|
||||
|
||||
return shouldNotBeFilterable
|
||||
? <div className="alerts-widget">
|
||||
{this.renderTable()}
|
||||
{limit && alertsCount
|
||||
? <button
|
||||
className="btn btn-sm btn-default btn-block"
|
||||
onClick={onGetMoreAlerts}
|
||||
disabled={isAlertsMaxedOut}
|
||||
style={{marginBottom: '20px'}}
|
||||
>
|
||||
{isAlertsMaxedOut
|
||||
? `All ${alertsCount} Alerts displayed`
|
||||
: 'Load next 30 Alerts'}
|
||||
</button>
|
||||
: null}
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{this.props.alerts.length
|
||||
? <table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
onClick={() => this.changeSort('name')}
|
||||
className="sortable-header"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('level')}
|
||||
className="sortable-header"
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('time')}
|
||||
className="sortable-header"
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('host')}
|
||||
className="sortable-header"
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.changeSort('value')}
|
||||
className="sortable-header"
|
||||
>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alerts.map(({name, level, time, host, value}) => {
|
||||
return (
|
||||
<tr key={`${name}-${level}-${time}-${host}-${value}`}>
|
||||
<td className="monotype">{name}</td>
|
||||
<td
|
||||
className={`monotype alert-level-${level.toLowerCase()}`}
|
||||
>
|
||||
{level}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
{new Date(Number(time)).toISOString()}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<Link to={`/sources/${id}/hosts/${host}`}>
|
||||
{host}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="monotype">{value}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
: <div className="generic-empty-state">
|
||||
<h5 className="no-user-select">
|
||||
Alerts appear here when you have Rules
|
||||
</h5>
|
||||
<br />
|
||||
<Link
|
||||
to={`/sources/${id}/alert-rules/new`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Create a Rule
|
||||
</Link>
|
||||
</div>}
|
||||
: <div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
{this.props.alerts.length
|
||||
? <SearchBar onSearch={this.filterAlerts} />
|
||||
: null}
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const SearchBar = React.createClass({
|
||||
propTypes: {
|
||||
onSearch: PropTypes.func.isRequired,
|
||||
},
|
||||
class SearchBar extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
},
|
||||
|
||||
this.handleSearch = ::this.handleSearch
|
||||
this.handleChange = ::this.handleChange
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const waitPeriod = 300
|
||||
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
|
||||
},
|
||||
}
|
||||
|
||||
handleSearch() {
|
||||
this.props.onSearch(this.state.searchTerm)
|
||||
},
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({searchTerm: e.target.value}, this.handleSearch)
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -205,7 +248,34 @@ const SearchBar = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
AlertsTable.propTypes = {
|
||||
alerts: arrayOf(
|
||||
shape({
|
||||
name: string,
|
||||
time: string,
|
||||
value: string,
|
||||
host: string,
|
||||
level: string,
|
||||
})
|
||||
),
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
shouldNotBeFilterable: bool,
|
||||
limit: number,
|
||||
onGetMoreAlerts: func,
|
||||
isAlertsMaxedOut: bool,
|
||||
alertsCount: number,
|
||||
}
|
||||
|
||||
SearchBar.propTypes = {
|
||||
onSearch: func.isRequired,
|
||||
}
|
||||
|
||||
export default AlertsTable
|
||||
|
|
|
@ -6,15 +6,24 @@ import NoKapacitorError from 'shared/components/NoKapacitorError'
|
|||
import CustomTimeRangeDropdown from 'shared/components/CustomTimeRangeDropdown'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {getAlerts} from '../apis'
|
||||
import {getAlerts} from 'src/alerts/apis'
|
||||
import AJAX from 'utils/ajax'
|
||||
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
|
||||
class AlertsApp extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const lowerInSec = props.timeRange
|
||||
? timeRanges.find(tr => tr.lower === props.timeRange.lower).seconds
|
||||
: undefined
|
||||
|
||||
const oneDayInSec = 86400
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
hasKapacitor: false,
|
||||
|
@ -22,12 +31,16 @@ class AlertsApp extends Component {
|
|||
isTimeOpen: false,
|
||||
timeRange: {
|
||||
upper: moment().format(),
|
||||
lower: moment().subtract(1, 'd').format(),
|
||||
lower: moment().subtract(lowerInSec || oneDayInSec, 'seconds').format(),
|
||||
},
|
||||
limit: props.limit || 0, // only used if AlertsApp receives a limit prop
|
||||
limitMultiplier: 1, // only used if AlertsApp receives a limit prop
|
||||
isAlertsMaxedOut: false, // only used if AlertsApp receives a limit prop
|
||||
}
|
||||
|
||||
this.fetchAlerts = ::this.fetchAlerts
|
||||
this.renderSubComponents = ::this.renderSubComponents
|
||||
this.handleGetMoreAlerts = ::this.handleGetMoreAlerts
|
||||
this.handleToggleTime = ::this.handleToggleTime
|
||||
this.handleCloseTime = ::this.handleCloseTime
|
||||
this.handleApplyTime = ::this.handleApplyTime
|
||||
|
@ -59,7 +72,8 @@ class AlertsApp extends Component {
|
|||
fetchAlerts() {
|
||||
getAlerts(
|
||||
this.props.source.links.proxy,
|
||||
this.state.timeRange
|
||||
this.state.timeRange,
|
||||
this.state.limit * this.state.limitMultiplier
|
||||
).then(resp => {
|
||||
const results = []
|
||||
|
||||
|
@ -90,23 +104,39 @@ class AlertsApp extends Component {
|
|||
name: `${s[nameIndex]}`,
|
||||
})
|
||||
})
|
||||
this.setState({loading: false, alerts: results})
|
||||
|
||||
// TODO: factor these setStates out to make a pure function and implement true limit & offset
|
||||
this.setState({
|
||||
loading: false,
|
||||
alerts: results,
|
||||
// this.state.alerts.length === results.length ||
|
||||
isAlertsMaxedOut:
|
||||
results.length !== this.props.limit * this.state.limitMultiplier,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleGetMoreAlerts() {
|
||||
this.setState({limitMultiplier: this.state.limitMultiplier + 1}, () => {
|
||||
this.fetchAlerts(this.state.limitMultiplier)
|
||||
})
|
||||
}
|
||||
|
||||
renderSubComponents() {
|
||||
const {source} = this.props
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
{this.state.hasKapacitor
|
||||
? <AlertsTable source={source} alerts={this.state.alerts} />
|
||||
: <NoKapacitorError source={source} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const {source, isWidget, limit} = this.props
|
||||
const {isAlertsMaxedOut, alerts} = this.state
|
||||
|
||||
return this.state.hasKapacitor
|
||||
? <AlertsTable
|
||||
source={source}
|
||||
alerts={this.state.alerts}
|
||||
shouldNotBeFilterable={isWidget}
|
||||
limit={limit}
|
||||
onGetMoreAlerts={this.handleGetMoreAlerts}
|
||||
isAlertsMaxedOut={isAlertsMaxedOut}
|
||||
alertsCount={alerts.length}
|
||||
/>
|
||||
: <NoKapacitorError source={source} />
|
||||
}
|
||||
|
||||
handleToggleTime() {
|
||||
|
@ -122,43 +152,51 @@ class AlertsApp extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {source} = this.props
|
||||
const {isWidget, source} = this.props
|
||||
const {loading, timeRange} = this.state
|
||||
|
||||
if (loading || !source) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Alert History
|
||||
</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
<CustomTimeRangeDropdown
|
||||
isVisible={this.state.isTimeOpen}
|
||||
onToggle={this.handleToggleTime}
|
||||
onClose={this.handleCloseTime}
|
||||
onApplyTimeRange={this.handleApplyTime}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FancyScrollbar className="page-contents">
|
||||
return isWidget
|
||||
? <FancyScrollbar autoHide={false}>
|
||||
{this.renderSubComponents()}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
: <div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Alert History
|
||||
</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
<CustomTimeRangeDropdown
|
||||
isVisible={this.state.isTimeOpen}
|
||||
onToggle={this.handleToggleTime}
|
||||
onClose={this.handleCloseTime}
|
||||
onApplyTimeRange={this.handleApplyTime}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
{this.renderSubComponents()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {bool, number, oneOfType, shape, string} = PropTypes
|
||||
|
||||
AlertsApp.propTypes = {
|
||||
source: shape({
|
||||
|
@ -169,7 +207,12 @@ AlertsApp.propTypes = {
|
|||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
addFlashMessage: func,
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
upper: oneOfType([shape(), string]),
|
||||
}),
|
||||
isWidget: bool,
|
||||
limit: number,
|
||||
}
|
||||
|
||||
export default AlertsApp
|
||||
|
|
|
@ -64,7 +64,7 @@ const Dashboard = ({
|
|||
cells={cells}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
source={source.links.proxy}
|
||||
source={source}
|
||||
onPositionChange={onPositionChange}
|
||||
onEditCell={onEditCell}
|
||||
onRenameCell={onRenameCell}
|
||||
|
|
|
@ -193,12 +193,9 @@ class DashboardPage extends Component {
|
|||
|
||||
handleEditTemplateVariables(templates, onSaveTemplatesSuccess) {
|
||||
return async () => {
|
||||
const {params: {dashboardID}, dashboards} = this.props
|
||||
const currentDashboard = dashboards.find(({id}) => id === +dashboardID)
|
||||
|
||||
try {
|
||||
await this.props.dashboardActions.putDashboard({
|
||||
...currentDashboard,
|
||||
...this.getActiveDashboard(),
|
||||
templates,
|
||||
})
|
||||
onSaveTemplatesSuccess()
|
||||
|
@ -242,10 +239,10 @@ class DashboardPage extends Component {
|
|||
inPresentationMode,
|
||||
handleChooseAutoRefresh,
|
||||
handleClickPresentationButton,
|
||||
params: {sourceID, dashboardID},
|
||||
params: {sourceID},
|
||||
} = this.props
|
||||
|
||||
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
||||
const dashboard = this.getActiveDashboard()
|
||||
const dashboardTime = {
|
||||
id: 'dashtime',
|
||||
tempVar: ':dashboardTime:',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'lodash'
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
|
||||
const {lower, upper} = timeRanges[2]
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
dashboards: [],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import groupByTimeOptions from 'hson!../data/groupByTimes.hson'
|
||||
import groupByTimeOptions from 'hson!src/data_explorer/data/groupByTimes.hson'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
|
||||
const {lower, upper} = timeRanges[2]
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
upper,
|
||||
|
|
|
@ -47,12 +47,10 @@ export const HostPage = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState() {
|
||||
const timeRange = timeRanges[2]
|
||||
|
||||
return {
|
||||
layouts: [],
|
||||
hosts: [],
|
||||
timeRange,
|
||||
timeRange: timeRanges.find(tr => tr.lower === 'now() - 1h'),
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -155,7 +153,7 @@ export const HostPage = React.createClass({
|
|||
timeRange={timeRange}
|
||||
cells={layoutCells}
|
||||
autoRefresh={autoRefresh}
|
||||
source={source.links.proxy}
|
||||
source={source}
|
||||
host={this.props.params.hostID}
|
||||
shouldNotBeEditable={true}
|
||||
/>
|
||||
|
|
|
@ -5,24 +5,26 @@ import {Router, Route, useRouterHistory} from 'react-router'
|
|||
import {createHistory} from 'history'
|
||||
import {syncHistoryWithStore} from 'react-router-redux'
|
||||
|
||||
import configureStore from 'src/store/configureStore'
|
||||
import {loadLocalStorage} from 'src/localStorage'
|
||||
|
||||
import App from 'src/App'
|
||||
import AlertsApp from 'src/alerts'
|
||||
import CheckSources from 'src/CheckSources'
|
||||
import {HostsPage, HostPage} from 'src/hosts'
|
||||
import {Login, UserIsAuthenticated, UserIsNotAuthenticated} from 'src/auth'
|
||||
import CheckSources from 'src/CheckSources'
|
||||
import {StatusPage} from 'src/status'
|
||||
import {HostsPage, HostPage} from 'src/hosts'
|
||||
import DataExplorer from 'src/data_explorer'
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||
import AlertsApp from 'src/alerts'
|
||||
import {
|
||||
KapacitorPage,
|
||||
KapacitorRulePage,
|
||||
KapacitorRulesPage,
|
||||
KapacitorTasksPage,
|
||||
} from 'src/kapacitor'
|
||||
import DataExplorer from 'src/data_explorer'
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||
import {CreateSource, SourcePage, ManageSources} from 'src/sources'
|
||||
import {AdminPage} from 'src/admin'
|
||||
import {CreateSource, SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
import configureStore from 'src/store/configureStore'
|
||||
import {loadLocalStorage} from './localStorage'
|
||||
|
||||
import {getMe} from 'shared/apis'
|
||||
|
||||
|
@ -34,6 +36,7 @@ import {
|
|||
meReceived,
|
||||
logoutLinkReceived,
|
||||
} from 'shared/actions/auth'
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import 'src/style/chronograf.scss'
|
||||
|
@ -85,11 +88,13 @@ const Root = React.createClass({
|
|||
|
||||
async startHeartbeat({shouldDispatchResponse}) {
|
||||
try {
|
||||
const {data: me, auth, logoutLink} = await getMe()
|
||||
// These non-me objects are added to every response by some AJAX trickery
|
||||
const {data: me, auth, logoutLink, external} = await getMe()
|
||||
if (shouldDispatchResponse) {
|
||||
dispatch(authReceived(auth))
|
||||
dispatch(meReceived(me))
|
||||
dispatch(logoutLinkReceived(logoutLink))
|
||||
dispatch(linksReceived({external}))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -122,22 +127,23 @@ const Root = React.createClass({
|
|||
/>
|
||||
<Route path="/sources/:sourceID" component={UserIsAuthenticated(App)}>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
<Route path="chronograf/data-explorer" component={DataExplorer} />
|
||||
<Route path="status" component={StatusPage} />
|
||||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="chronograf/data-explorer" component={DataExplorer} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route path="dashboards/:dashboardID" component={DashboardPage} />
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="alert-rules" component={KapacitorRulesPage} />
|
||||
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
|
||||
<Route path="alert-rules/new" component={KapacitorRulePage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="admin" component={AdminPage} />
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
|
|
|
@ -29,9 +29,8 @@ export const KapacitorRule = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState() {
|
||||
const fifteenMinutesIndex = 1
|
||||
return {
|
||||
timeRange: timeRanges[fifteenMinutesIndex],
|
||||
timeRange: timeRanges.find(tr => tr.lower === 'now() - 15m'),
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import * as actionTypes from 'shared/constants/actionTypes'
|
||||
|
||||
export const linksReceived = links => ({
|
||||
type: actionTypes.LINKS_RECEIVED,
|
||||
payload: {links},
|
||||
})
|
|
@ -2,7 +2,7 @@ import React, {PropTypes} from 'react'
|
|||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
|
||||
import autoRefreshItems from 'hson!../data/autoRefreshes.hson'
|
||||
import autoRefreshItems from 'hson!shared/data/autoRefreshes.hson'
|
||||
|
||||
const {number, func} = PropTypes
|
||||
|
||||
|
|
|
@ -321,12 +321,13 @@ export default class Dygraph extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div style={{height: '100%'}}>
|
||||
<div className="dygraph-child">
|
||||
<div
|
||||
ref={r => {
|
||||
this.graphContainer = r
|
||||
}}
|
||||
style={this.props.containerStyle}
|
||||
className="dygraph-child-container"
|
||||
/>
|
||||
<div
|
||||
ref={r => {
|
||||
|
|
|
@ -1,56 +1,45 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||
import LineGraph from 'shared/components/LineGraph'
|
||||
import SingleStat from 'shared/components/SingleStat'
|
||||
import NameableGraph from 'shared/components/NameableGraph'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
|
||||
|
||||
import timeRanges from 'hson!../data/timeRanges.hson'
|
||||
import NameableGraph from 'shared/components/NameableGraph'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
import AlertsApp from 'src/alerts/containers/AlertsApp'
|
||||
import NewsFeed from 'src/status/components/NewsFeed'
|
||||
import GettingStarted from 'src/status/components/GettingStarted'
|
||||
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
import {
|
||||
// TODO: get these const values dynamically
|
||||
STATUS_PAGE_ROW_COUNT,
|
||||
PAGE_HEADER_HEIGHT,
|
||||
PAGE_CONTAINER_MARGIN,
|
||||
LAYOUT_MARGIN,
|
||||
DASHBOARD_LAYOUT_ROW_HEIGHT,
|
||||
} from 'shared/constants'
|
||||
import {RECENT_ALERTS_LIMIT} from 'src/status/constants'
|
||||
|
||||
const GridLayout = WidthProvider(ReactGridLayout)
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
||||
class LayoutRenderer extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
this.state = {
|
||||
rowHeight: this.calculateRowHeight(),
|
||||
}
|
||||
|
||||
export const LayoutRenderer = React.createClass({
|
||||
propTypes: {
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
}),
|
||||
cells: arrayOf(
|
||||
shape({
|
||||
queries: arrayOf(
|
||||
shape({
|
||||
label: string,
|
||||
text: string,
|
||||
query: string,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
x: number.isRequired,
|
||||
y: number.isRequired,
|
||||
w: number.isRequired,
|
||||
h: number.isRequired,
|
||||
i: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
templates: arrayOf(shape()),
|
||||
host: string,
|
||||
source: string,
|
||||
onPositionChange: func,
|
||||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
onDeleteCell: func,
|
||||
onSummonOverlayTechnologies: func,
|
||||
shouldNotBeEditable: bool,
|
||||
synchronizer: func,
|
||||
},
|
||||
this.buildQueryForOldQuerySchema = ::this.buildQueryForOldQuerySchema
|
||||
this.standardizeQueries = ::this.standardizeQueries
|
||||
this.generateWidgetCell = ::this.generateWidgetCell
|
||||
this.generateVisualizations = ::this.generateVisualizations
|
||||
this.handleLayoutChange = ::this.handleLayoutChange
|
||||
this.triggerWindowResize = ::this.triggerWindowResize
|
||||
this.calculateRowHeight = ::this.calculateRowHeight
|
||||
this.updateWindowDimensions = ::this.updateWindowDimensions
|
||||
}
|
||||
|
||||
buildQueryForOldQuerySchema(q) {
|
||||
const {timeRange: {lower}, host} = this.props
|
||||
|
@ -82,42 +71,59 @@ export const LayoutRenderer = React.createClass({
|
|||
}
|
||||
|
||||
return text
|
||||
},
|
||||
}
|
||||
|
||||
renderRefreshingGraph(type, queries, cellHeight) {
|
||||
const {timeRange, autoRefresh, templates, synchronizer} = this.props
|
||||
standardizeQueries(cell, source) {
|
||||
return cell.queries.map(query => {
|
||||
// TODO: Canned dashboards (and possibly Kubernetes dashboard) use an old query schema,
|
||||
// which does not have enough information for the new `buildInfluxQLQuery` function
|
||||
// to operate on. We will use `buildQueryForOldQuerySchema` until we conform
|
||||
// on a stable query representation.
|
||||
let queryText
|
||||
if (query.queryConfig) {
|
||||
const {queryConfig: {rawText, range}} = query
|
||||
const timeRange = range || {upper: null, lower: ':dashboardTime:'}
|
||||
queryText = rawText || buildInfluxQLQuery(timeRange, query.queryConfig)
|
||||
} else {
|
||||
queryText = this.buildQueryForOldQuerySchema(query)
|
||||
}
|
||||
|
||||
if (type === 'single-stat') {
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
queries={[queries[0]]}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
)
|
||||
return Object.assign({}, query, {
|
||||
host: source.links.proxy,
|
||||
text: queryText,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
generateWidgetCell(cell) {
|
||||
const {source, timeRange} = this.props
|
||||
|
||||
switch (cell.type) {
|
||||
case 'alerts': {
|
||||
return (
|
||||
<AlertsApp
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
isWidget={true}
|
||||
limit={RECENT_ALERTS_LIMIT}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'news': {
|
||||
return <NewsFeed source={source} />
|
||||
}
|
||||
case 'guide': {
|
||||
return <GettingStarted />
|
||||
}
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: type === 'line-stepplot',
|
||||
stackedGraph: type === 'line-stacked',
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
showSingleStat={type === 'line-plus-single-stat'}
|
||||
isBarGraph={type === 'bar'}
|
||||
displayOptions={displayOptions}
|
||||
synchronizer={synchronizer}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
<div className="graph-empty">
|
||||
<p>No Results</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// Generates cell contents based on cell type, i.e. graphs, news feeds, etc.
|
||||
generateVisualizations() {
|
||||
const {
|
||||
source,
|
||||
|
@ -128,29 +134,14 @@ export const LayoutRenderer = React.createClass({
|
|||
onDeleteCell,
|
||||
onSummonOverlayTechnologies,
|
||||
shouldNotBeEditable,
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
templates,
|
||||
synchronizer,
|
||||
} = this.props
|
||||
|
||||
return cells.map(cell => {
|
||||
const queries = cell.queries.map(query => {
|
||||
// TODO: Canned dashboards (and possibly Kubernetes dashboard) use an old query schema,
|
||||
// which does not have enough information for the new `buildInfluxQLQuery` function
|
||||
// to operate on. We will use `buildQueryForOldQuerySchema` until we conform
|
||||
// on a stable query representation.
|
||||
let queryText
|
||||
if (query.queryConfig) {
|
||||
const {queryConfig: {rawText, range}} = query
|
||||
const timeRange = range || {upper: null, lower: ':dashboardTime:'}
|
||||
queryText =
|
||||
rawText || buildInfluxQLQuery(timeRange, query.queryConfig)
|
||||
} else {
|
||||
queryText = this.buildQueryForOldQuerySchema(query)
|
||||
}
|
||||
|
||||
return Object.assign({}, query, {
|
||||
host: source,
|
||||
text: queryText,
|
||||
})
|
||||
})
|
||||
const {type, h} = cell
|
||||
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
|
@ -163,12 +154,22 @@ export const LayoutRenderer = React.createClass({
|
|||
shouldNotBeEditable={shouldNotBeEditable}
|
||||
cell={cell}
|
||||
>
|
||||
{this.renderRefreshingGraph(cell.type, queries, cell.h)}
|
||||
{cell.isWidget
|
||||
? this.generateWidgetCell(cell)
|
||||
: <RefreshingGraph
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
templates={templates}
|
||||
synchronizer={synchronizer}
|
||||
type={type}
|
||||
queries={this.standardizeQueries(cell, source)}
|
||||
cellHeight={h}
|
||||
/>}
|
||||
</NameableGraph>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleLayoutChange(layout) {
|
||||
this.triggerWindowResize()
|
||||
|
@ -184,18 +185,46 @@ export const LayoutRenderer = React.createClass({
|
|||
})
|
||||
|
||||
this.props.onPositionChange(newCells)
|
||||
},
|
||||
}
|
||||
|
||||
triggerWindowResize() {
|
||||
// Hack to get dygraphs to fit properly during and after resize (dispatchEvent is a global method on window).
|
||||
const evt = document.createEvent('CustomEvent') // MUST be 'CustomEvent'
|
||||
evt.initCustomEvent('resize', false, false, null)
|
||||
dispatchEvent(evt)
|
||||
}
|
||||
|
||||
// ensures that Status Page height fits the window
|
||||
calculateRowHeight() {
|
||||
const {isStatusPage} = this.props
|
||||
|
||||
return isStatusPage
|
||||
? (window.innerHeight -
|
||||
STATUS_PAGE_ROW_COUNT * LAYOUT_MARGIN -
|
||||
PAGE_HEADER_HEIGHT -
|
||||
PAGE_CONTAINER_MARGIN -
|
||||
PAGE_CONTAINER_MARGIN) /
|
||||
STATUS_PAGE_ROW_COUNT
|
||||
: DASHBOARD_LAYOUT_ROW_HEIGHT
|
||||
}
|
||||
|
||||
// idea adopted from https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs
|
||||
updateWindowDimensions() {
|
||||
this.setState({rowHeight: this.calculateRowHeight()})
|
||||
}
|
||||
|
||||
render() {
|
||||
const layoutMargin = 4
|
||||
const {cells} = this.props
|
||||
const {rowHeight} = this.state
|
||||
|
||||
const isDashboard = !!this.props.onPositionChange
|
||||
|
||||
return (
|
||||
<GridLayout
|
||||
layout={this.props.cells}
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={83.5}
|
||||
margin={[layoutMargin, layoutMargin]}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.triggerWindowResize}
|
||||
|
@ -207,14 +236,60 @@ export const LayoutRenderer = React.createClass({
|
|||
{this.generateVisualizations()}
|
||||
</GridLayout>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
triggerWindowResize() {
|
||||
// Hack to get dygraphs to fit properly during and after resize (dispatchEvent is a global method on window).
|
||||
const evt = document.createEvent('CustomEvent') // MUST be 'CustomEvent'
|
||||
evt.initCustomEvent('resize', false, false, null)
|
||||
dispatchEvent(evt)
|
||||
},
|
||||
})
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.updateWindowDimensions)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateWindowDimensions)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
LayoutRenderer.propTypes = {
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
}),
|
||||
cells: arrayOf(
|
||||
shape({
|
||||
// isWidget cells will not have queries
|
||||
isWidget: bool,
|
||||
queries: arrayOf(
|
||||
shape({
|
||||
label: string,
|
||||
text: string,
|
||||
query: string,
|
||||
}).isRequired
|
||||
),
|
||||
x: number.isRequired,
|
||||
y: number.isRequired,
|
||||
w: number.isRequired,
|
||||
h: number.isRequired,
|
||||
i: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
templates: arrayOf(shape()),
|
||||
host: string,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onPositionChange: func,
|
||||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
onDeleteCell: func,
|
||||
onSummonOverlayTechnologies: func,
|
||||
shouldNotBeEditable: bool,
|
||||
synchronizer: func,
|
||||
isStatusPage: bool,
|
||||
}
|
||||
|
||||
export default LayoutRenderer
|
||||
|
|
|
@ -11,12 +11,14 @@ const NoKapacitorError = React.createClass({
|
|||
render() {
|
||||
const path = `/sources/${this.props.source.id}/kapacitors/new`
|
||||
return (
|
||||
<div>
|
||||
<div className="graph-empty">
|
||||
<p>
|
||||
The current source does not have an associated Kapacitor instance,
|
||||
please configure one.
|
||||
The current source does not have an associated Kapacitor instance
|
||||
<br /><br />
|
||||
<Link to={path} className="btn btn-sm btn-primary">
|
||||
Configure Kapacitor
|
||||
</Link>
|
||||
</p>
|
||||
<Link to={path}>Add Kapacitor</Link>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||
import LineGraph from 'shared/components/LineGraph'
|
||||
import SingleStat from 'shared/components/SingleStat'
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
||||
|
||||
const RefreshingGraph = ({
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
templates,
|
||||
synchronizer,
|
||||
type,
|
||||
queries,
|
||||
cellHeight,
|
||||
}) => {
|
||||
if (type === 'single-stat') {
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
queries={[queries[0]]}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: type === 'line-stepplot',
|
||||
stackedGraph: type === 'line-stacked',
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
showSingleStat={type === 'line-plus-single-stat'}
|
||||
isBarGraph={type === 'bar'}
|
||||
displayOptions={displayOptions}
|
||||
synchronizer={synchronizer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
RefreshingGraph.propTypes = {
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
}),
|
||||
autoRefresh: number.isRequired,
|
||||
templates: arrayOf(shape()),
|
||||
synchronizer: func,
|
||||
type: string.isRequired,
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
cellHeight: number.isRequired,
|
||||
}
|
||||
|
||||
export default RefreshingGraph
|
|
@ -5,7 +5,7 @@ import moment from 'moment'
|
|||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import timeRanges from 'hson!../data/timeRanges.hson'
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
|
||||
|
||||
const TimeRangeDropdown = React.createClass({
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const TEMPLATE_VARIABLE_SELECTED = 'TEMPLATE_VARIABLE_SELECTED'
|
||||
|
||||
export const LINKS_RECEIVED = 'LINKS_RECEIVED'
|
||||
|
|
|
@ -403,3 +403,11 @@ export const VIS_VIEWS = [GRAPH, TABLE]
|
|||
// InfluxQL Macros
|
||||
export const TEMP_VAR_INTERVAL = ':interval:'
|
||||
export const DEFAULT_DASHBOARD_GROUP_BY_INTERVAL = 'auto'
|
||||
|
||||
export const DEFAULT_HOME_PAGE = 'status'
|
||||
|
||||
export const STATUS_PAGE_ROW_COUNT = 10 // TODO: calculate based on actual Status Page cells
|
||||
export const PAGE_HEADER_HEIGHT = 60 // TODO: get this dynamically to ensure longevity
|
||||
export const PAGE_CONTAINER_MARGIN = 30 // TODO: get this dynamically to ensure longevity
|
||||
export const LAYOUT_MARGIN = 4
|
||||
export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
{defaultGroupBy: '30m', seconds: 172800, inputValue: 'Past 2 days', lower: 'now() - 2d', upper: null, menuOption: 'Past 2 days'},
|
||||
{defaultGroupBy: '1h', seconds: 604800, inputValue: 'Past 7 days', lower: 'now() - 7d', upper: null, menuOption: 'Past 7 days'},
|
||||
{defaultGroupBy: '6h', seconds: 2592000, inputValue: 'Past 30 days', lower: 'now() - 30d', upper: null, menuOption: 'Past 30 days'},
|
||||
{defaultGroupBy: '12h', seconds: 7776000, inputValue: 'Past 90 days', lower: 'now() - 90d', upper: null, menuOption: 'Past 90 days'},
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import app from './app'
|
||||
import auth from './auth'
|
||||
import errors from './errors'
|
||||
import links from './links'
|
||||
import notifications from './notifications'
|
||||
import sources from './sources'
|
||||
|
||||
|
@ -8,6 +9,7 @@ export default {
|
|||
app,
|
||||
auth,
|
||||
errors,
|
||||
links,
|
||||
notifications,
|
||||
sources,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import * as actionTypes from 'shared/constants/actionTypes'
|
||||
|
||||
const initialState = {
|
||||
external: {statusFeed: ''},
|
||||
}
|
||||
|
||||
const linksReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actionTypes.LINKS_RECEIVED: {
|
||||
const {links} = action.payload
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default linksReducer
|
|
@ -9,6 +9,8 @@ import {
|
|||
NavListItem,
|
||||
} from 'src/side_nav/components/NavItems'
|
||||
|
||||
import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
||||
|
||||
const {bool, shape, string} = PropTypes
|
||||
|
||||
const SideNav = React.createClass({
|
||||
|
@ -38,9 +40,12 @@ const SideNav = React.createClass({
|
|||
return isHidden
|
||||
? null
|
||||
: <NavBar location={location}>
|
||||
<div className="sidebar__logo">
|
||||
<Link to="/"><span className="icon cubo-uniform" /></Link>
|
||||
</div>
|
||||
<Link
|
||||
to={`${sourcePrefix}/${DEFAULT_HOME_PAGE}`}
|
||||
className="sidebar__logo"
|
||||
>
|
||||
<span className="icon cubo-uniform" />
|
||||
</Link>
|
||||
<NavBlock icon="cubo-node" link={`${sourcePrefix}/hosts`}>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||
</NavBlock>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// he is a library for safely encoding and decoding HTML Entities
|
||||
import he from 'he'
|
||||
|
||||
import {fetchJSONFeed as fetchJSONFeedAJAX} from 'src/status/apis'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import * as actionTypes from 'src/status/constants/actionTypes'
|
||||
|
||||
import {HTTP_NOT_FOUND} from 'shared/constants'
|
||||
|
||||
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())
|
||||
if (error.status === HTTP_NOT_FOUND) {
|
||||
dispatch(
|
||||
errorThrown(
|
||||
error,
|
||||
`Failed to fetch News Feed. JSON Feed at '${url}' returned 404 (Not Found)`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
dispatch(errorThrown(error, 'Failed to fetch NewsFeed'))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
|
||||
export const fetchJSONFeed = url =>
|
||||
AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
// For explanation of why this header makes this work:
|
||||
// https://stackoverflow.com/questions/22968406/how-to-skip-the-options-preflight-request-in-angularjs
|
||||
headers: {'Content-Type': 'text/plain; charset=UTF-8'},
|
||||
})
|
|
@ -0,0 +1,95 @@
|
|||
import React, {Component} from 'react'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
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 Chronograf’s 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
|
||||
<a href="https://community.influxdata.com/" target="_blank">
|
||||
Community Forum
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GettingStarted
|
|
@ -0,0 +1,59 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
const JSONFeedReader = ({data}) =>
|
||||
data && data.items
|
||||
? <div className="newsfeed">
|
||||
{data.items
|
||||
? data.items.map(
|
||||
({
|
||||
id,
|
||||
date_published,
|
||||
url,
|
||||
title,
|
||||
author: {name},
|
||||
image,
|
||||
content_text: contentText,
|
||||
}) =>
|
||||
<div key={id} className="newsfeed--post">
|
||||
<div className="newsfeed--date">
|
||||
{`${moment(date_published).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
|
|
@ -0,0 +1,85 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
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'
|
||||
|
||||
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)
|
|
@ -0,0 +1,6 @@
|
|||
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'
|
|
@ -0,0 +1 @@
|
|||
export const RECENT_ALERTS_LIMIT = 30
|
|
@ -0,0 +1,91 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
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'
|
||||
|
||||
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: ':dashboardTime:',
|
||||
type: 'constant',
|
||||
values: [
|
||||
{
|
||||
value: timeRange.lower,
|
||||
type: 'constant',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
const templates = [dashboardTime]
|
||||
|
||||
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 sourceName={source.name} />
|
||||
</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}
|
||||
/>
|
||||
: <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)
|
|
@ -0,0 +1,71 @@
|
|||
export const fixtureStatusPageCells = [
|
||||
{
|
||||
i: 'alerts-bar-graph',
|
||||
isWidget: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 12,
|
||||
h: 4,
|
||||
name: 'Alert Events per Day – Last 30 Days',
|
||||
queries: [
|
||||
{
|
||||
query:
|
||||
'SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > :dashboardTime: GROUP BY time(1d)',
|
||||
label: 'Events',
|
||||
queryConfig: {
|
||||
database: 'chronograf',
|
||||
measurement: 'alerts',
|
||||
retentionPolicy: 'autogen',
|
||||
fields: [
|
||||
{
|
||||
field: 'value',
|
||||
funcs: ['count'],
|
||||
},
|
||||
],
|
||||
tags: {},
|
||||
groupBy: {
|
||||
time: '1d',
|
||||
tags: [],
|
||||
},
|
||||
areTagsAccepted: false,
|
||||
rawText: null,
|
||||
range: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'bar',
|
||||
links: {
|
||||
self: '/chronograf/v1/status/23/cells/c-bar-graphs-fly',
|
||||
},
|
||||
},
|
||||
{
|
||||
i: 'recent-alerts',
|
||||
isWidget: true,
|
||||
name: 'Alerts – Last 30 Days',
|
||||
type: 'alerts',
|
||||
x: 0,
|
||||
y: 5,
|
||||
w: 6.5,
|
||||
h: 6,
|
||||
},
|
||||
{
|
||||
i: 'news-feed',
|
||||
isWidget: true,
|
||||
name: 'News Feed',
|
||||
type: 'news',
|
||||
x: 6.5,
|
||||
y: 5,
|
||||
w: 3,
|
||||
h: 6,
|
||||
},
|
||||
{
|
||||
i: 'getting-started',
|
||||
isWidget: true,
|
||||
name: 'Getting Started',
|
||||
type: 'guide',
|
||||
x: 9.5,
|
||||
y: 5,
|
||||
w: 2.5,
|
||||
h: 6,
|
||||
},
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
import StatusPage from 'src/status/containers/StatusPage'
|
||||
|
||||
export {StatusPage}
|
|
@ -0,0 +1,43 @@
|
|||
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
|
|
@ -0,0 +1,7 @@
|
|||
import statusUI from './ui'
|
||||
import JSONFeed from './JSONFeed'
|
||||
|
||||
export default {
|
||||
statusUI,
|
||||
JSONFeed,
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {AUTOREFRESH_DEFAULT} from 'shared/constants'
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
|
||||
import * as actionTypes from 'src/status/constants/actionTypes'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 90d')
|
||||
|
||||
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
|
|
@ -5,14 +5,16 @@ import thunkMiddleware from 'redux-thunk'
|
|||
|
||||
import errorsMiddleware from 'shared/middleware/errors'
|
||||
import resizeLayout from 'shared/middleware/resizeLayout'
|
||||
import adminReducer from 'src/admin/reducers/admin'
|
||||
import statusReducers from 'src/status/reducers'
|
||||
import sharedReducers from 'shared/reducers'
|
||||
import dataExplorerReducers from 'src/data_explorer/reducers'
|
||||
import adminReducer from 'src/admin/reducers/admin'
|
||||
import rulesReducer from 'src/kapacitor/reducers/rules'
|
||||
import dashboardUI from 'src/dashboards/reducers/ui'
|
||||
import persistStateEnhancer from './persistStateEnhancer'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...statusReducers,
|
||||
...sharedReducers,
|
||||
...dataExplorerReducers,
|
||||
admin: adminReducer,
|
||||
|
|
|
@ -28,10 +28,13 @@
|
|||
@import 'components/confirm-buttons';
|
||||
@import 'components/custom-time-range';
|
||||
@import 'components/dygraphs';
|
||||
@import 'components/fancy-scrollbars';
|
||||
@import 'components/flip-toggle';
|
||||
@import 'components/function-selector';
|
||||
@import 'components/graph-tips';
|
||||
@import 'components/graph';
|
||||
@import 'components/input-tag-list';
|
||||
@import 'components/newsfeed';
|
||||
@import 'components/page-header-dropdown';
|
||||
@import 'components/page-header-editable';
|
||||
@import 'components/page-spinner';
|
||||
|
@ -42,8 +45,6 @@
|
|||
@import 'components/search-widget';
|
||||
@import 'components/source-indicator';
|
||||
@import 'components/tables';
|
||||
@import 'components/function-selector';
|
||||
@import 'components/fancy-scrollbars';
|
||||
|
||||
// Pages
|
||||
|
||||
|
|
|
@ -66,18 +66,18 @@ $graph-gutter: 16px;
|
|||
.graph-container {
|
||||
@include no-user-select();
|
||||
|
||||
& > div:not(.graph-panel__refreshing) {
|
||||
& > .dygraph {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
& > div:not(.graph-panel__refreshing) > div:not(.graph-panel__refreshing) {
|
||||
& > .dygraph > .dygraph-child {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
& > div:not(.graph-panel__refreshing) > div:not(.graph-panel__refreshing) > div:first-child {
|
||||
& > .dygraph > .dygraph-child > .dygraph-child-container {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Newsfeed Cell (Status Dash)
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
.newsfeed--container {
|
||||
.fancy-scroll--track-h {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.newsfeed {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.newsfeed--post {
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
background-color: $g2-kevlar;
|
||||
border-radius: 3px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.newsfeed--post-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: $g18-cloud;
|
||||
padding: 12px;
|
||||
padding-right: 76px;
|
||||
}
|
||||
.newsfeed--date {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 22px;
|
||||
line-height: 20px;
|
||||
background-color: $g3-castle;
|
||||
color: $g10-wolf;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
.newsfeed--content {
|
||||
padding: 12px;
|
||||
|
||||
/* Condensing default typography styles to better suit the context */
|
||||
p {margin-top: 0;}
|
||||
p, li {font-size: 13px;}
|
||||
blockquote {
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0 12px;
|
||||
|
||||
&:before, &:after {content: none;}
|
||||
}
|
||||
ol, ul {margin: 0 0 8px 0;}
|
||||
li {padding-left: 0;}
|
||||
hr {margin: 8px 0;}
|
||||
|
||||
img {
|
||||
width: calc(100% - 48px);
|
||||
margin: 8px 24px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
|
@ -1,26 +1,36 @@
|
|||
.page-spinner {
|
||||
border: 0.3em solid rgba(51, 51, 51, 0.4);
|
||||
border: 4px solid rgba(51, 51, 51, 0.4);
|
||||
border-left-color: $c-pool;
|
||||
border-radius: 50%;
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
position: fixed;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: $sidebar-width; // Center the spinner based on the main content window, not the entire screen
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
/* This is a hack to tell the browser to use the GPU when rendering the spinner. */
|
||||
transform: translateZ(0);
|
||||
animation: spinner 0.6s infinite linear;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
animation: pageSpinner 0.8s infinite linear;
|
||||
}
|
||||
.chronograf-root > .page-spinner {
|
||||
// Center the spinner based on the main content window, not the entire screen
|
||||
left: calc(50% + #{$sidebar-width});
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
@keyframes pageSpinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
transform: translate(-50%,-50%) rotate(0deg);
|
||||
border-left-color: $c-pool;
|
||||
}
|
||||
25% {
|
||||
border-left-color: $c-comet;
|
||||
}
|
||||
50% {
|
||||
border-left-color: $c-pool;
|
||||
}
|
||||
75% {
|
||||
border-left-color: $c-rainforest;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
transform: translate(-50%,-50%) rotate(360deg);
|
||||
border-left-color: $c-pool;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
Sortable Tables
|
||||
----------------------------------------------
|
||||
*/
|
||||
.sortable-header {
|
||||
table.table thead th.sortable-header {
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
@ -82,9 +82,7 @@
|
|||
background-color: $g5-pepper;
|
||||
color: $g19-ghost;
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
}
|
||||
&:after {opacity: 1;}
|
||||
}
|
||||
&.sorting-ascending:after {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
|
|
|
@ -87,19 +87,19 @@ $dash-graph-options-arrow: 8px;
|
|||
left: 0;
|
||||
padding: 0;
|
||||
|
||||
& > div:not(.graph-empty) {
|
||||
& > .dygraph {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& > div:not(.graph-panel__refreshing) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
& > .dygraph > .dygraph-child {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.graph-panel__refreshing {
|
||||
top: (-$dash-graph-heading + 5px) !important;
|
||||
|
|
|
@ -135,7 +135,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currently-connected-source {
|
||||
color: $c-rainforest;
|
||||
font-weight: 600;
|
||||
|
@ -169,3 +168,27 @@ br {
|
|||
border-left: 2px solid $g5-pepper;
|
||||
width: 278px;
|
||||
}
|
||||
|
||||
/*
|
||||
Styles for the Status Dashboard
|
||||
-----------------------------------------------------------------------------
|
||||
Not enough of these to merit their own page, will organize later
|
||||
*/
|
||||
.alerts-widget,
|
||||
.getting-started {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.getting-started--cell {
|
||||
color: $g11-sidewalk;
|
||||
background-color: $g2-kevlar;
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.intro {
|
||||
@include gradient-h($c-pool,$c-star);
|
||||
color: $g20-white;
|
||||
}
|
||||
|
||||
p {font-size: 13px;}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,14 @@ import axios from 'axios'
|
|||
|
||||
let links
|
||||
|
||||
export default async function AJAX({
|
||||
const generateResponseWithLinks = (response, {auth, logout, external}) => ({
|
||||
...response,
|
||||
auth: {links: auth},
|
||||
logoutLink: logout,
|
||||
external,
|
||||
})
|
||||
|
||||
const AJAX = async ({
|
||||
url,
|
||||
resource,
|
||||
id,
|
||||
|
@ -10,7 +17,7 @@ export default async function AJAX({
|
|||
data = {},
|
||||
params = {},
|
||||
headers = {},
|
||||
}) {
|
||||
}) => {
|
||||
try {
|
||||
const basepath = window.basepath || ''
|
||||
let response
|
||||
|
@ -39,17 +46,24 @@ export default async function AJAX({
|
|||
headers,
|
||||
})
|
||||
|
||||
const {auth} = links
|
||||
|
||||
return {
|
||||
...response,
|
||||
auth: {links: auth},
|
||||
logoutLink: links.logout,
|
||||
}
|
||||
return generateResponseWithLinks(response, links)
|
||||
} catch (error) {
|
||||
const {response} = error
|
||||
|
||||
const {auth} = links
|
||||
throw {...response, auth: {links: auth}, logout: links.logout} // eslint-disable-line no-throw-literal
|
||||
throw generateResponseWithLinks(response, links) // eslint-disable-line no-throw-literal
|
||||
}
|
||||
}
|
||||
|
||||
export const get = async url => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default AJAX
|
||||
|
|
Loading…
Reference in New Issue