Merge branch 'master' into new-sidenav-user-icon

pull/1567/head
Alex Paxton 2017-06-01 12:29:13 -07:00 committed by GitHub
commit d07389c7ea
9 changed files with 210 additions and 73 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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),
}

View File

@ -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">

View File

@ -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,
})

View File

@ -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) {