Merge pull request #1315 from influxdata/feature/template-variables_proxy-params
Feature/template variables dropdown ui & proxy paramspull/1326/head
commit
187c32a255
|
@ -10,7 +10,8 @@
|
|||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
||||
1. [#1316](https://github.com/influxdata/chronograf/pull/1316): Add templates API scoped within a dashboard
|
||||
|
||||
1. [#1311](https://github.com/influxdata/chronograf/pull/1311): Display currently selected values in TVControlBar
|
||||
1. [#1315](https://github.com/influxdata/chronograf/pull/1315): Send selected TV values to proxy
|
||||
|
||||
### UI Improvements
|
||||
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add default display for empty dashboard
|
||||
|
|
2
Makefile
2
Makefile
|
@ -29,7 +29,7 @@ define CHRONOGIRAFFE
|
|||
,"" _\_
|
||||
," ## | 0 0.
|
||||
," ## ,-\__ `.
|
||||
," / `--._;) - "HAI, I'm Chronogiraffe. Will you be my friend?"
|
||||
," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!"
|
||||
," ## /
|
||||
," ## /
|
||||
endef
|
||||
|
|
|
@ -11,10 +11,45 @@ import {
|
|||
renameDashboardCell,
|
||||
syncDashboardCell,
|
||||
editTemplate,
|
||||
templateSelected,
|
||||
} from 'src/dashboards/actions'
|
||||
|
||||
let state
|
||||
const d1 = {id: 1, cells: [], name: 'd1', templates: []}
|
||||
const d1 = {
|
||||
id: 1,
|
||||
cells: [],
|
||||
name: 'd1',
|
||||
templates: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'query',
|
||||
label: 'test query',
|
||||
tempVar: '$REGION',
|
||||
query: {
|
||||
db: 'db1',
|
||||
rp: 'rp1',
|
||||
measurement: 'm1',
|
||||
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
|
||||
},
|
||||
values: [
|
||||
{value: 'us-west', type: 'tagKey', selected: false},
|
||||
{value: 'us-east', type: 'tagKey', selected: true},
|
||||
{value: 'us-mount', type: 'tagKey', selected: false},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'csv',
|
||||
label: 'test csv',
|
||||
tempVar: '$TEMPERATURE',
|
||||
values: [
|
||||
{value: '98.7', type: 'measurement', selected: false},
|
||||
{value: '99.1', type: 'measurement', selected: false},
|
||||
{value: '101.3', type: 'measurement', selected: true},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const d2 = {id: 2, cells: [], name: 'd2', templates: []}
|
||||
const dashboards = [d1, d2]
|
||||
const c1 = {
|
||||
|
@ -142,4 +177,17 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
const actual = reducer(state, editTemplate(dash.id, tempVar.id, updates))
|
||||
expect(actual.dashboards[0].templates[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can select a different template variable', () => {
|
||||
const dash = _.cloneDeep(d1)
|
||||
state = {
|
||||
dashboards: [dash]
|
||||
}
|
||||
const value = dash.templates[0].values[2].value
|
||||
const actual = reducer({dashboards}, templateSelected(dash.id, dash.templates[0].id, [{value}]))
|
||||
|
||||
expect(actual.dashboards[0].templates[0].values[0].selected).to.equal(false)
|
||||
expect(actual.dashboards[0].templates[0].values[1].selected).to.equal(false)
|
||||
expect(actual.dashboards[0].templates[0].values[2].selected).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -12,6 +12,10 @@ import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
|||
|
||||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||
|
||||
import {
|
||||
TEMPLATE_VARIABLE_SELECTED,
|
||||
} from 'shared/constants/actionTypes'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
type: 'LOAD_DASHBOARDS',
|
||||
payload: {
|
||||
|
@ -111,6 +115,15 @@ export const editCellQueryStatus = (queryID, status) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const templateSelected = (dashboardID, templateID, values) => ({
|
||||
type: TEMPLATE_VARIABLE_SELECTED,
|
||||
payload: {
|
||||
dashboardID,
|
||||
templateID,
|
||||
values,
|
||||
},
|
||||
})
|
||||
|
||||
export const editTemplate = (dashboardID, templateID, updates) => ({
|
||||
type: 'EDIT_TEMPLATE',
|
||||
payload: {
|
||||
|
@ -124,23 +137,32 @@ export const editTemplate = (dashboardID, templateID, updates) => ({
|
|||
|
||||
const templates = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'measurement',
|
||||
id: '1',
|
||||
type: 'query',
|
||||
label: 'test query',
|
||||
code: '$HOSTS',
|
||||
tempVar: '$REGION',
|
||||
query: {
|
||||
db: 'db1',
|
||||
rp: 'rp1',
|
||||
measurement: 'm1',
|
||||
text: 'SHOW TAGS WHERE HUNTER = "coo"',
|
||||
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
|
||||
},
|
||||
values: ['h1', 'h2', 'h3'],
|
||||
values: [
|
||||
{value: 'us-west', type: 'tagKey', selected: false},
|
||||
{value: 'us-east', type: 'tagKey', selected: true},
|
||||
{value: 'us-mount', type: 'tagKey', selected: false},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: '2',
|
||||
type: 'csv',
|
||||
label: 'test csv',
|
||||
code: '$INFLX',
|
||||
values: ['A', 'B', 'C'],
|
||||
tempVar: '$TEMPERATURE',
|
||||
values: [
|
||||
{value: '98.7', type: 'measurement', selected: false},
|
||||
{value: '99.1', type: 'measurement', selected: false},
|
||||
{value: '101.3', type: 'measurement', selected: true},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import omit from 'lodash/omit'
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const Dashboard = ({
|
||||
source,
|
||||
|
@ -17,11 +20,14 @@ const Dashboard = ({
|
|||
inPresentationMode,
|
||||
onOpenTemplateManager,
|
||||
onSummonOverlayTechnologies,
|
||||
onSelectTemplate,
|
||||
}) => {
|
||||
if (dashboard.id === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const {templates} = dashboard
|
||||
|
||||
const cells = dashboard.cells.map((cell) => {
|
||||
const dashboardCell = {...cell}
|
||||
dashboardCell.queries = dashboardCell.queries.map(({label, query, queryConfig, db}) =>
|
||||
|
@ -39,13 +45,40 @@ const Dashboard = ({
|
|||
|
||||
return (
|
||||
<div className={classnames('dashboard container-fluid full-width page-contents', {'presentation-mode': inPresentationMode})}>
|
||||
<div className="tv-control-bar">
|
||||
Template Variables
|
||||
<button className="btn btn-primary btn-sm" onClick={onOpenTemplateManager}>Manage</button>
|
||||
<div className="template-control-bar">
|
||||
<div className="page-header__left">
|
||||
Template Variables
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
{
|
||||
templates.map(({id, values}) => {
|
||||
let selected
|
||||
const items = values.map((value) => {
|
||||
if (value.selected) {
|
||||
selected = value.value
|
||||
}
|
||||
return {...value, text: value.value}
|
||||
})
|
||||
// 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 (
|
||||
<Dropdown
|
||||
key={id}
|
||||
items={items}
|
||||
selected={selected || "Loading..."}
|
||||
onChoose={(item) => onSelectTemplate(id, [item].map((x) => omit(x, 'text')))}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
<button className="btn btn-primary btn-sm" onClick={onOpenTemplateManager}>Manage</button>
|
||||
</div>
|
||||
</div>
|
||||
{cells.length ?
|
||||
<LayoutRenderer
|
||||
timeRange={timeRange}
|
||||
templates={templates}
|
||||
cells={cells}
|
||||
autoRefresh={autoRefresh}
|
||||
source={source.links.proxy}
|
||||
|
@ -71,6 +104,7 @@ const Dashboard = ({
|
|||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
|
@ -96,6 +130,22 @@ Dashboard.propTypes = {
|
|||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
onOpenTemplateManager: func.isRequired,
|
||||
onSelectTemplate: func.isRequired,
|
||||
templates: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
label: string.isRequired,
|
||||
tempVar: string.isRequired,
|
||||
query: shape({
|
||||
db: string.isRequired,
|
||||
rp: string,
|
||||
influxql: string.isRequired,
|
||||
}),
|
||||
values: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
})).isRequired,
|
||||
})),
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
|
|
|
@ -40,8 +40,8 @@ class DashboardPage extends Component {
|
|||
this.handleRenameDashboardCell = ::this.handleRenameDashboardCell
|
||||
this.handleUpdateDashboardCell = ::this.handleUpdateDashboardCell
|
||||
this.handleCloseTemplateManager = ::this.handleCloseTemplateManager
|
||||
this.handleSummonOverlayTechnologies = ::this
|
||||
.handleSummonOverlayTechnologies
|
||||
this.handleSummonOverlayTechnologies = ::this.handleSummonOverlayTechnologies
|
||||
this.handleSelectTemplate = ::this.handleSelectTemplate
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -143,6 +143,11 @@ class DashboardPage extends Component {
|
|||
this.props.dashboardActions.deleteDashboardCellAsync(cell)
|
||||
}
|
||||
|
||||
handleSelectTemplate(templateID, values) {
|
||||
const {params: {dashboardID}} = this.props
|
||||
this.props.dashboardActions.templateSelected(+dashboardID, templateID, values)
|
||||
}
|
||||
|
||||
getActiveDashboard() {
|
||||
const {params: {dashboardID}, dashboards} = this.props
|
||||
return dashboards.find(d => d.id === +dashboardID)
|
||||
|
@ -237,6 +242,7 @@ class DashboardPage extends Component {
|
|||
onUpdateCell={this.handleUpdateDashboardCell}
|
||||
onOpenTemplateManager={this.handleOpenTemplateManager}
|
||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||
onSelectTemplate={this.handleSelectTemplate}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
@ -279,10 +285,15 @@ DashboardPage.propTypes = {
|
|||
code: string.isRequired,
|
||||
query: shape({
|
||||
db: string.isRequired,
|
||||
text: string.isRequired,
|
||||
rp: string,
|
||||
influxql: string.isRequired,
|
||||
}),
|
||||
values: arrayOf(string.isRequired),
|
||||
})
|
||||
values: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
})).isRequired,
|
||||
}),
|
||||
),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -10,6 +10,10 @@ const initialState = {
|
|||
cellQueryStatus: {queryID: null, status: null},
|
||||
}
|
||||
|
||||
import {
|
||||
TEMPLATE_VARIABLE_SELECTED,
|
||||
} from 'shared/constants/actionTypes'
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'LOAD_DASHBOARDS': {
|
||||
|
@ -180,6 +184,33 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, cellQueryStatus: {queryID, status}}
|
||||
}
|
||||
|
||||
case TEMPLATE_VARIABLE_SELECTED: {
|
||||
const {dashboardID, templateID, values: updatedSelectedValues} = action.payload
|
||||
const newDashboards = state.dashboards.map((dashboard) => {
|
||||
if (dashboard.id === dashboardID) {
|
||||
const newTemplates = dashboard.templates.map((staleTemplate) => {
|
||||
if (staleTemplate.id === templateID) {
|
||||
const newValues = staleTemplate.values.map((staleValue) => {
|
||||
let selected = false
|
||||
for (let i = 0; i < updatedSelectedValues.length; i++) {
|
||||
if (updatedSelectedValues[i].value === staleValue.value) {
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return {...staleValue, selected}
|
||||
})
|
||||
return {...staleTemplate, values: newValues}
|
||||
}
|
||||
return staleTemplate
|
||||
})
|
||||
return {...dashboard, templates: newTemplates}
|
||||
}
|
||||
return dashboard
|
||||
})
|
||||
return {...state, dashboards: newDashboards}
|
||||
}
|
||||
|
||||
case 'EDIT_TEMPLATE': {
|
||||
const {dashboardID, templateID, updates} = action.payload
|
||||
|
||||
|
|
|
@ -35,10 +35,10 @@ export const handleError = (error, query, editQueryStatus) => {
|
|||
console.error(error)
|
||||
}
|
||||
|
||||
export const fetchTimeSeriesAsync = async ({source, db, rp, query}, editQueryStatus = noop) => {
|
||||
export const fetchTimeSeriesAsync = async ({source, db, rp, query, templates}, editQueryStatus = noop) => {
|
||||
handleLoading(query, editQueryStatus)
|
||||
try {
|
||||
const {data} = await proxy({source, db, rp, query: query.text})
|
||||
const {data} = await proxy({source, db, rp, query: query.text, templates})
|
||||
return handleSuccess(data, query, editQueryStatus)
|
||||
} catch (error) {
|
||||
handleError(error, query, editQueryStatus)
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
const fetchTimeSeries = async (source, database, query) => {
|
||||
try {
|
||||
return await proxy({source, query, database})
|
||||
} catch (error) {
|
||||
console.error('error from proxy: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default fetchTimeSeries
|
|
@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
|||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
element,
|
||||
func,
|
||||
number,
|
||||
|
@ -17,18 +18,35 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
propTypes: {
|
||||
children: element,
|
||||
autoRefresh: number.isRequired,
|
||||
templates: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
label: string.isRequired,
|
||||
tempVar: string.isRequired,
|
||||
query: shape({
|
||||
db: string.isRequired,
|
||||
rp: string,
|
||||
influxql: string.isRequired,
|
||||
}),
|
||||
values: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
})).isRequired,
|
||||
})),
|
||||
queries: arrayOf(shape({
|
||||
host: oneOfType([string, arrayOf(string)]),
|
||||
text: string,
|
||||
}).isRequired).isRequired,
|
||||
editQueryStatus: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
lastQuerySuccessful: false,
|
||||
timeSeries: [],
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const {queries, autoRefresh} = this.props
|
||||
this.executeQueries(queries)
|
||||
|
@ -36,6 +54,7 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh)
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const shouldRefetch = this.queryDifference(this.props.queries, nextProps.queries).length
|
||||
|
||||
|
@ -51,41 +70,45 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
queryDifference(left, right) {
|
||||
const leftStrs = left.map((q) => `${q.host}${q.text}`)
|
||||
const rightStrs = right.map((q) => `${q.host}${q.text}`)
|
||||
return _.difference(_.union(leftStrs, rightStrs), _.intersection(leftStrs, rightStrs))
|
||||
},
|
||||
|
||||
async executeQueries(queries) {
|
||||
const {templates, editQueryStatus} = this.props
|
||||
|
||||
if (!queries.length) {
|
||||
this.setState({
|
||||
timeSeries: [],
|
||||
})
|
||||
this.setState({timeSeries: []})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({isFetching: true})
|
||||
let count = 0
|
||||
const newSeries = []
|
||||
for (const query of queries) {
|
||||
|
||||
const timeSeriesPromises = queries.map((query) => {
|
||||
const {host, database, rp} = query
|
||||
const response = await fetchTimeSeriesAsync({source: host, db: database, rp, query}, this.props.editQueryStatus)
|
||||
newSeries.push({response})
|
||||
count += 1
|
||||
if (count === queries.length) {
|
||||
const querySuccessful = !this._noResultsForQuery(newSeries)
|
||||
this.setState({
|
||||
lastQuerySuccessful: querySuccessful,
|
||||
isFetching: false,
|
||||
timeSeries: newSeries,
|
||||
})
|
||||
}
|
||||
}
|
||||
return fetchTimeSeriesAsync({source: host, db: database, rp, query, templates}, editQueryStatus)
|
||||
})
|
||||
|
||||
Promise.all(timeSeriesPromises).then(timeSeries => {
|
||||
const newSeries = timeSeries.map((response) => ({response}))
|
||||
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
|
||||
|
||||
this.setState({
|
||||
timeSeries: newSeries,
|
||||
lastQuerySuccessful,
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalID)
|
||||
this.intervalID = false
|
||||
},
|
||||
|
||||
render() {
|
||||
const {timeSeries} = this.state
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ export const LayoutRenderer = React.createClass({
|
|||
type: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
templates: arrayOf(shape()).isRequired,
|
||||
host: string,
|
||||
source: string,
|
||||
onPositionChange: func,
|
||||
|
@ -89,11 +90,40 @@ export const LayoutRenderer = React.createClass({
|
|||
return text
|
||||
},
|
||||
|
||||
renderRefreshingGraph(type, queries) {
|
||||
const {autoRefresh, templates} = this.props
|
||||
|
||||
if (type === 'single-stat') {
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
queries={[queries[0]]}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: type === 'line-stepplot',
|
||||
stackedGraph: type === 'line-stacked',
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
showSingleStat={type === 'line-plus-single-stat'}
|
||||
displayOptions={displayOptions}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
generateVisualizations() {
|
||||
const {autoRefresh, timeRange, source, cells, onEditCell, onRenameCell, onUpdateCell, onDeleteCell, onSummonOverlayTechnologies, shouldNotBeEditable} = this.props
|
||||
const {timeRange, source, cells, onEditCell, onRenameCell, onUpdateCell, onDeleteCell, onSummonOverlayTechnologies, shouldNotBeEditable} = this.props
|
||||
|
||||
return cells.map((cell) => {
|
||||
const qs = cell.queries.map((query) => {
|
||||
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
|
||||
|
@ -112,29 +142,6 @@ export const LayoutRenderer = React.createClass({
|
|||
})
|
||||
})
|
||||
|
||||
if (cell.type === 'single-stat') {
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
<NameableGraph
|
||||
onEditCell={onEditCell}
|
||||
onRenameCell={onRenameCell}
|
||||
onUpdateCell={onUpdateCell}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
shouldNotBeEditable={shouldNotBeEditable}
|
||||
cell={cell}
|
||||
>
|
||||
<RefreshingSingleStat queries={[qs[0]]} autoRefresh={autoRefresh} />
|
||||
</NameableGraph>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: cell.type === 'line-stepplot',
|
||||
stackedGraph: cell.type === 'line-stacked',
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
<NameableGraph
|
||||
|
@ -146,12 +153,7 @@ export const LayoutRenderer = React.createClass({
|
|||
shouldNotBeEditable={shouldNotBeEditable}
|
||||
cell={cell}
|
||||
>
|
||||
<RefreshingLineGraph
|
||||
queries={qs}
|
||||
autoRefresh={autoRefresh}
|
||||
showSingleStat={cell.type === 'line-plus-single-stat'}
|
||||
displayOptions={displayOptions}
|
||||
/>
|
||||
{this.renderRefreshingGraph(cell.type, queries)}
|
||||
</NameableGraph>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classNames from 'classnames'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
|
||||
const Dropdown = React.createClass({
|
||||
propTypes: {
|
||||
children: PropTypes.node.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
text: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onChoose: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
isOpen: false,
|
||||
}
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.setState({isOpen: false})
|
||||
},
|
||||
handleSelection(item) {
|
||||
this.toggleMenu()
|
||||
this.props.onChoose(item)
|
||||
},
|
||||
toggleMenu(e) {
|
||||
if (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
},
|
||||
render() {
|
||||
const self = this
|
||||
const {items, className} = self.props
|
||||
|
||||
return (
|
||||
<div onClick={this.toggleMenu} className={classNames(`dropdown ${className}`, {open: self.state.isOpen})}>
|
||||
<div className="btn btn-sm btn-info dropdown-toggle">
|
||||
{this.props.children}
|
||||
</div>
|
||||
{self.state.isOpen ?
|
||||
<ul className="dropdown-menu show">
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<li className="dropdown-item" key={i} onClick={() => self.handleSelection(item)}>
|
||||
<a href="#">
|
||||
{item.text}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default OnClickOutside(Dropdown)
|
|
@ -0,0 +1 @@
|
|||
export const TEMPLATE_VARIABLE_SELECTED = 'TEMPLATE_VARIABLE_SELECTED'
|
|
@ -66,7 +66,7 @@ $dash-graph-options-arrow: 8px;
|
|||
}
|
||||
|
||||
.dashboard {
|
||||
.tv-control-bar {
|
||||
.template-control-bar {
|
||||
height: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
@ -77,6 +77,13 @@ $dash-graph-options-arrow: 8px;
|
|||
margin-bottom: 4px;
|
||||
padding: 10px 15px;
|
||||
@extend .cell-shell;
|
||||
.dropdown {
|
||||
flex: 0 1 auto;
|
||||
min-width: 100px;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.react-grid-item {
|
||||
@extend .cell-shell;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
|
||||
export const proxy = async ({source, query, db, rp}) => {
|
||||
export const proxy = async ({source, query, db, rp, templates}) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url: source,
|
||||
data: {
|
||||
templates,
|
||||
query,
|
||||
db,
|
||||
rp,
|
||||
|
|
Loading…
Reference in New Issue