Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
5d11d8fa
Unverified
Commit
5d11d8fa
authored
Sep 11, 2020
by
Ryan McKinley
Committed by
GitHub
Sep 11, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Annotations: add standard annotations support (and use it for flux queries) (#27375)
parent
4707508f
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
948 additions
and
87 deletions
+948
-87
packages/grafana-data/src/types/annotations.ts
+87
-0
packages/grafana-data/src/types/data.ts
+0
-21
packages/grafana-data/src/types/datasource.ts
+25
-22
packages/grafana-data/src/types/index.ts
+1
-0
packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
+2
-0
public/app/features/annotations/annotations_srv.ts
+92
-17
public/app/features/annotations/components/AnnotationResultMapper.tsx
+195
-0
public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx
+186
-0
public/app/features/annotations/editor_ctrl.ts
+17
-1
public/app/features/annotations/partials/editor.html
+9
-1
public/app/features/annotations/standardAnnotationSupport.test.ts
+83
-0
public/app/features/annotations/standardAnnotationSupport.ts
+188
-0
public/app/features/annotations/types.ts
+25
-0
public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx
+12
-15
public/app/plugins/datasource/influxdb/components/VariableQueryEditor.tsx
+4
-3
public/app/plugins/datasource/influxdb/datasource.ts
+19
-1
public/app/plugins/datasource/influxdb/partials/query.editor.html
+3
-3
public/app/plugins/datasource/loki/datasource.test.ts
+0
-3
No files found.
packages/grafana-data/src/types/annotations.ts
0 → 100644
View file @
5d11d8fa
import
{
DataQuery
,
QueryEditorProps
}
from
'./datasource'
;
import
{
DataFrame
}
from
'./dataFrame'
;
import
{
ComponentType
}
from
'react'
;
/**
* This JSON object is stored in the dashboard json model.
*/
export
interface
AnnotationQuery
<
TQuery
extends
DataQuery
=
DataQuery
>
{
datasource
:
string
;
enable
:
boolean
;
name
:
string
;
iconColor
:
string
;
// Standard datasource query
target
?:
TQuery
;
// Convert a dataframe to an AnnotationEvent
mappings
?:
AnnotationEventMappings
;
}
export
interface
AnnotationEvent
{
id
?:
string
;
annotation
?:
any
;
dashboardId
?:
number
;
panelId
?:
number
;
userId
?:
number
;
login
?:
string
;
email
?:
string
;
avatarUrl
?:
string
;
time
?:
number
;
timeEnd
?:
number
;
isRegion
?:
boolean
;
title
?:
string
;
text
?:
string
;
type
?:
string
;
tags
?:
string
[];
// Currently used to merge annotations from alerts and dashboard
source
?:
any
;
// source.type === 'dashboard'
}
/**
* @alpha -- any value other than `field` is experimental
*/
export
enum
AnnotationEventFieldSource
{
Field
=
'field'
,
// Default -- find the value with a matching key
Text
=
'text'
,
// Write a constant string into the value
Skip
=
'skip'
,
// Do not include the field
}
export
interface
AnnotationEventFieldMapping
{
source
?:
AnnotationEventFieldSource
;
// defautls to 'field'
value
?:
string
;
regex
?:
string
;
}
export
type
AnnotationEventMappings
=
Partial
<
Record
<
keyof
AnnotationEvent
,
AnnotationEventFieldMapping
>>
;
/**
* Since Grafana 7.2
*
* This offers a generic approach to annotation processing
*/
export
interface
AnnotationSupport
<
TQuery
extends
DataQuery
=
DataQuery
,
TAnno
=
AnnotationQuery
<
TQuery
>>
{
/**
* This hook lets you manipulate any existing stored values before running them though the processor.
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object
*/
prepareAnnotation
?(
json
:
any
):
TAnno
;
/**
* Convert the stored JSON model to a standard datasource query object.
* This query will be executed in the datasource and the results converted into events.
* Returning an undefined result will quietly skip query execution
*/
prepareQuery
?(
anno
:
TAnno
):
TQuery
|
undefined
;
/**
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
*/
processEvents
?(
anno
:
TAnno
,
data
:
DataFrame
):
AnnotationEvent
[]
|
undefined
;
/**
* Specify a custom QueryEditor for the annotation page. If not specified, the standard one will be used
*/
QueryEditor
?:
ComponentType
<
QueryEditorProps
<
any
,
TQuery
>>
;
}
packages/grafana-data/src/types/data.ts
View file @
5d11d8fa
...
...
@@ -132,27 +132,6 @@ export enum NullValueMode {
AsZero
=
'null as zero'
,
}
export
interface
AnnotationEvent
{
id
?:
string
;
annotation
?:
any
;
dashboardId
?:
number
;
panelId
?:
number
;
userId
?:
number
;
login
?:
string
;
email
?:
string
;
avatarUrl
?:
string
;
time
?:
number
;
timeEnd
?:
number
;
isRegion
?:
boolean
;
title
?:
string
;
text
?:
string
;
type
?:
string
;
tags
?:
string
[];
// Currently used to merge annotations from alerts and dashboard
source
?:
any
;
// source.type === 'dashboard'
}
/**
* Describes and API for exposing panel specific data configurations.
*/
...
...
packages/grafana-data/src/types/datasource.ts
View file @
5d11d8fa
...
...
@@ -3,7 +3,8 @@ import { ComponentType } from 'react';
import
{
GrafanaPlugin
,
PluginMeta
}
from
'./plugin'
;
import
{
PanelData
}
from
'./panel'
;
import
{
LogRowModel
}
from
'./logs'
;
import
{
AnnotationEvent
,
KeyValue
,
LoadingState
,
TableData
,
TimeSeries
}
from
'./data'
;
import
{
AnnotationEvent
,
AnnotationSupport
}
from
'./annotations'
;
import
{
KeyValue
,
LoadingState
,
TableData
,
TimeSeries
}
from
'./data'
;
import
{
DataFrame
,
DataFrameDTO
}
from
'./dataFrame'
;
import
{
RawTimeRange
,
TimeRange
}
from
'./time'
;
import
{
ScopedVars
}
from
'./ScopedVars'
;
...
...
@@ -155,8 +156,7 @@ export interface DataSourceConstructor<
*/
export
abstract
class
DataSourceApi
<
TQuery
extends
DataQuery
=
DataQuery
,
TOptions
extends
DataSourceJsonData
=
DataSourceJsonData
,
TAnno
=
TQuery
// defatult to direct query
TOptions
extends
DataSourceJsonData
=
DataSourceJsonData
>
{
/**
* Set in constructor
...
...
@@ -267,13 +267,23 @@ export abstract class DataSourceApi<
showContextToggle
?(
row
?:
LogRowModel
):
boolean
;
interpolateVariablesInQueries
?(
queries
:
TQuery
[],
scopedVars
:
ScopedVars
|
{}):
TQuery
[];
/**
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
* An annotation processor allows explict control for how annotations are managed.
*
* It is only necessary to configure an annotation processor if the default behavior is not desirable
*/
annotation
Query
?(
options
:
AnnotationQueryRequest
<
TAnno
>
):
Promise
<
AnnotationEvent
[]
>
;
annotation
s
?:
AnnotationSupport
<
TQuery
>
;
interpolateVariablesInQueries
?(
queries
:
TQuery
[],
scopedVars
:
ScopedVars
|
{}):
TQuery
[];
/**
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard.
* This function will only be called if an angular {@link AnnotationsQueryCtrl} is configured and
* the {@link annotations} is undefined
*
* @deprecated -- prefer using {@link AnnotationSupport}
*/
annotationQuery
?(
options
:
AnnotationQueryRequest
<
TQuery
>
):
Promise
<
AnnotationEvent
[]
>
;
}
export
interface
MetadataInspectorProps
<
...
...
@@ -473,12 +483,6 @@ export interface MetricFindValue {
expandable
?:
boolean
;
}
export
interface
BaseAnnotationQuery
{
datasource
:
string
;
enable
:
boolean
;
name
:
string
;
}
export
interface
DataSourceJsonData
{
authType
?:
string
;
defaultRegion
?:
string
;
...
...
@@ -547,20 +551,19 @@ export interface DataSourceSelectItem {
/**
* Options passed to the datasource.annotationQuery method. See docs/plugins/developing/datasource.md
*
* @deprecated -- use {@link AnnotationSupport}
*/
export
interface
AnnotationQueryRequest
<
TAnno
=
{}
>
{
export
interface
AnnotationQueryRequest
<
MoreOptions
=
{}
>
{
range
:
TimeRange
;
rangeRaw
:
RawTimeRange
;
interval
:
string
;
intervalMs
:
number
;
maxDataPoints
?:
number
;
app
:
CoreApp
|
string
;
// Should be DataModel but cannot import that here from the main app. Needs to be moved to package first.
dashboard
:
any
;
// The annotation query and common properties
annotation
:
BaseAnnotationQuery
&
TAnno
;
annotation
:
{
datasource
:
string
;
enable
:
boolean
;
name
:
string
;
}
&
MoreOptions
;
}
export
interface
HistoryItem
<
TQuery
extends
DataQuery
=
DataQuery
>
{
...
...
packages/grafana-data/src/types/index.ts
View file @
5d11d8fa
export
*
from
'./data'
;
export
*
from
'./dataFrame'
;
export
*
from
'./dataLink'
;
export
*
from
'./annotations'
;
export
*
from
'./logs'
;
export
*
from
'./navModel'
;
export
*
from
'./select'
;
...
...
packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
View file @
5d11d8fa
...
...
@@ -122,6 +122,8 @@ export class DataSourceWithBackend<
/**
* Override to skip executing a query
*
* @returns false if the query should be skipped
*
* @virtual
*/
filterQuery
?(
query
:
TQuery
):
boolean
;
...
...
public/app/features/annotations/annotations_srv.ts
View file @
5d11d8fa
...
...
@@ -7,12 +7,30 @@ import coreModule from 'app/core/core_module';
// Utils & Services
import
{
dedupAnnotations
}
from
'./events_processing'
;
// Types
import
{
DashboardModel
,
PanelModel
}
from
'../dashboard/state'
;
import
{
AnnotationEvent
,
AppEvents
,
DataSourceApi
,
PanelEvents
,
TimeRange
,
CoreApp
}
from
'@grafana/data'
;
import
{
DashboardModel
}
from
'../dashboard/state'
;
import
{
AnnotationEvent
,
AppEvents
,
DataSourceApi
,
PanelEvents
,
rangeUtil
,
DataQueryRequest
,
CoreApp
,
ScopedVars
,
}
from
'@grafana/data'
;
import
{
getBackendSrv
,
getDataSourceSrv
}
from
'@grafana/runtime'
;
import
{
appEvents
}
from
'app/core/core'
;
import
{
getTimeSrv
}
from
'../dashboard/services/TimeSrv'
;
import
kbn
from
'app/core/utils/kbn'
;
import
{
Observable
,
of
}
from
'rxjs'
;
import
{
map
}
from
'rxjs/operators'
;
import
{
AnnotationQueryResponse
,
AnnotationQueryOptions
}
from
'./types'
;
import
{
standardAnnotationSupport
,
singleFrameFromPanelData
}
from
'./standardAnnotationSupport'
;
import
{
runRequest
}
from
'../dashboard/state/runRequest'
;
let
counter
=
100
;
function
getNextRequestId
()
{
return
'AQ'
+
counter
++
;
}
export
class
AnnotationsSrv
{
globalAnnotationsPromise
:
any
;
...
...
@@ -32,7 +50,7 @@ export class AnnotationsSrv {
this
.
datasourcePromises
=
null
;
}
getAnnotations
(
options
:
{
dashboard
:
DashboardModel
;
panel
:
PanelModel
;
range
:
TimeRange
}
)
{
getAnnotations
(
options
:
AnnotationQueryOptions
)
{
return
Promise
.
all
([
this
.
getGlobalAnnotations
(
options
),
this
.
getAlertStates
(
options
)])
.
then
(
results
=>
{
// combine the annotations and flatten results
...
...
@@ -103,7 +121,7 @@ export class AnnotationsSrv {
return
this
.
alertStatesPromise
;
}
getGlobalAnnotations
(
options
:
{
dashboard
:
DashboardModel
;
panel
:
PanelModel
;
range
:
TimeRange
}
)
{
getGlobalAnnotations
(
options
:
AnnotationQueryOptions
)
{
const
dashboard
=
options
.
dashboard
;
if
(
this
.
globalAnnotationsPromise
)
{
...
...
@@ -114,9 +132,6 @@ export class AnnotationsSrv {
const
promises
=
[];
const
dsPromises
=
[];
// No more points than pixels
const
maxDataPoints
=
window
.
innerWidth
||
document
.
documentElement
.
clientWidth
||
document
.
body
.
clientWidth
;
for
(
const
annotation
of
dashboard
.
annotations
.
list
)
{
if
(
!
annotation
.
enable
)
{
continue
;
...
...
@@ -130,21 +145,21 @@ export class AnnotationsSrv {
promises
.
push
(
datasourcePromise
.
then
((
datasource
:
DataSourceApi
)
=>
{
if
(
!
datasource
.
annotationQuery
)
{
return
[];
}
// Add interval to annotation queries
const
interval
=
kbn
.
calculateInterval
(
range
,
maxDataPoints
,
datasource
.
interval
);
// Use the legacy annotationQuery unless annotation support is explicitly defined
if
(
datasource
.
annotationQuery
&&
!
datasource
.
annotations
)
{
return
datasource
.
annotationQuery
({
...
interval
,
app
:
CoreApp
.
Dashboard
,
range
,
rangeRaw
:
range
.
raw
,
annotation
:
annotation
,
dashboard
:
dashboard
,
});
}
// Note: future annotatoin lifecycle will use observables directly
return
executeAnnotationQuery
(
options
,
datasource
,
annotation
)
.
toPromise
()
.
then
(
res
=>
{
return
res
.
events
??
[];
});
})
.
then
(
results
=>
{
// store response in annotation object if this is a snapshot call
...
...
@@ -195,4 +210,64 @@ export class AnnotationsSrv {
}
}
export
function
executeAnnotationQuery
(
options
:
AnnotationQueryOptions
,
datasource
:
DataSourceApi
,
savedJsonAnno
:
any
):
Observable
<
AnnotationQueryResponse
>
{
const
processor
=
{
...
standardAnnotationSupport
,
...
datasource
.
annotations
,
};
const
annotation
=
processor
.
prepareAnnotation
!
(
savedJsonAnno
);
if
(
!
annotation
)
{
return
of
({});
}
const
query
=
processor
.
prepareQuery
!
(
annotation
);
if
(
!
query
)
{
return
of
({});
}
// No more points than pixels
const
maxDataPoints
=
window
.
innerWidth
||
document
.
documentElement
.
clientWidth
||
document
.
body
.
clientWidth
;
// Add interval to annotation queries
const
interval
=
rangeUtil
.
calculateInterval
(
options
.
range
,
maxDataPoints
,
datasource
.
interval
);
const
scopedVars
:
ScopedVars
=
{
__interval
:
{
text
:
interval
.
interval
,
value
:
interval
.
interval
},
__interval_ms
:
{
text
:
interval
.
intervalMs
.
toString
(),
value
:
interval
.
intervalMs
},
__annotation
:
{
text
:
annotation
.
name
,
value
:
annotation
},
};
const
queryRequest
:
DataQueryRequest
=
{
startTime
:
Date
.
now
(),
requestId
:
getNextRequestId
(),
range
:
options
.
range
,
maxDataPoints
,
scopedVars
,
...
interval
,
app
:
CoreApp
.
Dashboard
,
timezone
:
options
.
dashboard
.
timezone
,
targets
:
[
{
...
query
,
refId
:
'Anno'
,
},
],
};
return
runRequest
(
datasource
,
queryRequest
).
pipe
(
map
(
panelData
=>
{
const
frame
=
singleFrameFromPanelData
(
panelData
);
const
events
=
frame
?
processor
.
processEvents
!
(
annotation
,
frame
)
:
[];
return
{
panelData
,
frame
,
events
};
})
);
}
coreModule
.
service
(
'annotationsSrv'
,
AnnotationsSrv
);
public/app/features/annotations/components/AnnotationResultMapper.tsx
0 → 100644
View file @
5d11d8fa
import
React
,
{
PureComponent
}
from
'react'
;
import
{
SelectableValue
,
getFieldDisplayName
,
AnnotationEvent
,
AnnotationEventMappings
,
AnnotationEventFieldMapping
,
formattedValueToString
,
AnnotationEventFieldSource
,
getValueFormat
,
}
from
'@grafana/data'
;
import
{
annotationEventNames
,
AnnotationFieldInfo
}
from
'../standardAnnotationSupport'
;
import
{
Select
,
Tooltip
,
Icon
}
from
'@grafana/ui'
;
import
{
AnnotationQueryResponse
}
from
'../types'
;
// const valueOptions: Array<SelectableValue<AnnotationEventFieldSource>> = [
// { value: AnnotationEventFieldSource.Field, label: 'Field', description: 'Set the field value from a response field' },
// { value: AnnotationEventFieldSource.Text, label: 'Text', description: 'Enter direct text for the value' },
// { value: AnnotationEventFieldSource.Skip, label: 'Skip', description: 'Hide this field' },
// ];
interface
Props
{
response
?:
AnnotationQueryResponse
;
mappings
?:
AnnotationEventMappings
;
change
:
(
mappings
?:
AnnotationEventMappings
)
=>
void
;
}
interface
State
{
fieldNames
:
Array
<
SelectableValue
<
string
>>
;
}
export
class
AnnotationFieldMapper
extends
PureComponent
<
Props
,
State
>
{
constructor
(
props
:
Props
)
{
super
(
props
);
this
.
state
=
{
fieldNames
:
[],
};
}
updateFields
=
()
=>
{
const
frame
=
this
.
props
.
response
?.
frame
;
if
(
frame
&&
frame
.
fields
)
{
const
fieldNames
=
frame
.
fields
.
map
(
f
=>
{
const
name
=
getFieldDisplayName
(
f
,
frame
);
let
description
=
''
;
for
(
let
i
=
0
;
i
<
frame
.
length
;
i
++
)
{
if
(
i
>
0
)
{
description
+=
', '
;
}
if
(
i
>
2
)
{
description
+=
'...'
;
break
;
}
description
+=
f
.
values
.
get
(
i
);
}
if
(
description
.
length
>
50
)
{
description
=
description
.
substring
(
0
,
50
)
+
'...'
;
}
return
{
label
:
`
${
name
}
(
${
f
.
type
}
)`
,
value
:
name
,
description
,
};
});
this
.
setState
({
fieldNames
});
}
};
componentDidMount
()
{
this
.
updateFields
();
}
componentDidUpdate
(
oldProps
:
Props
)
{
if
(
oldProps
.
response
!==
this
.
props
.
response
)
{
this
.
updateFields
();
}
}
onFieldSourceChange
=
(
k
:
keyof
AnnotationEvent
,
v
:
SelectableValue
<
AnnotationEventFieldSource
>
)
=>
{
const
mappings
=
this
.
props
.
mappings
||
{};
const
mapping
=
mappings
[
k
]
||
{};
this
.
props
.
change
({
...
mappings
,
[
k
]:
{
...
mapping
,
source
:
v
.
value
||
AnnotationEventFieldSource
.
Field
,
},
});
};
onFieldNameChange
=
(
k
:
keyof
AnnotationEvent
,
v
:
SelectableValue
<
string
>
)
=>
{
const
mappings
=
this
.
props
.
mappings
||
{};
const
mapping
=
mappings
[
k
]
||
{};
this
.
props
.
change
({
...
mappings
,
[
k
]:
{
...
mapping
,
value
:
v
.
value
,
source
:
AnnotationEventFieldSource
.
Field
,
},
});
};
renderRow
(
row
:
AnnotationFieldInfo
,
mapping
:
AnnotationEventFieldMapping
,
first
?:
AnnotationEvent
)
{
const
{
fieldNames
}
=
this
.
state
;
let
picker
=
fieldNames
;
const
current
=
mapping
.
value
;
let
currentValue
=
fieldNames
.
find
(
f
=>
current
===
f
.
value
);
if
(
current
)
{
picker
=
[...
fieldNames
];
if
(
!
currentValue
)
{
picker
.
push
({
label
:
current
,
value
:
current
,
});
}
}
let
value
=
first
?
first
[
row
.
key
]
:
''
;
if
(
value
&&
row
.
key
.
startsWith
(
'time'
))
{
const
fmt
=
getValueFormat
(
'dateTimeAsIso'
);
value
=
formattedValueToString
(
fmt
(
value
as
number
));
}
if
(
value
===
null
||
value
===
undefined
)
{
value
=
''
;
// empty string
}
return
(
<
tr
key=
{
row
.
key
}
>
<
td
>
{
row
.
key
}{
' '
}
{
row
.
help
&&
(
<
Tooltip
content=
{
row
.
help
}
>
<
Icon
name=
"info-circle"
/>
</
Tooltip
>
)
}
</
td
>
{
/* <td>
<Select
value={valueOptions.find(v => v.value === mapping.source) || valueOptions[0]}
options={valueOptions}
onChange={(v: SelectableValue<AnnotationEventFieldSource>) => {
this.onFieldSourceChange(row.key, v);
}}
/>
</td> */
}
<
td
>
<
Select
value=
{
currentValue
}
options=
{
picker
}
placeholder=
{
row
.
placeholder
||
row
.
key
}
onChange=
{
(
v
:
SelectableValue
<
string
>
)
=>
{
this
.
onFieldNameChange
(
row
.
key
,
v
);
}
}
noOptionsMessage=
"Unknown field names"
allowCustomValue=
{
true
}
/>
</
td
>
<
td
>
{
`${value}`
}
</
td
>
</
tr
>
);
}
render
()
{
const
first
=
this
.
props
.
response
?.
events
?.[
0
];
const
mappings
=
this
.
props
.
mappings
||
{};
return
(
<
table
className=
"filter-table"
>
<
thead
>
<
tr
>
<
th
>
Annotation
</
th
>
<
th
>
From
</
th
>
<
th
>
First Value
</
th
>
</
tr
>
</
thead
>
<
tbody
>
{
annotationEventNames
.
map
(
row
=>
{
return
this
.
renderRow
(
row
,
mappings
[
row
.
key
]
||
{},
first
);
})
}
</
tbody
>
</
table
>
);
}
}
public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx
0 → 100644
View file @
5d11d8fa
import
React
,
{
PureComponent
}
from
'react'
;
import
{
AnnotationEventMappings
,
DataQuery
,
LoadingState
,
DataSourceApi
,
AnnotationQuery
}
from
'@grafana/data'
;
import
{
Spinner
,
Icon
,
IconName
,
Button
}
from
'@grafana/ui'
;
import
{
getDashboardSrv
}
from
'app/features/dashboard/services/DashboardSrv'
;
import
{
getTimeSrv
}
from
'app/features/dashboard/services/TimeSrv'
;
import
{
cx
,
css
}
from
'emotion'
;
import
{
standardAnnotationSupport
}
from
'../standardAnnotationSupport'
;
import
{
executeAnnotationQuery
}
from
'../annotations_srv'
;
import
{
PanelModel
}
from
'app/features/dashboard/state'
;
import
{
AnnotationQueryResponse
}
from
'../types'
;
import
{
AnnotationFieldMapper
}
from
'./AnnotationResultMapper'
;
import
coreModule
from
'app/core/core_module'
;
interface
Props
{
datasource
:
DataSourceApi
;
annotation
:
AnnotationQuery
<
DataQuery
>
;
change
:
(
annotation
:
AnnotationQuery
<
DataQuery
>
)
=>
void
;
}
interface
State
{
running
?:
boolean
;
response
?:
AnnotationQueryResponse
;
}
export
default
class
StandardAnnotationQueryEditor
extends
PureComponent
<
Props
,
State
>
{
state
=
{}
as
State
;
componentDidMount
()
{
this
.
verifyDataSource
();
}
componentDidUpdate
(
oldProps
:
Props
)
{
if
(
this
.
props
.
annotation
!==
oldProps
.
annotation
)
{
this
.
verifyDataSource
();
}
}
verifyDataSource
()
{
const
{
datasource
,
annotation
}
=
this
.
props
;
// Handle any migration issues
const
processor
=
{
...
standardAnnotationSupport
,
...
datasource
.
annotations
,
};
const
fixed
=
processor
.
prepareAnnotation
!
(
annotation
);
if
(
fixed
!==
annotation
)
{
this
.
props
.
change
(
fixed
);
}
else
{
this
.
onRunQuery
();
}
}
onRunQuery
=
async
()
=>
{
const
{
datasource
,
annotation
}
=
this
.
props
;
this
.
setState
({
running
:
true
,
});
const
response
=
await
executeAnnotationQuery
(
{
range
:
getTimeSrv
().
timeRange
(),
panel
:
{}
as
PanelModel
,
dashboard
:
getDashboardSrv
().
getCurrent
(),
},
datasource
,
annotation
).
toPromise
();
this
.
setState
({
running
:
false
,
response
,
});
};
onQueryChange
=
(
target
:
DataQuery
)
=>
{
this
.
props
.
change
({
...
this
.
props
.
annotation
,
target
,
});
};
onMappingChange
=
(
mappings
:
AnnotationEventMappings
)
=>
{
this
.
props
.
change
({
...
this
.
props
.
annotation
,
mappings
,
});
};
renderStatus
()
{
const
{
response
,
running
}
=
this
.
state
;
let
rowStyle
=
'alert-info'
;
let
text
=
'...'
;
let
icon
:
IconName
|
undefined
=
undefined
;
if
(
running
||
response
?.
panelData
?.
state
===
LoadingState
.
Loading
||
!
response
)
{
text
=
'loading...'
;
}
else
{
const
{
events
,
panelData
,
frame
}
=
response
;
if
(
panelData
?.
error
)
{
rowStyle
=
'alert-error'
;
icon
=
'exclamation-triangle'
;
text
=
panelData
.
error
.
message
??
'error'
;
}
else
if
(
!
events
?.
length
)
{
rowStyle
=
'alert-warning'
;
icon
=
'exclamation-triangle'
;
text
=
'No events found'
;
}
else
{
text
=
`
${
events
.
length
}
events (from
${
frame
?.
fields
.
length
}
fields
)
`;
}
}
return (
<div
className={cx(
rowStyle,
css`
margin
:
4
px
0
px
;
padding
:
4
px
;
display
:
flex
;
justify
-
content
:
space
-
between
;
align
-
items
:
center
;
`
)}
>
<div>
{icon && (
<>
<Icon name={icon} />
</>
)}
{text}
</div>
<div>
{running ? (
<Spinner />
) : (
<Button variant="secondary" size="xs" onClick={this.onRunQuery}>
TEST
</Button>
)}
</div>
</div>
);
}
render() {
const { datasource, annotation } = this.props;
const { response } = this.state;
// Find the annotaiton runner
let QueryEditor = datasource.annotations?.QueryEditor || datasource.components?.QueryEditor;
if (!QueryEditor) {
return <div>Annotations are not supported. This datasource needs to export a QueryEditor</div>;
}
const query = annotation.target ?? { refId: 'Anno' };
return (
<>
<QueryEditor
key={datasource?.name}
query={query}
datasource={datasource}
onChange={this.onQueryChange}
onRunQuery={this.onRunQuery}
data={response?.panelData}
range={getTimeSrv().timeRange()}
/>
{this.renderStatus()}
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
<br />
</>
);
}
}
// Careful to use a unique directive name! many plugins already use "annotationEditor" and have conflicts
coreModule.directive('standardAnnotationEditor', [
'reactDirective',
(reactDirective: any) => {
return reactDirective(StandardAnnotationQueryEditor, ['annotation', 'datasource', 'change']);
},
]);
public/app/features/annotations/editor_ctrl.ts
View file @
5d11d8fa
...
...
@@ -7,10 +7,13 @@ import DatasourceSrv from '../plugins/datasource_srv';
import
appEvents
from
'app/core/app_events'
;
import
{
AppEvents
}
from
'@grafana/data'
;
// Registeres the angular directive
import
'./components/StandardAnnotationQueryEditor'
;
export
class
AnnotationsEditorCtrl
{
mode
:
any
;
datasources
:
any
;
annotations
:
any
;
annotations
:
any
[]
;
currentAnnotation
:
any
;
currentDatasource
:
any
;
currentIsNew
:
any
;
...
...
@@ -69,6 +72,19 @@ export class AnnotationsEditorCtrl {
});
}
/**
* Called from the react editor
*/
onAnnotationChange
=
(
annotation
:
any
)
=>
{
const
currentIndex
=
this
.
dashboard
.
annotations
.
list
.
indexOf
(
this
.
currentAnnotation
);
if
(
currentIndex
>=
0
)
{
this
.
dashboard
.
annotations
.
list
[
currentIndex
]
=
annotation
;
}
else
{
console
.
warn
(
'updating annotatoin, but not in the dashboard'
,
annotation
);
}
this
.
currentAnnotation
=
annotation
;
};
edit
(
annotation
:
any
)
{
this
.
currentAnnotation
=
annotation
;
this
.
currentAnnotation
.
showIn
=
this
.
currentAnnotation
.
showIn
||
0
;
...
...
public/app/features/annotations/partials/editor.html
View file @
5d11d8fa
...
...
@@ -117,7 +117,15 @@
<h5
class=
"section-heading"
>
Query
</h5>
<rebuild-on-change
property=
"ctrl.currentDatasource"
>
<plugin-component
type=
"annotations-query-ctrl"
>
</plugin-component>
<!-- Legacy angular -->
<plugin-component
ng-if=
"!ctrl.currentDatasource.annotations"
type=
"annotations-query-ctrl"
>
</plugin-component>
<!-- React query editor -->
<standard-annotation-editor
ng-if=
"ctrl.currentDatasource.annotations"
annotation=
"ctrl.currentAnnotation"
datasource=
"ctrl.currentDatasource"
change=
"ctrl.onAnnotationChange"
/>
</rebuild-on-change>
<div
class=
"gf-form"
>
...
...
public/app/features/annotations/standardAnnotationSupport.test.ts
0 → 100644
View file @
5d11d8fa
import
{
toDataFrame
,
FieldType
}
from
'@grafana/data'
;
import
{
getAnnotationsFromFrame
}
from
'./standardAnnotationSupport'
;
describe
(
'DataFrame to annotations'
,
()
=>
{
test
(
'simple conversion'
,
()
=>
{
const
frame
=
toDataFrame
({
fields
:
[
{
type
:
FieldType
.
time
,
values
:
[
1
,
2
,
3
]
},
{
name
:
'first string field'
,
values
:
[
't1'
,
't2'
,
't3'
]
},
{
name
:
'tags'
,
values
:
[
'aaa,bbb'
,
'bbb,ccc'
,
'zyz'
]
},
],
});
const
events
=
getAnnotationsFromFrame
(
frame
);
expect
(
events
).
toMatchInlineSnapshot
(
`
Array [
Object {
"tags": Array [
"aaa",
"bbb",
],
"text": "t1",
"time": 1,
},
Object {
"tags": Array [
"bbb",
"ccc",
],
"text": "t2",
"time": 2,
},
Object {
"tags": Array [
"zyz",
],
"text": "t3",
"time": 3,
},
]
`
);
});
test
(
'explicit mappins'
,
()
=>
{
const
frame
=
toDataFrame
({
fields
:
[
{
name
:
'time1'
,
values
:
[
111
,
222
,
333
]
},
{
name
:
'time2'
,
values
:
[
100
,
200
,
300
]
},
{
name
:
'aaaaa'
,
values
:
[
'a1'
,
'a2'
,
'a3'
]
},
{
name
:
'bbbbb'
,
values
:
[
'b1'
,
'b2'
,
'b3'
]
},
],
});
const
events
=
getAnnotationsFromFrame
(
frame
,
{
text
:
{
value
:
'bbbbb'
},
time
:
{
value
:
'time2'
},
timeEnd
:
{
value
:
'time1'
},
title
:
{
value
:
'aaaaa'
},
});
expect
(
events
).
toMatchInlineSnapshot
(
`
Array [
Object {
"text": "b1",
"time": 100,
"timeEnd": 111,
"title": "a1",
},
Object {
"text": "b2",
"time": 200,
"timeEnd": 222,
"title": "a2",
},
Object {
"text": "b3",
"time": 300,
"timeEnd": 333,
"title": "a3",
},
]
`
);
});
});
public/app/features/annotations/standardAnnotationSupport.ts
0 → 100644
View file @
5d11d8fa
import
{
DataFrame
,
AnnotationQuery
,
AnnotationSupport
,
PanelData
,
transformDataFrame
,
FieldType
,
Field
,
KeyValue
,
AnnotationEvent
,
AnnotationEventMappings
,
getFieldDisplayName
,
AnnotationEventFieldSource
,
}
from
'@grafana/data'
;
import
isString
from
'lodash/isString'
;
export
const
standardAnnotationSupport
:
AnnotationSupport
=
{
/**
* Assume the stored value is standard model.
*/
prepareAnnotation
:
(
json
:
any
)
=>
{
if
(
isString
(
json
?.
query
))
{
const
{
query
,
...
rest
}
=
json
;
return
{
...
rest
,
target
:
{
query
,
},
mappings
:
{},
};
}
return
json
as
AnnotationQuery
;
},
/**
* Convert the stored JSON model and environment to a standard datasource query object.
* This query will be executed in the datasource and the results converted into events.
* Returning an undefined result will quietly skip query execution
*/
prepareQuery
:
(
anno
:
AnnotationQuery
)
=>
anno
.
target
,
/**
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
*/
processEvents
:
(
anno
:
AnnotationQuery
,
data
:
DataFrame
)
=>
{
return
getAnnotationsFromFrame
(
data
,
anno
.
mappings
);
},
};
/**
* Flatten all panel data into a single frame
*/
export
function
singleFrameFromPanelData
(
rsp
:
PanelData
):
DataFrame
|
undefined
{
if
(
!
rsp
?.
series
?.
length
)
{
return
undefined
;
}
if
(
rsp
.
series
.
length
===
1
)
{
return
rsp
.
series
[
0
];
}
return
transformDataFrame
(
[
{
id
:
'seriesToColumns'
,
options
:
{
byField
:
'Time'
},
},
],
rsp
.
series
)[
0
];
}
interface
AnnotationEventFieldSetter
{
key
:
keyof
AnnotationEvent
;
field
?:
Field
;
text
?:
string
;
regex
?:
RegExp
;
split
?:
string
;
// for tags
}
export
interface
AnnotationFieldInfo
{
key
:
keyof
AnnotationEvent
;
split
?:
string
;
field
?:
(
frame
:
DataFrame
)
=>
Field
|
undefined
;
placeholder
?:
string
;
help
?:
string
;
}
export
const
annotationEventNames
:
AnnotationFieldInfo
[]
=
[
{
key
:
'time'
,
field
:
(
frame
:
DataFrame
)
=>
frame
.
fields
.
find
(
f
=>
f
.
type
===
FieldType
.
time
),
placeholder
:
'time, or the first time field'
,
},
{
key
:
'timeEnd'
,
help
:
'When this field is defined, the annotation will be treated as a range'
},
{
key
:
'title'
,
},
{
key
:
'text'
,
field
:
(
frame
:
DataFrame
)
=>
frame
.
fields
.
find
(
f
=>
f
.
type
===
FieldType
.
string
),
placeholder
:
'text, or the first text field'
,
},
{
key
:
'tags'
,
split
:
','
,
help
:
'The results will be split on comma (,)'
},
// { key: 'userId' },
// { key: 'login' },
// { key: 'email' },
];
export
function
getAnnotationsFromFrame
(
frame
:
DataFrame
,
options
?:
AnnotationEventMappings
):
AnnotationEvent
[]
{
if
(
!
frame
?.
length
)
{
return
[];
}
let
hasTime
=
false
;
let
hasText
=
false
;
const
byName
:
KeyValue
<
Field
>
=
{};
for
(
const
f
of
frame
.
fields
)
{
const
name
=
getFieldDisplayName
(
f
,
frame
);
byName
[
name
.
toLowerCase
()]
=
f
;
}
if
(
!
options
)
{
options
=
{};
}
const
fields
:
AnnotationEventFieldSetter
[]
=
[];
for
(
const
evts
of
annotationEventNames
)
{
const
opt
=
options
[
evts
.
key
]
||
{};
//AnnotationEventFieldMapping
if
(
opt
.
source
===
AnnotationEventFieldSource
.
Skip
)
{
continue
;
}
const
setter
:
AnnotationEventFieldSetter
=
{
key
:
evts
.
key
,
split
:
evts
.
split
};
if
(
opt
.
source
===
AnnotationEventFieldSource
.
Text
)
{
setter
.
text
=
opt
.
value
;
}
else
{
const
lower
=
(
opt
.
value
||
evts
.
key
).
toLowerCase
();
setter
.
field
=
byName
[
lower
];
if
(
!
setter
.
field
&&
evts
.
field
)
{
setter
.
field
=
evts
.
field
(
frame
);
}
}
if
(
setter
.
field
||
setter
.
text
)
{
fields
.
push
(
setter
);
if
(
setter
.
key
===
'time'
)
{
hasTime
=
true
;
}
else
if
(
setter
.
key
===
'text'
)
{
hasText
=
true
;
}
}
}
if
(
!
hasTime
||
!
hasText
)
{
return
[];
// throw an error?
}
// Add each value to the string
const
events
:
AnnotationEvent
[]
=
[];
for
(
let
i
=
0
;
i
<
frame
.
length
;
i
++
)
{
const
anno
:
AnnotationEvent
=
{};
for
(
const
f
of
fields
)
{
let
v
:
any
=
undefined
;
if
(
f
.
text
)
{
v
=
f
.
text
;
// TODO support templates!
}
else
if
(
f
.
field
)
{
v
=
f
.
field
.
values
.
get
(
i
);
if
(
v
!==
undefined
&&
f
.
regex
)
{
const
match
=
f
.
regex
.
exec
(
v
);
if
(
match
)
{
v
=
match
[
1
]
?
match
[
1
]
:
match
[
0
];
}
}
}
if
(
v
!==
undefined
)
{
if
(
f
.
split
)
{
v
=
(
v
as
string
).
split
(
','
);
}
(
anno
as
any
)[
f
.
key
]
=
v
;
}
}
events
.
push
(
anno
);
}
return
events
;
}
public/app/features/annotations/types.ts
0 → 100644
View file @
5d11d8fa
import
{
PanelData
,
DataFrame
,
AnnotationEvent
,
TimeRange
}
from
'@grafana/data'
;
import
{
DashboardModel
,
PanelModel
}
from
'../dashboard/state'
;
export
interface
AnnotationQueryOptions
{
dashboard
:
DashboardModel
;
panel
:
PanelModel
;
range
:
TimeRange
;
}
export
interface
AnnotationQueryResponse
{
/**
* All the data flattened to a single frame
*/
frame
?:
DataFrame
;
/**
* The processed annotation events
*/
events
?:
AnnotationEvent
[];
/**
* The original panel response
*/
panelData
?:
PanelData
;
}
public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx
View file @
5d11d8fa
import
React
,
{
PureComponent
}
from
'react'
;
import
coreModule
from
'app/core/core_module'
;
import
{
InfluxQuery
}
from
'../types'
;
import
{
SelectableValue
}
from
'@grafana/data'
;
import
{
SelectableValue
,
QueryEditorProps
}
from
'@grafana/data'
;
import
{
cx
,
css
}
from
'emotion'
;
import
{
InlineFormLabel
,
...
...
@@ -12,12 +12,10 @@ import {
CodeEditorSuggestionItemKind
,
}
from
'@grafana/ui'
;
import
{
getTemplateSrv
}
from
'@grafana/runtime'
;
import
InfluxDatasource
from
'../datasource'
;
interface
Props
{
target
:
InfluxQuery
;
change
:
(
target
:
InfluxQuery
)
=>
void
;
refresh
:
()
=>
void
;
}
// @ts-ignore -- complicated since the datasource is not really reactified yet!
type
Props
=
QueryEditorProps
<
InfluxDatasource
,
InfluxQuery
>
;
const
samples
:
Array
<
SelectableValue
<
string
>>
=
[
{
label
:
'Show buckets'
,
description
:
'List the avaliable buckets (table)'
,
value
:
'buckets()'
},
...
...
@@ -87,20 +85,19 @@ v1.tagValues(
export
class
FluxQueryEditor
extends
PureComponent
<
Props
>
{
onFluxQueryChange
=
(
query
:
string
)
=>
{
const
{
target
,
change
}
=
this
.
props
;
change
({
...
target
,
query
});
this
.
props
.
refresh
();
this
.
props
.
onChange
({
...
this
.
props
.
query
,
query
});
this
.
props
.
onRunQuery
();
};
onSampleChange
=
(
val
:
SelectableValue
<
string
>
)
=>
{
this
.
props
.
c
hange
({
...
this
.
props
.
target
,
this
.
props
.
onC
hange
({
...
this
.
props
.
query
,
query
:
val
.
value
!
,
});
// Angular HACK: Since the target does not actually change!
this
.
forceUpdate
();
this
.
props
.
refresh
();
this
.
props
.
onRunQuery
();
};
getSuggestions
=
():
CodeEditorSuggestionItem
[]
=>
{
...
...
@@ -157,7 +154,7 @@ export class FluxQueryEditor extends PureComponent<Props> {
};
render
()
{
const
{
target
}
=
this
.
props
;
const
{
query
}
=
this
.
props
;
const
helpTooltip
=
(
<
div
>
...
...
@@ -171,7 +168,7 @@ export class FluxQueryEditor extends PureComponent<Props> {
<
CodeEditor
height=
{
'200px'
}
language=
"sql"
value=
{
target
.
query
||
''
}
value=
{
query
.
query
||
''
}
onBlur=
{
this
.
onFluxQueryChange
}
onSave=
{
this
.
onFluxQueryChange
}
showMiniMap=
{
false
}
...
...
@@ -211,6 +208,6 @@ export class FluxQueryEditor extends PureComponent<Props> {
coreModule
.
directive
(
'fluxQueryEditor'
,
[
'reactDirective'
,
(
reactDirective
:
any
)
=>
{
return
reactDirective
(
FluxQueryEditor
,
[
'
target'
,
'change'
,
'refresh
'
]);
return
reactDirective
(
FluxQueryEditor
,
[
'
query'
,
'onChange'
,
'onRunQuery
'
]);
},
]);
public/app/plugins/datasource/influxdb/components/VariableQueryEditor.tsx
View file @
5d11d8fa
...
...
@@ -19,12 +19,13 @@ export default class VariableQueryEditor extends PureComponent<Props> {
if
(
datasource
.
isFlux
)
{
return
(
<
FluxQueryEditor
target=
{
{
datasource=
{
datasource
}
query=
{
{
refId
:
'A'
,
query
,
}
}
refresh
=
{
this
.
onRefresh
}
c
hange=
{
v
=>
onChange
(
v
.
query
)
}
onRunQuery
=
{
this
.
onRefresh
}
onC
hange=
{
v
=>
onChange
(
v
.
query
)
}
/>
);
}
...
...
public/app/plugins/datasource/influxdb/datasource.ts
View file @
5d11d8fa
...
...
@@ -21,6 +21,7 @@ import { InfluxQueryBuilder } from './query_builder';
import
{
InfluxQuery
,
InfluxOptions
,
InfluxVersion
}
from
'./types'
;
import
{
getBackendSrv
,
getTemplateSrv
,
DataSourceWithBackend
,
frameToMetricFindValue
}
from
'@grafana/runtime'
;
import
{
Observable
,
from
}
from
'rxjs'
;
import
{
FluxQueryEditor
}
from
'./components/FluxQueryEditor'
;
export
default
class
InfluxDatasource
extends
DataSourceWithBackend
<
InfluxQuery
,
InfluxOptions
>
{
type
:
string
;
...
...
@@ -55,6 +56,13 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
this
.
httpMode
=
settingsData
.
httpMode
||
'GET'
;
this
.
responseParser
=
new
ResponseParser
();
this
.
isFlux
=
settingsData
.
version
===
InfluxVersion
.
Flux
;
if
(
this
.
isFlux
)
{
// When flux, use an annotation processor rather than the `annotationQuery` lifecycle
this
.
annotations
=
{
QueryEditor
:
FluxQueryEditor
,
};
}
}
query
(
request
:
DataQueryRequest
<
InfluxQuery
>
):
Observable
<
DataQueryResponse
>
{
...
...
@@ -74,6 +82,16 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
}
/**
* Returns false if the query should be skipped
*/
filterQuery
(
query
:
InfluxQuery
):
boolean
{
if
(
this
.
isFlux
)
{
return
!!
query
.
query
;
}
return
true
;
}
/**
* Only applied on flux queries
*/
applyTemplateVariables
(
query
:
InfluxQuery
,
scopedVars
:
ScopedVars
):
Record
<
string
,
any
>
{
...
...
@@ -183,7 +201,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
async
annotationQuery
(
options
:
AnnotationQueryRequest
<
any
>
):
Promise
<
AnnotationEvent
[]
>
{
if
(
this
.
isFlux
)
{
return
Promise
.
reject
({
message
:
'
Annotations are not yet supported with flux queries
'
,
message
:
'
Flux requires the standard annotation query
'
,
});
}
...
...
public/app/plugins/datasource/influxdb/partials/query.editor.html
View file @
5d11d8fa
<query-editor-row
ng-if=
"ctrl.datasource.isFlux"
query-ctrl=
"ctrl"
can-collapse=
"true"
has-text-edit-mode=
"true"
>
<flux-query-editor
target
=
"ctrl.target"
c
hange=
"ctrl.onChange"
refresh
=
"ctrl.onRunQuery"
query
=
"ctrl.target"
onC
hange=
"ctrl.onChange"
onRunQuery
=
"ctrl.onRunQuery"
></flux-query-editor>
</query-editor-row>
...
...
public/app/plugins/datasource/loki/datasource.test.ts
View file @
5d11d8fa
...
...
@@ -539,9 +539,6 @@ function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> {
raw
:
timeRange
,
},
rangeRaw
:
timeRange
,
app
:
'test'
,
interval
:
'1m'
,
intervalMs
:
6000
,
};
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment