feat(ui): Template variables can now select their source database

pull/5377/head
Bucky Schwarz 2020-02-05 09:56:10 -08:00 committed by Bucky Schwarz
parent 16ac349228
commit fff7836818
16 changed files with 1215 additions and 481 deletions

View File

@ -8,6 +8,7 @@
1. [#5348](https://github.com/influxdata/chronograf/pull/5348): Add query parameter for dashboard auto refresh
1. [#5352](https://github.com/influxdata/chronograf/pull/5352): Add etcd as an alternate backend store
1. [#5367](https://github.com/influxdata/chronograf/pull/5367): Template variables can now select their source database
### Other

View File

@ -183,10 +183,11 @@ type TemplateID string
// Template represents a series of choices to replace TemplateVars within InfluxQL
type Template struct {
TemplateVar
ID TemplateID `json:"id"` // ID is the unique ID associated with this template
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, csv, constant, measurements, databases, map, influxql, text
Label string `json:"label"` // Label is a user-facing description of the Template
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
ID TemplateID `json:"id"` // ID is the unique ID associated with this template
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, csv, constant, measurements, databases, map, influxql, text
Label string `json:"label"` // Label is a user-facing description of the Template
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
SourceID int `json:"sourceID,string"` // Source is the id of the data source
}
// Query retrieves a Response from a TimeSeries.

View File

@ -354,11 +354,12 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
}
template := &Template{
ID: string(t.ID),
TempVar: t.Var,
Values: vals,
Type: t.Type,
Label: t.Label,
ID: string(t.ID),
TempVar: t.Var,
Values: vals,
Type: t.Type,
Label: t.Label,
SourceID: int64(t.SourceID),
}
if t.Query != nil {
template.Query = &TemplateQuery{
@ -544,8 +545,9 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
Var: t.TempVar,
Values: vals,
},
Type: t.Type,
Label: t.Label,
Type: t.Type,
Label: t.Label,
SourceID: int(t.SourceID),
}
if t.Query != nil {

File diff suppressed because it is too large Load Diff

View File

@ -97,6 +97,7 @@ message Template {
string type = 4; // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
string label = 5; // Label is a user-facing description of the Template
TemplateQuery query = 6; // Query is used to generate the choices for a template
int64 sourceID = 7; // Source is the id of the data source
}
message TemplateValue {
@ -233,7 +234,7 @@ message LogViewerColumn {
repeated ColumnEncoding Encodings = 3; // Encodings is the array of encoded properties associated with a log viewer column
}
message ColumnEncoding {
message ColumnEncoding {
string Type = 1; // Type is the purpose of the encoding, for example: severity color
string Value = 2; // Value is what the encoding corresponds to
string Name = 3; // Name is the optional encoding name

View File

@ -43,7 +43,7 @@ func Test_Protoboards(t *testing.T) {
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `{"protoboards":[{"id":"1","meta":{"name":"protodashboard 1","icon":"http://example.com/icon.png","version":"1.2.3","measurements":["m1","m2"],"dashboardVersion":"1.7.0","description":"this is great","author":"Chronogiraffe","license":"Apache-2.0","url":"http://example.com"},"data":{"cells":[{"x":0,"y":0,"w":0,"h":0,"name":"","queries":null,"axes":null,"type":"","colors":null,"legend":{},"tableOptions":{"verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fixFirstColumn":false},"fieldOptions":null,"timeFormat":"","decimalPlaces":{"isEnforced":false,"digits":0},"note":"","noteVisibility":""}],"templates":[{"tempVar":"","values":null,"id":"","type":"","label":""}]},"links":{"self":"/chronograf/v1/protoboards/1"}},{"id":"2","meta":{"name":"protodashboard 2","icon":"http://example.com/icon.png","version":"1.2.3","measurements":["m1","m2"],"dashboardVersion":"1.7.0","description":"this is great","author":"Chronogiraffe","license":"Apache-2.0","url":"http://example.com"},"data":{"cells":[{"x":8,"y":0,"w":3,"h":5,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_steal\") AS \"mean_usage_steal\", mean(\"usage_system\") AS \"mean_usage_system\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"host\"='denizs-MacBook-Pro.local' GROUP BY time(:interval:) FILL(null)","queryConfig":{"database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_steal","args":[{"value":"usage_steal","type":"field","alias":""}]},{"value":"mean","type":"func","alias":"mean_usage_system","args":[{"value":"usage_steal","type":"field","alias":""}]}],"tags":{"host":["denizs-MacBook-Pro.local"]},"groupBy":{"time":"auto","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":null,"range":null,"shifts":null},"source":"","type":"influxql"}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"}},"type":"line","colors":[],"legend":{},"tableOptions":{"verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fixFirstColumn":false},"fieldOptions":[],"timeFormat":"","decimalPlaces":{"isEnforced":true,"digits":2},"note":"","noteVisibility":""}],"templates":null},"links":{"self":"/chronograf/v1/protoboards/2"}}]}`},
body: `{"protoboards":[{"id":"1","meta":{"name":"protodashboard 1","icon":"http://example.com/icon.png","version":"1.2.3","measurements":["m1","m2"],"dashboardVersion":"1.7.0","description":"this is great","author":"Chronogiraffe","license":"Apache-2.0","url":"http://example.com"},"data":{"cells":[{"x":0,"y":0,"w":0,"h":0,"name":"","queries":null,"axes":null,"type":"","colors":null,"legend":{},"tableOptions":{"verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fixFirstColumn":false},"fieldOptions":null,"timeFormat":"","decimalPlaces":{"isEnforced":false,"digits":0},"note":"","noteVisibility":""}],"templates":[{"tempVar":"","values":null,"id":"","type":"","label":"","sourceID":"0"}]},"links":{"self":"/chronograf/v1/protoboards/1"}},{"id":"2","meta":{"name":"protodashboard 2","icon":"http://example.com/icon.png","version":"1.2.3","measurements":["m1","m2"],"dashboardVersion":"1.7.0","description":"this is great","author":"Chronogiraffe","license":"Apache-2.0","url":"http://example.com"},"data":{"cells":[{"x":8,"y":0,"w":3,"h":5,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_steal\") AS \"mean_usage_steal\", mean(\"usage_system\") AS \"mean_usage_system\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"host\"='denizs-MacBook-Pro.local' GROUP BY time(:interval:) FILL(null)","queryConfig":{"database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_steal","args":[{"value":"usage_steal","type":"field","alias":""}]},{"value":"mean","type":"func","alias":"mean_usage_system","args":[{"value":"usage_steal","type":"field","alias":""}]}],"tags":{"host":["denizs-MacBook-Pro.local"]},"groupBy":{"time":"auto","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":null,"range":null,"shifts":null},"source":"","type":"influxql"}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"10","scale":"linear"}},"type":"line","colors":[],"legend":{},"tableOptions":{"verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fixFirstColumn":false},"fieldOptions":[],"timeFormat":"","decimalPlaces":{"isEnforced":true,"digits":2},"note":"","noteVisibility":""}],"templates":null},"links":{"self":"/chronograf/v1/protoboards/2"}}]}`},
arg: []chronograf.Protoboard{
{
ID: "1",

View File

@ -1,6 +1,6 @@
{
"name": "chronograf-ui",
"version": "1.7.17",
"version": "1.8.0",
"private": false,
"license": "AGPL-3.0",
"description": "",

View File

@ -731,7 +731,8 @@ const updateTimeRangeFromQueryParams = (dashboardID: string) => (
export const getDashboardWithTemplatesAsync = (
dashboardId: string,
source: Source
source: Source,
sources: Source[]
) => async (dispatch): Promise<void> => {
let dashboard: Dashboard
@ -747,7 +748,7 @@ export const getDashboardWithTemplatesAsync = (
const selections = templateSelectionsFromQueryParams()
let templates
templates = await hydrateTemplates(dashboard.templates, {
templates = await hydrateTemplates(dashboard.templates, sources, {
proxyUrl: source.links.proxy,
selections,
})
@ -777,11 +778,12 @@ export const getDashboardWithTemplatesAsync = (
export const rehydrateTemplatesAsync = (
dashboardId: string,
source: Source
source: Source,
sources: Source[]
) => async (dispatch, getState): Promise<void> => {
const dashboard = getDashboard(getState(), dashboardId)
const templates = await hydrateTemplates(dashboard.templates, {
const templates = await hydrateTemplates(dashboard.templates, sources, {
proxyUrl: source.links.proxy,
})

View File

@ -182,8 +182,6 @@ class CellEditorOverlay extends Component<Props, State> {
ceoTimeRange
)
return [...dashboardTemplates, dashboardTime, upperDashboardTime]
return dashboardTemplates
}
private get isSaveable(): boolean {

View File

@ -50,6 +50,7 @@ const SourceSelector: SFC<Props> = ({
isDynamicSourceSelected={isDynamicSourceSelected}
onChangeSource={onChangeSource}
onSelectDynamicSource={onSelectDynamicSource}
widthPixels={250}
/>
<Radio>
<Radio.Button

View File

@ -140,7 +140,7 @@ class DashboardPage extends Component<Props, State> {
}
}
public async componentDidMount() {
public componentDidMount() {
const {refreshRate, updateQueryParams} = this.props
const compareOptionToRefreshRate = (r: AutoRefreshOption) =>
r.milliseconds === refreshRate
@ -176,7 +176,7 @@ class DashboardPage extends Component<Props, State> {
window.addEventListener('resize', this.handleWindowResize, true)
await this.getDashboard()
this.getDashboard()
this.fetchAnnotations()
this.getDashboardLinks()
@ -383,10 +383,15 @@ class DashboardPage extends Component<Props, State> {
this.setState({windowHeight: window.innerHeight})
}
private getDashboard = async () => {
const {dashboardID, source, getDashboardWithTemplatesAsync} = this.props
private getDashboard = () => {
const {
dashboardID,
source,
sources,
getDashboardWithTemplatesAsync,
} = this.props
await getDashboardWithTemplatesAsync(dashboardID, source)
getDashboardWithTemplatesAsync(dashboardID, source, sources)
this.updateActiveDashboard()
}
@ -516,12 +521,13 @@ class DashboardPage extends Component<Props, State> {
const {
dashboard,
source,
sources,
templateVariableLocalSelected,
rehydrateTemplatesAsync,
} = this.props
templateVariableLocalSelected(dashboard.id, template.id, value)
rehydrateTemplatesAsync(dashboard.id, source)
rehydrateTemplatesAsync(dashboard.id, source, sources)
}
private handleSaveTemplateVariables = async (

View File

@ -18,6 +18,7 @@ interface Props {
isDynamicSourceSelected?: boolean
onSelectDynamicSource?: () => void
onChangeSource: (source: Source, type: QueryType) => void
widthPixels?: number
}
interface SourceDropdownItem {
@ -31,7 +32,7 @@ class SourceDropdown extends PureComponent<Props> {
<Dropdown
onChange={this.handleSelect}
selectedID={this.selectedID}
widthPixels={250}
widthPixels={this.props.widthPixels}
>
{this.dropdownItems}
</Dropdown>

View File

@ -1,3 +1,4 @@
// Libraries
import React, {
PureComponent,
ComponentClass,
@ -5,23 +6,26 @@ import React, {
KeyboardEvent,
} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import {get, isEmpty} from 'lodash'
// Utils
import {ErrorHandling} from 'src/shared/decorators/errors'
import OverlayContainer from 'src/reusable_ui/components/overlays/OverlayContainer'
import OverlayHeading from 'src/reusable_ui/components/overlays/OverlayHeading'
import OverlayBody from 'src/reusable_ui/components/overlays/OverlayBody'
import Dropdown from 'src/shared/components/Dropdown'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {notify as notifyActionCreator} from 'src/shared/actions/notifications'
import {formatTempVar} from 'src/tempVars/utils'
import {
reconcileSelectedAndLocalSelectedValues,
pickSelected,
} from 'src/dashboards/utils/tempVars'
// Actions
import {notify as notifyActionCreator} from 'src/shared/actions/notifications'
// Components
import ConfirmButton from 'src/shared/components/ConfirmButton'
import OverlayContainer from 'src/reusable_ui/components/overlays/OverlayContainer'
import OverlayHeading from 'src/reusable_ui/components/overlays/OverlayHeading'
import OverlayBody from 'src/reusable_ui/components/overlays/OverlayBody'
import Dropdown from 'src/shared/components/Dropdown'
import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder'
import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder'
import MapTemplateBuilder from 'src/tempVars/components/MapTemplateBuilder'
@ -31,7 +35,9 @@ import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuild
import TagValuesTemplateBuilder from 'src/tempVars/components/TagValuesTemplateBuilder'
import MetaQueryTemplateBuilder from 'src/tempVars/components/MetaQueryTemplateBuilder'
import TextTemplateBuilder from 'src/tempVars/components/TextTemplateBuilder'
import SourceDropdown from 'src/flux/components/SourceDropdown'
// Types
import {
Template,
TemplateType,
@ -40,7 +46,10 @@ import {
Source,
RemoteDataState,
Notification,
QueryType,
} from 'src/types'
// Constants
import {
TEMPLATE_TYPES_LIST,
DEFAULT_TEMPLATES,
@ -53,11 +62,13 @@ interface Props {
template?: Template
templates: Template[]
source: Source
sources: Source[]
onCancel: () => void
onCreate?: (template: Template) => Promise<any>
onUpdate?: (template: Template) => Promise<any>
onDelete?: () => Promise<any>
notify: (n: Notification) => void
isDynamicSourceSelected: boolean
}
interface State {
@ -65,6 +76,8 @@ interface State {
isNew: boolean
savingStatus: RemoteDataState
deletingStatus: RemoteDataState
isDynamicSourceSelected: boolean
selectedSource: Source
}
const TEMPLATE_BUILDERS = {
@ -81,14 +94,17 @@ const TEMPLATE_BUILDERS = {
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
const DEFAULT_SOURCE_DATABASE_ID = '0'
@ErrorHandling
class TemplateVariableEditor extends PureComponent<Props, State> {
constructor(props) {
super(props)
const defaultState = {
savingStatus: RemoteDataState.NotStarted,
deletingStatus: RemoteDataState.NotStarted,
isDynamicSourceSelected: props.isDynamicSourceSelected,
selectedSource: props.selectedSource,
}
const {template} = this.props
@ -109,8 +125,13 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
}
public render() {
const {source, onCancel, notify, templates} = this.props
const {nextTemplate, isNew} = this.state
const {sources, onCancel, notify, templates} = this.props
const {
isDynamicSourceSelected,
selectedSource,
nextTemplate,
isNew,
} = this.state
const TemplateBuilder = this.templateBuilder
return (
@ -136,7 +157,20 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
</OverlayHeading>
<OverlayBody>
<div className="faux-form">
<div className="form-group col-sm-6">
<div className="form-group col-sm-4">
<label>Data Source</label>
<SourceDropdown
onSelectDynamicSource={this.handleSelectDynamicSource}
sources={sources}
source={selectedSource}
isDynamicSourceSelected={isDynamicSourceSelected}
onChangeSource={this.handleChangeSource}
allowDynamicSource={true}
type={QueryType.InfluxQL}
widthPixels={0}
/>
</div>
<div className="form-group col-sm-4">
<label>Name</label>
<input
type="text"
@ -148,7 +182,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
spellCheck={false}
/>
</div>
<div className="form-group col-sm-6">
<div className="form-group col-sm-4">
<label>Type</label>
<Dropdown
items={TEMPLATE_TYPES_LIST}
@ -161,7 +195,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
<TemplateBuilder
template={nextTemplate}
templates={templates}
source={source}
source={selectedSource}
onUpdateTemplate={this.handleUpdateTemplate}
notify={notify}
onUpdateDefaultTemplateValue={
@ -291,7 +325,18 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
return
}
const {onUpdate, onCreate, notify} = this.props
const {nextTemplate, isNew} = this.state
const {
nextTemplate,
isNew,
isDynamicSourceSelected,
selectedSource,
} = this.state
nextTemplate.sourceID = DEFAULT_SOURCE_DATABASE_ID
if (!isDynamicSourceSelected) {
nextTemplate.sourceID = selectedSource.id
}
nextTemplate.tempVar = formatTempVar(nextTemplate.tempVar)
@ -331,7 +376,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
} = this.state
let canSaveValues = true
if (type === TemplateType.CSV && _.isEmpty(values)) {
if (type === TemplateType.CSV && isEmpty(values)) {
canSaveValues = false
}
@ -373,6 +418,22 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
return 'Save'
}
// When we change the source database here and below, we want to reset the template complete
private handleSelectDynamicSource = () => {
this.setState({
isDynamicSourceSelected: true,
nextTemplate: DEFAULT_TEMPLATE(),
})
}
private handleChangeSource = (source: Source) => {
this.setState({
isDynamicSourceSelected: false,
selectedSource: source,
nextTemplate: DEFAULT_TEMPLATE(),
})
}
private handleDelete = (): void => {
const {onDelete} = this.props
@ -384,6 +445,23 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
}
}
const mapDispatchToProps = {notify: notifyActionCreator}
const mapDispatchToProps = {
notify: notifyActionCreator,
}
export default connect(null, mapDispatchToProps)(TemplateVariableEditor)
const mapStateToProps = (state, props) => {
const {sources} = state
const sourceID = get(props, 'template.sourceID', DEFAULT_SOURCE_DATABASE_ID)
const selectedSource = sources.find(source => source.id === sourceID)
const isDynamicSourceSelected =
selectedSource === undefined || sourceID === DEFAULT_SOURCE_DATABASE_ID
return {
sources,
isDynamicSourceSelected,
selectedSource,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(
TemplateVariableEditor
)

View File

@ -7,7 +7,7 @@ import templateReplace, {
import {resolveValues} from 'src/tempVars/utils'
import {Template, RemoteDataState} from 'src/types'
import {Source, Template, RemoteDataState} from 'src/types'
type TemplateName = string
@ -240,6 +240,7 @@ export async function hydrateTemplate(
export async function hydrateTemplates(
templates: Template[],
sources: Source[],
hydrateOptions: HydrateTemplateOptions
) {
const graph = graphFromTemplates(templates)
@ -251,10 +252,17 @@ export async function hydrateTemplates(
node.status = RemoteDataState.Loading
const templateSource = sources.find(
s => s.id === node.initialTemplate.sourceID
)
const proxyUrl = templateSource
? templateSource.links.proxy
: hydrateOptions.proxyUrl
node.hydratedTemplate = await hydrateTemplate(
node.initialTemplate,
resolvedTemplates,
hydrateOptions
{...hydrateOptions, proxyUrl}
)
node.status = RemoteDataState.Done

View File

@ -54,6 +54,7 @@ export interface Template {
type: TemplateType
label: string
query?: TemplateQuery
sourceID?: string
}
export interface TemplateUpdate {

View File

@ -382,7 +382,7 @@ describe('hydrateTemplates', () => {
},
}
const result = await hydrateTemplates(templates, {fetcher: fakeFetcher})
const result = await hydrateTemplates(templates, [], {fetcher: fakeFetcher})
expect(result.find(t => t.id === 'a').values).toContainEqual({
value: 'success',