Merge branch 'master' into new-sidenav-user-icon
commit
d07389c7ea
|
@ -3,6 +3,7 @@
|
|||
### Bug Fixes
|
||||
1. [#1530](https://github.com/influxdata/chronograf/pull/1530): Update query config field ordering to always match input query
|
||||
1. [#1535](https://github.com/influxdata/chronograf/pull/1535): Fix add field functions to existing Kapacitor rules
|
||||
1. [#1564](https://github.com/influxdata/chronograf/pull/1564): Fix regression of logout menu item functionality
|
||||
1. [#1562](https://github.com/influxdata/chronograf/pull/1562): Fix InfluxQL parsing with multiple tag values for a tag key
|
||||
|
||||
### Features
|
||||
|
@ -14,6 +15,7 @@
|
|||
1. [#1549](https://github.com/influxdata/chronograf/pull/1549): Reset graph zoom when a new time range is selected
|
||||
1. [#1544](https://github.com/influxdata/chronograf/pull/1544): Upgrade to new version of Influx Theme, remove excess stylesheets
|
||||
1. [#1567](https://github.com/influxdata/chronograf/pull/1567): Replace outline style User icon with solid style
|
||||
1. [#1560](https://github.com/influxdata/chronograf/pull/1560): Apply mean to fields by default
|
||||
1. [#1561](https://github.com/influxdata/chronograf/pull/1561): Disable query save in dashboard editing if the query does not have a database, measurement, and field
|
||||
|
||||
## v1.3.1.0 [2017-05-22]
|
||||
|
|
|
@ -900,11 +900,11 @@ def main(args):
|
|||
lines.sort()
|
||||
|
||||
print ("## Docker")
|
||||
print("docker pull quay.io/influxdb/chronograf:"+get_current_version_tag())
|
||||
print("`docker pull quay.io/influxdb/chronograf:"+get_current_version_tag() + "`")
|
||||
print("")
|
||||
print("## Packages")
|
||||
print("")
|
||||
print("Arch | Package | SHA256")
|
||||
print("Platform | Package | SHA256")
|
||||
print("--- | --- | ---")
|
||||
for line in lines:
|
||||
print(line)
|
||||
|
|
|
@ -179,30 +179,34 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
|
||||
router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.UpdateRetentionPolicy)
|
||||
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.DropRetentionPolicy)
|
||||
var authRoutes AuthRoutes
|
||||
|
||||
allRoutes := &AllRoutes{
|
||||
Logger: opts.Logger,
|
||||
}
|
||||
|
||||
router.Handler("GET", "/chronograf/v1/", allRoutes)
|
||||
|
||||
var out http.Handler
|
||||
|
||||
/* Authentication */
|
||||
logout := "/oauth/logout"
|
||||
basepath := ""
|
||||
if opts.PrefixRoutes {
|
||||
basepath = opts.Basepath
|
||||
}
|
||||
|
||||
/* Authentication */
|
||||
if opts.UseAuth {
|
||||
// Encapsulate the router with OAuth2
|
||||
var auth http.Handler
|
||||
auth, authRoutes = AuthAPI(opts, router)
|
||||
auth, allRoutes.AuthRoutes = AuthAPI(opts, router)
|
||||
allRoutes.LogoutLink = "/oauth/logout"
|
||||
|
||||
// Create middleware to redirect to the appropriate provider logout
|
||||
targetURL := "/"
|
||||
router.GET(logout, Logout(targetURL, basepath, authRoutes))
|
||||
// Create middleware that redirects to the appropriate provider logout
|
||||
router.GET(allRoutes.LogoutLink, Logout("/", basepath, allRoutes.AuthRoutes))
|
||||
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, auth))
|
||||
} else {
|
||||
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, router))
|
||||
}
|
||||
|
||||
router.GET("/chronograf/v1/", AllRoutes(authRoutes, path.Join(opts.Basepath, logout), opts.Logger))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
@ -38,26 +38,33 @@ type getRoutesResponse struct {
|
|||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
}
|
||||
|
||||
// AllRoutes returns all top level routes within chronograf
|
||||
func AllRoutes(authRoutes []AuthRoute, logout string, logger chronograf.Logger) http.HandlerFunc {
|
||||
// AllRoutes is a handler that returns all links to resources in Chronograf server.
|
||||
// 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.
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP returns all top level routes within chronograf
|
||||
func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
routes := getRoutesResponse{
|
||||
Sources: "/chronograf/v1/sources",
|
||||
Layouts: "/chronograf/v1/layouts",
|
||||
Me: "/chronograf/v1/me",
|
||||
Mappings: "/chronograf/v1/mappings",
|
||||
Dashboards: "/chronograf/v1/dashboards",
|
||||
Auth: make([]AuthRoute, len(authRoutes)),
|
||||
}
|
||||
if logout != "" {
|
||||
routes.Logout = &logout
|
||||
Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
|
||||
}
|
||||
|
||||
for i, route := range authRoutes {
|
||||
// The JSON response will have no field present for the LogoutLink if there is no logout link.
|
||||
if a.LogoutLink != "" {
|
||||
routes.Logout = &a.LogoutLink
|
||||
}
|
||||
|
||||
for i, route := range a.AuthRoutes {
|
||||
routes.Auth[i] = route
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
encodeJSON(w, http.StatusOK, routes, logger)
|
||||
return
|
||||
})
|
||||
encodeJSON(w, http.StatusOK, routes, a.Logger)
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@ import (
|
|||
|
||||
func TestAllRoutes(t *testing.T) {
|
||||
logger := log.New(log.DebugLevel)
|
||||
handler := AllRoutes([]AuthRoute{}, "", logger)
|
||||
handler := &AllRoutes{
|
||||
Logger: logger,
|
||||
}
|
||||
req := httptest.NewRequest("GET", "http://docbrowns-inventions.com", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
@ -27,4 +29,47 @@ 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":[]}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAllRoutesWithAuth(t *testing.T) {
|
||||
logger := log.New(log.DebugLevel)
|
||||
handler := &AllRoutes{
|
||||
AuthRoutes: []AuthRoute{
|
||||
{
|
||||
Name: "github",
|
||||
Label: "GitHub",
|
||||
Login: "/oauth/github/login",
|
||||
Logout: "/oauth/github/logout",
|
||||
Callback: "/oauth/github/callback",
|
||||
},
|
||||
},
|
||||
LogoutLink: "/oauth/logout",
|
||||
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("TestAllRoutesWithAuth not able to retrieve body")
|
||||
}
|
||||
var routes getRoutesResponse
|
||||
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"}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,10 +43,13 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
const newState = reducer(state, chooseNamespace(queryId, {
|
||||
database: 'telegraf',
|
||||
retentionPolicy: 'monitor',
|
||||
}))
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
database: 'telegraf',
|
||||
retentionPolicy: 'monitor',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].database).to.equal('telegraf')
|
||||
expect(newState[queryId].retentionPolicy).to.equal('monitor')
|
||||
|
@ -63,23 +66,33 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
const two = reducer(one, chooseNamespace(queryId, {
|
||||
database: '_internal',
|
||||
retentionPolicy: 'daily',
|
||||
}))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryId, {
|
||||
database: '_internal',
|
||||
retentionPolicy: 'daily',
|
||||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryId, 'disk'))
|
||||
state = reducer(three, toggleField(queryId, {field: 'a great field', funcs: []}))
|
||||
state = reducer(
|
||||
three,
|
||||
toggleField(queryId, {field: 'a great field', funcs: []})
|
||||
)
|
||||
})
|
||||
|
||||
describe('choosing a new namespace', () => {
|
||||
it('clears out the old measurement and fields', () => { // what about tags?
|
||||
it('clears out the old measurement and fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].measurement).to.exist
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(state, chooseNamespace(queryId, {
|
||||
database: 'newdb',
|
||||
retentionPolicy: 'newrp',
|
||||
}))
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
database: 'newdb',
|
||||
retentionPolicy: 'newrp',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].measurement).not.to.exist
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
|
@ -87,13 +100,19 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
|
||||
describe('choosing a new measurement', () => {
|
||||
it('leaves the namespace and clears out the old fields', () => { // what about tags?
|
||||
it('leaves the namespace and clears out the old fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(state, chooseMeasurement(queryId, 'newmeasurement'))
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseMeasurement(queryId, 'newmeasurement')
|
||||
)
|
||||
|
||||
expect(state[queryId].database).to.equal(newState[queryId].database)
|
||||
expect(state[queryId].retentionPolicy).to.equal(newState[queryId].retentionPolicy)
|
||||
expect(state[queryId].retentionPolicy).to.equal(
|
||||
newState[queryId].retentionPolicy
|
||||
)
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
@ -103,12 +122,50 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const isKapacitorRule = true
|
||||
const newState = reducer(state, toggleField(queryId, {field: 'a different field', funcs: []}, isKapacitorRule))
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(
|
||||
queryId,
|
||||
{field: 'a different field', funcs: []},
|
||||
isKapacitorRule
|
||||
)
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].field).to.equal('a different field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TOGGLE_FIELDS', () => {
|
||||
it('can toggle multiple fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {field: 'a different field', funcs: []})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(2)
|
||||
expect(newState[queryId].fields[1].field).to.equal('a different field')
|
||||
})
|
||||
|
||||
it('applies a funcs to newly selected fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const oneFieldOneFunc = reducer(
|
||||
state,
|
||||
applyFuncsToField(queryId, {field: 'a great field', funcs: ['func1']})
|
||||
)
|
||||
|
||||
const newState = reducer(
|
||||
oneFieldOneFunc,
|
||||
toggleField(queryId, {field: 'a different field', funcs: []})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields[1].funcs.length).to.equal(1)
|
||||
expect(newState[queryId].fields[1].funcs[0]).to.equal('func1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('APPLY_FUNCS_TO_FIELD', () => {
|
||||
|
@ -192,7 +249,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('creates a new entry if it\'s the first key', () => {
|
||||
it("creates a new entry if it's the first key", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
tags: {},
|
||||
|
@ -283,7 +340,9 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].areTagsAccepted).to.equal(!initialState[queryId].areTagsAccepted)
|
||||
expect(nextState[queryId].areTagsAccepted).to.equal(
|
||||
!initialState[queryId].areTagsAccepted
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -314,7 +373,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
expect(nextState[queryId]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('updates a query\'s raw text', () => {
|
||||
it("updates a query's raw text", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
}
|
||||
|
@ -326,7 +385,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
expect(nextState[queryId].rawText).to.equal('foo')
|
||||
})
|
||||
|
||||
it('updates a query\'s raw status', () => {
|
||||
it("updates a query's raw status", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {PropTypes} from 'react'
|
|||
import {Link} from 'react-router'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const {node, string} = PropTypes
|
||||
const {bool, node, string} = PropTypes
|
||||
|
||||
const NavListItem = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -16,7 +16,10 @@ const NavListItem = React.createClass({
|
|||
const isActive = location.startsWith(link)
|
||||
|
||||
return (
|
||||
<Link className={classnames('sidebar__menu-item', {active: isActive})} to={link}>
|
||||
<Link
|
||||
className={classnames('sidebar__menu-item', {active: isActive})}
|
||||
to={link}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
@ -27,13 +30,20 @@ const NavHeader = React.createClass({
|
|||
propTypes: {
|
||||
link: string,
|
||||
title: string,
|
||||
useAnchor: bool,
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Link className="sidebar__menu-route" to={this.props.link}>
|
||||
<h3 className="sidebar__menu-heading">{this.props.title}</h3>
|
||||
</Link>
|
||||
)
|
||||
const {link, title, useAnchor} = this.props
|
||||
|
||||
// Some nav items, such as Logout, need to hit an external link rather
|
||||
// than simply route to an internal page. Anchor tags serve that purpose.
|
||||
return useAnchor
|
||||
? <a className="sidebar__menu-route" href={link}>
|
||||
<h3 className="sidebar__menu-heading">{title}</h3>
|
||||
</a>
|
||||
: <Link className="sidebar__menu-route" to={link}>
|
||||
<h3 className="sidebar__menu-heading">{title}</h3>
|
||||
</Link>
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -63,7 +73,9 @@ const NavBlock = React.createClass({
|
|||
})
|
||||
|
||||
return (
|
||||
<div className={classnames('sidebar__square', className, {active: isActive})}>
|
||||
<div
|
||||
className={classnames('sidebar__square', className, {active: isActive})}
|
||||
>
|
||||
{this.renderLink()}
|
||||
<div className={wrapperClassName || 'sidebar__menu-wrapper'}>
|
||||
<div className="sidebar__menu">
|
||||
|
|
|
@ -19,16 +19,12 @@ const SideNav = React.createClass({
|
|||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
me: shape({
|
||||
email: string,
|
||||
}),
|
||||
isHidden: bool.isRequired,
|
||||
logoutLink: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
me,
|
||||
params: {sourceID},
|
||||
location: {pathname: location},
|
||||
isHidden,
|
||||
|
@ -37,7 +33,7 @@ const SideNav = React.createClass({
|
|||
|
||||
const sourcePrefix = `/sources/${sourceID}`
|
||||
const dataExplorerLink = `${sourcePrefix}/chronograf/data-explorer`
|
||||
const showLogout = !!(me && me.name)
|
||||
const showLogout = !!logoutLink
|
||||
|
||||
return isHidden
|
||||
? null
|
||||
|
@ -81,7 +77,7 @@ const SideNav = React.createClass({
|
|||
</NavBlock>
|
||||
{showLogout
|
||||
? <NavBlock icon="user" className="sidebar__square-last">
|
||||
<NavHeader link={logoutLink} title="Logout" />
|
||||
<NavHeader useAnchor={true} link={logoutLink} title="Logout" />
|
||||
</NavBlock>
|
||||
: null}
|
||||
</NavBar>
|
||||
|
@ -89,10 +85,9 @@ const SideNav = React.createClass({
|
|||
})
|
||||
|
||||
const mapStateToProps = ({
|
||||
auth: {me, logoutLink},
|
||||
auth: {logoutLink},
|
||||
app: {ephemeral: {inPresentationMode}},
|
||||
}) => ({
|
||||
me,
|
||||
isHidden: inPresentationMode,
|
||||
logoutLink,
|
||||
})
|
||||
|
|
|
@ -16,31 +16,44 @@ export function chooseMeasurement(query, measurement) {
|
|||
})
|
||||
}
|
||||
|
||||
export function toggleField(query, {field, funcs}, isKapacitorRule = false) {
|
||||
export const toggleField = (query, {field, funcs}, isKapacitorRule = false) => {
|
||||
const isSelected = query.fields.find(f => f.field === field)
|
||||
if (isSelected) {
|
||||
const nextFields = query.fields.filter(f => f.field !== field)
|
||||
if (!nextFields.length) {
|
||||
const nextGroupBy = Object.assign({}, query.groupBy, {time: null})
|
||||
return Object.assign({}, query, {
|
||||
const nextGroupBy = {...query.groupBy, time: null}
|
||||
return {
|
||||
...query,
|
||||
fields: nextFields,
|
||||
groupBy: nextGroupBy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign({}, query, {
|
||||
return {
|
||||
...query,
|
||||
fields: nextFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isKapacitorRule) {
|
||||
return Object.assign({}, query, {
|
||||
return {
|
||||
...query,
|
||||
fields: [{field, funcs}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let newFuncs = ['mean']
|
||||
if (query.fields.length) {
|
||||
newFuncs = query.fields.find(f => f.funcs).funcs
|
||||
}
|
||||
|
||||
return {
|
||||
...query,
|
||||
fields: query.fields.concat({
|
||||
field,
|
||||
funcs: newFuncs,
|
||||
}),
|
||||
}
|
||||
return Object.assign({}, query, {
|
||||
fields: query.fields.concat({field, funcs}),
|
||||
})
|
||||
}
|
||||
|
||||
export function groupByTime(query, time) {
|
||||
|
|
Loading…
Reference in New Issue