Merge branch 'master' into theme-refactor

pull/10616/head
Alex P 2017-05-23 14:42:56 -07:00
commit b45aa7338a
14 changed files with 218 additions and 35 deletions

View File

@ -1,6 +1,7 @@
## v1.3.2.0 [unreleased]
### Bug Fixes
1. [#1530](https://github.com/influxdata/chronograf/pull/1530): Update query config field ordering to always match input query
### Features
@ -34,6 +35,8 @@ In versions 1.3.1+, installing a new version of Chronograf automatically clears
### UI Improvements
1. [#1451](https://github.com/influxdata/chronograf/pull/1451): Refactor scrollbars to support non-webkit browsers
1. [#1453](https://github.com/influxdata/chronograf/pull/1453): Increase the query builder's default height in cell editor mode and in the data explorer
1. [#1453](https://github.com/influxdata/chronograf/pull/1453): Give QueryMaker a greater initial height than Visualization
1. [#1475](https://github.com/influxdata/chronograf/pull/1475): Add ability to toggle visibility of the Template Control Bar
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Make the [template variables](https://docs.influxdata.com/chronograf/v1.3/guides/dashboard-template-variables/) manager more space efficient
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Add page spinners to pages that did not have them
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Denote which source is connected in the sources table

View File

@ -110,14 +110,15 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) {
}
}
fields := map[string][]string{}
fields := make(map[string][]string)
order := make(map[string]int)
for _, fld := range stmt.Fields {
switch f := fld.Expr.(type) {
default:
return raw, nil
case *influxql.Call:
// only support certain query config functions
if _, ok := supportedFuncs[f.Name]; !ok {
if _, ok = supportedFuncs[f.Name]; !ok {
return raw, nil
}
// Query configs only support single argument functions
@ -134,6 +135,7 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) {
return raw, nil
}
if call, ok := fields[ref.Val]; !ok {
order[ref.Val] = len(fields)
fields[ref.Val] = []string{f.Name}
} else {
fields[ref.Val] = append(call, f.Name)
@ -143,16 +145,19 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) {
return raw, nil
}
if _, ok := fields[f.Val]; !ok {
order[f.Val] = len(fields)
fields[f.Val] = []string{}
}
}
}
qc.Fields = make([]chronograf.Field, len(fields))
for fld, funcs := range fields {
qc.Fields = append(qc.Fields, chronograf.Field{
i := order[fld]
qc.Fields[i] = chronograf.Field{
Field: fld,
Funcs: funcs,
})
}
}
if stmt.Condition == nil {

View File

@ -15,6 +15,66 @@ func TestConvert(t *testing.T) {
want chronograf.QueryConfig
wantErr bool
}{
{
name: "Test field order",
influxQL: `SELECT "usage_idle", "usage_guest_nice", "usage_system", "usage_guest" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime:`,
want: chronograf.QueryConfig{
Database: "telegraf",
Measurement: "cpu",
RetentionPolicy: "autogen",
Fields: []chronograf.Field{
chronograf.Field{
Field: "usage_idle",
Funcs: []string{},
},
chronograf.Field{
Field: "usage_guest_nice",
Funcs: []string{},
},
chronograf.Field{
Field: "usage_system",
Funcs: []string{},
},
chronograf.Field{
Field: "usage_guest",
Funcs: []string{},
},
},
Tags: map[string][]string{},
GroupBy: chronograf.GroupBy{
Tags: []string{},
},
},
},
{
name: "Test field function order",
influxQL: `SELECT mean("usage_idle"), median("usage_idle"), count("usage_guest_nice"), mean("usage_guest_nice") FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime:`,
want: chronograf.QueryConfig{
Database: "telegraf",
Measurement: "cpu",
RetentionPolicy: "autogen",
Fields: []chronograf.Field{
chronograf.Field{
Field: "usage_idle",
Funcs: []string{
"mean",
"median",
},
},
chronograf.Field{
Field: "usage_guest_nice",
Funcs: []string{
"count",
"mean",
},
},
},
Tags: map[string][]string{},
GroupBy: chronograf.GroupBy{
Tags: []string{},
},
},
},
{
name: "Test named count field",
influxQL: `SELECT moving_average(mean("count"),14) FROM "usage_computed"."autogen".unique_clusters_by_day WHERE time > now() - 90d AND product = 'influxdb' group by time(1d)`,
@ -248,7 +308,7 @@ func TestConvert(t *testing.T) {
}
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Convert() = %#v, want %#v", got, tt.want)
t.Errorf("Convert() = \n%#v\n want \n%#v\n", got, tt.want)
}
})
}

View File

@ -4,6 +4,7 @@ import {
disablePresentationMode,
// delayEnablePresentationMode,
setAutoRefresh,
templateControlBarVisibilityToggled,
} from 'src/shared/actions/app'
describe('Shared.Reducers.appReducer', () => {
@ -13,6 +14,7 @@ describe('Shared.Reducers.appReducer', () => {
},
persisted: {
autoRefresh: 0,
showTemplateControlBar: false,
},
}
@ -37,4 +39,17 @@ describe('Shared.Reducers.appReducer', () => {
expect(reducedState.persisted.autoRefresh).to.equal(expectedMs)
})
it('should handle TEMPLATE_CONTROL_BAR_VISIBILITY_TOGGLED', () => {
const reducedState = appReducer(
initialState,
templateControlBarVisibilityToggled()
)
const expectedTestState = !reducedState.persisted.showTemplateControlBar
expect(initialState.persisted.showTemplateControlBar).to.equal(
expectedTestState
)
})
})

View File

@ -20,6 +20,7 @@ const Dashboard = ({
templatesIncludingDashTime,
onSummonOverlayTechnologies,
onSelectTemplate,
showTemplateControlBar,
}) => {
if (dashboard.id === 0) {
return null
@ -41,16 +42,20 @@ const Dashboard = ({
})
return (
<FancyScrollbar className={classnames(
'page-contents',
{'presentation-mode': inPresentationMode}
)}>
<FancyScrollbar
className={classnames('page-contents', {
'presentation-mode': inPresentationMode,
})}
>
<div className="dashboard container-fluid full-width">
<TemplateControlBar
templates={dashboard.templates}
onSelectTemplate={onSelectTemplate}
onOpenTemplateManager={onOpenTemplateManager}
/>
{inPresentationMode
? null
: <TemplateControlBar
templates={dashboard.templates}
onSelectTemplate={onSelectTemplate}
onOpenTemplateManager={onOpenTemplateManager}
isOpen={showTemplateControlBar}
/>}
{cells.length
? <LayoutRenderer
templates={templatesIncludingDashTime}
@ -116,6 +121,7 @@ Dashboard.propTypes = {
timeRange: shape({}).isRequired,
onOpenTemplateManager: func.isRequired,
onSelectTemplate: func.isRequired,
showTemplateControlBar: bool,
}
export default Dashboard

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
@ -19,6 +20,8 @@ const DashboardHeader = ({
source,
onAddCell,
onEditDashboard,
onToggleTempVarControls,
showTemplateControlBar,
}) =>
isHidden
? null
@ -59,6 +62,16 @@ const DashboardHeader = ({
Rename
</button>
: null}
{dashboard
? <div
className={classnames('btn btn-info btn-sm', {
active: showTemplateControlBar,
})}
onClick={onToggleTempVarControls}
>
<span className="icon cube" />Template Variables
</div>
: null}
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
selected={autoRefresh}
@ -95,6 +108,8 @@ DashboardHeader.propTypes = {
source: shape({}),
onAddCell: func,
onEditDashboard: func,
onToggleTempVarControls: func,
showTemplateControlBar: bool,
}
export default DashboardHeader

View File

@ -44,6 +44,7 @@ class DashboardEditHeader extends Component {
<input
className="page-header--editing"
name="name"
value={name}
placeholder="Name this Dashboard"
value={name}
onChange={e => this.handleChange(e.target.value)}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
@ -8,14 +9,18 @@ const TemplateControlBar = ({
templates,
onSelectTemplate,
onOpenTemplateManager,
isOpen,
}) => (
<div className="template-control-bar">
<div className="template-control--controls">
{templates.map(({id, values, tempVar}) => {
const items = values.map(value => ({...value, text: value.value}))
const selectedItem = items.find(item => item.selected) || items[0]
const selectedText = selectedItem && selectedItem.text
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
<div className="template-control--controls">
{templates.length
? templates.map(({id, values, tempVar}) => {
const items = values.map(value => ({...value, text: value.value}))
const selectedItem = items.find(item => item.selected) || items[0]
const selectedText = selectedItem && selectedItem.text
<<<<<<< HEAD
// TODO: change Dropdown to a MultiSelectDropdown, `selected` to
// the full array, and [item] to all `selected` values when we update
// this component to support multiple values
@ -36,18 +41,43 @@ const TemplateControlBar = ({
</div>
)
})}
=======
// TODO: change Dropdown to a MultiSelectDropdown, `selected` to
// the full array, and [item] to all `selected` values when we update
// this component to support multiple values
return (
<div key={id} className="template-control--dropdown">
<Dropdown
items={items}
buttonSize="btn-xs"
useAutoComplete={true}
selected={selectedText || '(No values)'}
onChoose={item =>
onSelectTemplate(id, [item].map(x => omit(x, 'text')))}
/>
<label className="template-control--label">
{tempVar}
</label>
</div>
)
})
: <div className="template-control--empty">
This dashboard does not have any Template Variables
</div>}
</div>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={onOpenTemplateManager}
>
<span className="icon cog-thick" />
Manage
</button>
>>>>>>> master
</div>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={onOpenTemplateManager}
>
<span className="icon cog-thick" />
Templates
</button>
</div>
)
const {arrayOf, func, shape, string} = PropTypes
const {arrayOf, bool, func, shape, string} = PropTypes
TemplateControlBar.propTypes = {
templates: arrayOf(
@ -63,6 +93,7 @@ TemplateControlBar.propTypes = {
).isRequired,
onSelectTemplate: func.isRequired,
onOpenTemplateManager: func.isRequired,
isOpen: bool,
}
export default TemplateControlBar

View File

@ -15,7 +15,10 @@ import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import * as dashboardActionCreators from 'src/dashboards/actions'
import {setAutoRefresh} from 'shared/actions/app'
import {
setAutoRefresh,
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
class DashboardPage extends Component {
@ -48,6 +51,7 @@ class DashboardPage extends Component {
this.handleSelectTemplate = ::this.handleSelectTemplate
this.handleEditTemplateVariables = ::this.handleEditTemplateVariables
this.handleRunQueryFailure = ::this.handleRunQueryFailure
this.handleToggleTempVarControls = ::this.handleToggleTempVarControls
}
componentDidMount() {
@ -206,6 +210,10 @@ class DashboardPage extends Component {
this.props.errorThrown(error)
}
handleToggleTempVarControls() {
this.props.templateControlBarVisibilityToggled()
}
getActiveDashboard() {
const {params: {dashboardID}, dashboards} = this.props
return dashboards.find(d => d.id === +dashboardID)
@ -215,6 +223,7 @@ class DashboardPage extends Component {
const {
source,
timeRange,
showTemplateControlBar,
dashboards,
autoRefresh,
cellQueryStatus,
@ -289,6 +298,8 @@ class DashboardPage extends Component {
source={source}
onAddCell={this.handleAddCell}
onEditDashboard={this.handleEditDashboard}
onToggleTempVarControls={this.handleToggleTempVarControls}
showTemplateControlBar={showTemplateControlBar}
>
{dashboards
? dashboards.map((d, i) => (
@ -317,6 +328,7 @@ class DashboardPage extends Component {
templatesIncludingDashTime={templatesIncludingDashTime}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
onSelectTemplate={this.handleSelectTemplate}
showTemplateControlBar={showTemplateControlBar}
/>
: null}
</div>
@ -374,7 +386,9 @@ DashboardPage.propTypes = {
),
handleChooseAutoRefresh: func.isRequired,
autoRefresh: number.isRequired,
templateControlBarVisibilityToggled: func.isRequired,
timeRange: shape({}).isRequired,
showTemplateControlBar: bool.isRequired,
inPresentationMode: bool.isRequired,
handleClickPresentationButton: func,
cellQueryStatus: shape({
@ -386,7 +400,10 @@ DashboardPage.propTypes = {
const mapStateToProps = state => {
const {
app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}},
app: {
ephemeral: {inPresentationMode},
persisted: {autoRefresh, showTemplateControlBar},
},
dashboardUI: {dashboards, timeRange, cellQueryStatus},
} = state
@ -394,6 +411,7 @@ const mapStateToProps = state => {
dashboards,
autoRefresh,
timeRange,
showTemplateControlBar,
inPresentationMode,
cellQueryStatus,
}
@ -401,6 +419,10 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
templateControlBarVisibilityToggled: bindActionCreators(
templateControlBarVisibilityToggledAction,
dispatch
),
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),

View File

@ -24,6 +24,10 @@ export const setAutoRefresh = milliseconds => ({
},
})
export const templateControlBarVisibilityToggled = () => ({
type: 'TEMPLATE_CONTROL_BAR_VISIBILITY_TOGGLED',
})
export const noop = () => ({
type: 'NOOP',
payload: {},

View File

@ -8,6 +8,7 @@ const initialState = {
},
persisted: {
autoRefresh: AUTOREFRESH_DEFAULT,
showTemplateControlBar: false,
},
}
@ -46,6 +47,12 @@ const appPersistedReducer = (state = initialAppPersistedState, action) => {
}
}
case 'TEMPLATE_CONTROL_BAR_VISIBILITY_TOGGLED': {
const {showTemplateControlBar} = state
return {...state, showTemplateControlBar: !showTemplateControlBar}
}
default:
return state
}

View File

View File

@ -7,13 +7,22 @@
$template-control--margin: 2px;
$template-control--min-height: 52px;
$template-control-dropdown-min-width: 146px;
.template-control-bar {
display: none;
height: auto;
margin-bottom: 8px;
&.show {
display: block;
}
}
.template-control--container {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding: $template-control--margin;
@extend .cell-shell;
background-color: $g0-obsidian;
@ -35,9 +44,16 @@ button.btn.template-control--manage {
flex: 1 0 0;
flex-wrap: wrap;
}
.template-control--empty {
color: $g11-sidewalk;
font-size: 14px;
font-weight: 500;
margin-left: 18px;
@include no-user-select();
}
.template-control--dropdown {
flex: 0 1 auto;
min-width: 150px;
min-width: $template-control-dropdown-min-width;
display: flex;
flex-direction: column;
align-items: stretch;
@ -48,14 +64,12 @@ button.btn.template-control--manage {
margin: 0;
flex: 1 0 0;
}
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;
width: 100%;
font-size: 12px;
font-family: $code-font;
}
}
.template-control--label {
@include no-user-select();
order: 1;

View File