diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f0de23fc..32be83f054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/influx/query.go b/influx/query.go index b8c358920c..640066ad7b 100644 --- a/influx/query.go +++ b/influx/query.go @@ -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 { diff --git a/influx/query_test.go b/influx/query_test.go index 64ffc67dd1..116df26c59 100644 --- a/influx/query_test.go +++ b/influx/query_test.go @@ -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) } }) } diff --git a/ui/spec/shared/reducers/appSpec.js b/ui/spec/shared/reducers/appSpec.js index 5d1190c779..94935ae1f1 100644 --- a/ui/spec/shared/reducers/appSpec.js +++ b/ui/spec/shared/reducers/appSpec.js @@ -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 + ) + }) }) diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 6f97e0ced4..7bb27e4977 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -20,6 +20,7 @@ const Dashboard = ({ templatesIncludingDashTime, onSummonOverlayTechnologies, onSelectTemplate, + showTemplateControlBar, }) => { if (dashboard.id === 0) { return null @@ -41,16 +42,20 @@ const Dashboard = ({ }) return ( - +
- + {inPresentationMode + ? null + : } {cells.length ? isHidden ? null @@ -59,6 +62,16 @@ const DashboardHeader = ({ Rename : null} + {dashboard + ?
+ Template Variables +
+ : null} this.handleChange(e.target.value)} diff --git a/ui/src/dashboards/components/TemplateControlBar.js b/ui/src/dashboards/components/TemplateControlBar.js index 4b729d949f..9e32e2a983 100644 --- a/ui/src/dashboards/components/TemplateControlBar.js +++ b/ui/src/dashboards/components/TemplateControlBar.js @@ -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, }) => ( -
-
- {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 +
+
+
+ {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 = ({
) })} +======= + // 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 ( +
+ + onSelectTemplate(id, [item].map(x => omit(x, 'text')))} + /> + +
+ ) + }) + :
+ This dashboard does not have any Template Variables +
} +
+ +>>>>>>> master
-
) -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 diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index b0b1fdf35e..70d1036c23 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -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}
@@ -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), diff --git a/ui/src/shared/actions/app.js b/ui/src/shared/actions/app.js index f2af1731ae..df95c736dc 100644 --- a/ui/src/shared/actions/app.js +++ b/ui/src/shared/actions/app.js @@ -24,6 +24,10 @@ export const setAutoRefresh = milliseconds => ({ }, }) +export const templateControlBarVisibilityToggled = () => ({ + type: 'TEMPLATE_CONTROL_BAR_VISIBILITY_TOGGLED', +}) + export const noop = () => ({ type: 'NOOP', payload: {}, diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js index 366ac49ec6..32c4959a7a 100644 --- a/ui/src/shared/reducers/app.js +++ b/ui/src/shared/reducers/app.js @@ -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 } diff --git a/ui/src/style/components/dropdown.scss b/ui/src/style/components/dropdown.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/src/style/components/template-control-bar.scss b/ui/src/style/components/template-control-bar.scss index 0b83f8c077..aab34e1172 100644 --- a/ui/src/style/components/template-control-bar.scss +++ b/ui/src/style/components/template-control-bar.scss @@ -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; diff --git a/ui/src/style/theme/theme-dark.scss b/ui/src/style/theme/theme-dark.scss new file mode 100644 index 0000000000..e69de29bb2