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
7d32caea
Commit
7d32caea
authored
Sep 08, 2019
by
Ryan McKinley
Committed by
Dominik Prokop
Sep 09, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Transformers: configure result transformations after query(alpha) (#18740)
parent
205c0a58
Show whitespace changes
Inline
Side-by-side
Showing
35 changed files
with
786 additions
and
71 deletions
+786
-71
conf/defaults.ini
+4
-0
packages/grafana-data/src/utils/transformers/filter.ts
+4
-3
packages/grafana-data/src/utils/transformers/filterByName.test.ts
+66
-0
packages/grafana-data/src/utils/transformers/filterByName.ts
+38
-0
packages/grafana-data/src/utils/transformers/ids.ts
+2
-0
packages/grafana-data/src/utils/transformers/noop.ts
+23
-0
packages/grafana-data/src/utils/transformers/reduce.ts
+6
-6
packages/grafana-data/src/utils/transformers/transformers.ts
+8
-5
packages/grafana-runtime/src/config.ts
+6
-0
packages/grafana-ui/src/components/AlphaNotice/AlphaNotice.tsx
+44
-0
packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx
+5
-2
packages/grafana-ui/src/components/JSONFormatter/json_explorer/helpers.ts
+0
-0
packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts
+5
-5
packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx
+1
-1
packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx
+163
-0
packages/grafana-ui/src/components/TransformersUI/ReduceTransformerEditor.tsx
+35
-0
packages/grafana-ui/src/components/TransformersUI/TransformationRow.tsx
+85
-0
packages/grafana-ui/src/components/TransformersUI/TransformationsEditor.tsx
+127
-0
packages/grafana-ui/src/components/TransformersUI/transformers.ts
+8
-0
packages/grafana-ui/src/components/TransformersUI/types.ts
+15
-0
packages/grafana-ui/src/components/index.ts
+6
-1
pkg/api/frontendsettings.go
+1
-0
pkg/setting/setting.go
+13
-0
public/app/core/components/jsontree/jsontree.ts
+1
-1
public/app/core/core.ts
+1
-2
public/app/features/alerting/TestRuleResult.tsx
+1
-2
public/app/features/dashboard/dashgrid/PanelChrome.tsx
+1
-1
public/app/features/dashboard/panel_editor/QueriesTab.tsx
+42
-4
public/app/features/dashboard/panel_editor/QueryInspector.tsx
+1
-2
public/app/features/dashboard/state/PanelModel.ts
+11
-2
public/app/features/dashboard/state/PanelQueryRunner.ts
+50
-7
public/app/features/panel/metrics_panel_ctrl.ts
+1
-0
public/app/features/plugins/PluginStateInfo.tsx
+10
-26
public/app/plugins/datasource/dashboard/SharedQueryRunner.ts
+1
-0
public/app/plugins/panel/graph/module.ts
+1
-1
No files found.
conf/defaults.ini
View file @
7d32caea
...
...
@@ -682,3 +682,7 @@ app_tls_skip_verify_insecure = false
[enterprise]
license_path
=
[feature_toggles]
# enable features, separated by spaces
enable
=
packages/grafana-data/src/utils/transformers/filter.ts
View file @
7d32caea
import
{
DataTransformerInfo
,
NoopDataTransformer
}
from
'./transformers'
;
import
{
DataTransformerInfo
}
from
'./transformers'
;
import
{
noopTransformer
}
from
'./noop'
;
import
{
DataFrame
,
Field
}
from
'../../types/dataFrame'
;
import
{
FieldMatcherID
}
from
'../matchers/ids'
;
import
{
DataTransformerID
}
from
'./ids'
;
...
...
@@ -23,7 +24,7 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
*/
transformer
:
(
options
:
FilterOptions
)
=>
{
if
(
!
options
.
include
&&
!
options
.
exclude
)
{
return
NoopDataTransformer
;
return
noopTransformer
.
transformer
({})
;
}
const
include
=
options
.
include
?
getFieldMatcher
(
options
.
include
)
:
null
;
...
...
@@ -75,7 +76,7 @@ export const filterFramesTransformer: DataTransformerInfo<FilterOptions> = {
*/
transformer
:
(
options
:
FilterOptions
)
=>
{
if
(
!
options
.
include
&&
!
options
.
exclude
)
{
return
NoopDataTransformer
;
return
noopTransformer
.
transformer
({})
;
}
const
include
=
options
.
include
?
getFrameMatchers
(
options
.
include
)
:
null
;
...
...
packages/grafana-data/src/utils/transformers/filterByName.test.ts
0 → 100644
View file @
7d32caea
import
{
toDataFrame
,
transformDataFrame
}
from
'../index'
;
import
{
FieldType
}
from
'../../index'
;
import
{
DataTransformerID
}
from
'./ids'
;
export
const
seriesWithNamesToMatch
=
toDataFrame
({
fields
:
[
{
name
:
'startsWithA'
,
type
:
FieldType
.
time
,
values
:
[
1000
,
2000
]
},
{
name
:
'B'
,
type
:
FieldType
.
boolean
,
values
:
[
true
,
false
]
},
{
name
:
'startsWithC'
,
type
:
FieldType
.
string
,
values
:
[
'a'
,
'b'
]
},
{
name
:
'D'
,
type
:
FieldType
.
number
,
values
:
[
1
,
2
]
},
],
});
describe
(
'filterByName transformer'
,
()
=>
{
it
(
'returns original series if no options provided'
,
()
=>
{
const
cfg
=
{
id
:
DataTransformerID
.
filterFields
,
options
:
{},
};
const
filtered
=
transformDataFrame
([
cfg
],
[
seriesWithNamesToMatch
])[
0
];
expect
(
filtered
.
fields
.
length
).
toBe
(
4
);
});
describe
(
'respects'
,
()
=>
{
it
(
'inclusion'
,
()
=>
{
const
cfg
=
{
id
:
DataTransformerID
.
filterFieldsByName
,
options
:
{
include
:
'/^(startsWith)/'
,
},
};
const
filtered
=
transformDataFrame
([
cfg
],
[
seriesWithNamesToMatch
])[
0
];
expect
(
filtered
.
fields
.
length
).
toBe
(
2
);
expect
(
filtered
.
fields
[
0
].
name
).
toBe
(
'startsWithA'
);
});
it
(
'exclusion'
,
()
=>
{
const
cfg
=
{
id
:
DataTransformerID
.
filterFieldsByName
,
options
:
{
exclude
:
'/^(startsWith)/'
,
},
};
const
filtered
=
transformDataFrame
([
cfg
],
[
seriesWithNamesToMatch
])[
0
];
expect
(
filtered
.
fields
.
length
).
toBe
(
2
);
expect
(
filtered
.
fields
[
0
].
name
).
toBe
(
'B'
);
});
it
(
'inclusion and exclusion'
,
()
=>
{
const
cfg
=
{
id
:
DataTransformerID
.
filterFieldsByName
,
options
:
{
exclude
:
'/^(startsWith)/'
,
include
:
`/^(B)$/`
,
},
};
const
filtered
=
transformDataFrame
([
cfg
],
[
seriesWithNamesToMatch
])[
0
];
expect
(
filtered
.
fields
.
length
).
toBe
(
1
);
expect
(
filtered
.
fields
[
0
].
name
).
toBe
(
'B'
);
});
});
});
packages/grafana-data/src/utils/transformers/filterByName.ts
0 → 100644
View file @
7d32caea
import
{
DataTransformerInfo
}
from
'./transformers'
;
import
{
FieldMatcherID
}
from
'../matchers/ids'
;
import
{
DataTransformerID
}
from
'./ids'
;
import
{
filterFieldsTransformer
,
FilterOptions
}
from
'./filter'
;
export
interface
FilterFieldsByNameTransformerOptions
{
include
?:
string
;
exclude
?:
string
;
}
export
const
filterFieldsByNameTransformer
:
DataTransformerInfo
<
FilterFieldsByNameTransformerOptions
>
=
{
id
:
DataTransformerID
.
filterFieldsByName
,
name
:
'Filter fields by name'
,
description
:
'select a subset of fields'
,
defaultOptions
:
{},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer
:
(
options
:
FilterFieldsByNameTransformerOptions
)
=>
{
const
filterOptions
:
FilterOptions
=
{};
if
(
options
.
include
)
{
filterOptions
.
include
=
{
id
:
FieldMatcherID
.
byName
,
options
:
options
.
include
,
};
}
if
(
options
.
exclude
)
{
filterOptions
.
exclude
=
{
id
:
FieldMatcherID
.
byName
,
options
:
options
.
exclude
,
};
}
return
filterFieldsTransformer
.
transformer
(
filterOptions
);
},
};
packages/grafana-data/src/utils/transformers/ids.ts
View file @
7d32caea
...
...
@@ -5,5 +5,7 @@ export enum DataTransformerID {
reduce
=
'reduce'
,
// Run calculations on fields
filterFields
=
'filterFields'
,
// Pick some fields (keep all frames)
filterFieldsByName
=
'filterFieldsByName'
,
// Pick fields with name matching regex (keep all frames)
filterFrames
=
'filterFrames'
,
// Pick some frames (keep all fields)
noop
=
'noop'
,
// Does nothing to the dataframe
}
packages/grafana-data/src/utils/transformers/noop.ts
0 → 100644
View file @
7d32caea
import
{
DataTransformerInfo
}
from
'./transformers'
;
import
{
DataTransformerID
}
from
'./ids'
;
import
{
DataFrame
}
from
'../../types/dataFrame'
;
export
interface
NoopTransformerOptions
{
include
?:
string
;
exclude
?:
string
;
}
export
const
noopTransformer
:
DataTransformerInfo
<
NoopTransformerOptions
>
=
{
id
:
DataTransformerID
.
noop
,
name
:
'noop'
,
description
:
'No-operation transformer'
,
defaultOptions
:
{},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer
:
(
options
:
NoopTransformerOptions
)
=>
{
return
(
data
:
DataFrame
[])
=>
data
;
},
};
packages/grafana-data/src/utils/transformers/reduce.ts
View file @
7d32caea
...
...
@@ -8,26 +8,26 @@ import { KeyValue } from '../../types/data';
import
{
ArrayVector
}
from
'../vector'
;
import
{
guessFieldTypeForField
}
from
'../processDataFrame'
;
export
interface
ReduceOptions
{
reducers
:
string
[];
export
interface
Reduce
Transformer
Options
{
reducers
:
ReducerID
[];
fields
?:
MatcherConfig
;
// Assume all fields
}
export
const
reduceTransformer
:
DataTransformerInfo
<
ReduceOptions
>
=
{
export
const
reduceTransformer
:
DataTransformerInfo
<
Reduce
Transformer
Options
>
=
{
id
:
DataTransformerID
.
reduce
,
name
:
'Reducer'
,
description
:
'Return a DataFrame with the reduction results'
,
defaultOptions
:
{
calc
s
:
[
ReducerID
.
min
,
ReducerID
.
max
,
ReducerID
.
mean
,
ReducerID
.
last
],
reducer
s
:
[
ReducerID
.
min
,
ReducerID
.
max
,
ReducerID
.
mean
,
ReducerID
.
last
],
},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer
:
(
options
:
ReduceOptions
)
=>
{
transformer
:
(
options
:
Reduce
Transformer
Options
)
=>
{
const
matcher
=
options
.
fields
?
getFieldMatcher
(
options
.
fields
)
:
alwaysFieldMatcher
;
const
calculators
=
fieldReducers
.
list
(
options
.
reducers
)
;
const
calculators
=
options
.
reducers
&&
options
.
reducers
.
length
?
fieldReducers
.
list
(
options
.
reducers
)
:
[]
;
const
reducers
=
calculators
.
map
(
c
=>
c
.
id
);
return
(
data
:
DataFrame
[])
=>
{
...
...
packages/grafana-data/src/utils/transformers/transformers.ts
View file @
7d32caea
...
...
@@ -15,9 +15,6 @@ export interface DataTransformerConfig<TOptions = any> {
options
:
TOptions
;
}
// Transformer that does nothing
export
const
NoopDataTransformer
=
(
data
:
DataFrame
[])
=>
data
;
/**
* Apply configured transformations to the input data
*/
...
...
@@ -49,8 +46,10 @@ export function transformDataFrame(options: DataTransformerConfig[], data: DataF
// Initalize the Registry
import
{
appendTransformer
,
AppendOptions
}
from
'./append'
;
import
{
reduceTransformer
,
ReduceOptions
}
from
'./reduce'
;
import
{
reduceTransformer
,
Reduce
Transformer
Options
}
from
'./reduce'
;
import
{
filterFieldsTransformer
,
filterFramesTransformer
}
from
'./filter'
;
import
{
filterFieldsByNameTransformer
,
FilterFieldsByNameTransformerOptions
}
from
'./filterByName'
;
import
{
noopTransformer
}
from
'./noop'
;
/**
* Registry of transformation options that can be driven by
...
...
@@ -69,14 +68,18 @@ class TransformerRegistry extends Registry<DataTransformerInfo> {
return
appendTransformer
.
transformer
(
options
||
appendTransformer
.
defaultOptions
)(
data
)[
0
];
}
reduce
(
data
:
DataFrame
[],
options
:
ReduceOptions
):
DataFrame
[]
{
reduce
(
data
:
DataFrame
[],
options
:
Reduce
Transformer
Options
):
DataFrame
[]
{
return
reduceTransformer
.
transformer
(
options
)(
data
);
}
}
export
const
dataTransformers
=
new
TransformerRegistry
(()
=>
[
noopTransformer
,
filterFieldsTransformer
,
filterFieldsByNameTransformer
,
filterFramesTransformer
,
appendTransformer
,
reduceTransformer
,
]);
export
{
ReduceTransformerOptions
,
FilterFieldsByNameTransformerOptions
};
packages/grafana-runtime/src/config.ts
View file @
7d32caea
...
...
@@ -10,6 +10,9 @@ export interface BuildInfo {
hasUpdate
:
boolean
;
}
interface
FeatureToggles
{
transformations
:
boolean
;
}
export
class
GrafanaBootConfig
{
datasources
:
{
[
str
:
string
]:
DataSourceInstanceSettings
}
=
{};
panels
:
{
[
key
:
string
]:
PanelPluginMeta
}
=
{};
...
...
@@ -41,6 +44,9 @@ export class GrafanaBootConfig {
disableSanitizeHtml
=
false
;
theme
:
GrafanaTheme
;
pluginsToPreload
:
string
[]
=
[];
featureToggles
:
FeatureToggles
=
{
transformations
:
false
,
};
constructor
(
options
:
GrafanaBootConfig
)
{
this
.
theme
=
options
.
bootData
.
user
.
lightTheme
?
getTheme
(
GrafanaThemeType
.
Light
)
:
getTheme
(
GrafanaThemeType
.
Dark
);
...
...
packages/grafana-ui/src/components/AlphaNotice/AlphaNotice.tsx
0 → 100644
View file @
7d32caea
import
React
,
{
FC
,
useContext
}
from
'react'
;
import
{
css
,
cx
}
from
'emotion'
;
import
{
PluginState
,
ThemeContext
}
from
'../../index'
;
import
{
Tooltip
}
from
'../index'
;
interface
Props
{
state
?:
PluginState
;
text
?:
JSX
.
Element
;
className
?:
string
;
}
export
const
AlphaNotice
:
FC
<
Props
>
=
({
state
,
text
,
className
})
=>
{
const
tooltipContent
=
text
||
(
<
div
>
<
h5
>
Alpha Feature
</
h5
>
<
p
>
This feature is a work in progress and updates may include breaking changes.
</
p
>
</
div
>
);
const
theme
=
useContext
(
ThemeContext
);
const
styles
=
cx
(
className
,
css
`
background: linear-gradient(to bottom,
${
theme
.
colors
.
blueBase
}
,
${
theme
.
colors
.
blueShade
}
);
color:
${
theme
.
colors
.
gray7
}
;
white-space: nowrap;
border-radius: 3px;
text-shadow: none;
font-size: 13px;
padding: 4px 8px;
cursor: help;
display: inline-block;
`
);
return
(
<
Tooltip
content=
{
tooltipContent
}
theme=
{
'info'
}
placement=
{
'top'
}
>
<
div
className=
{
styles
}
>
<
i
className=
"fa fa-warning"
/>
{
state
}
</
div
>
</
Tooltip
>
);
};
p
ublic/app/core
/components/JSONFormatter/JSONFormatter.tsx
→
p
ackages/grafana-ui/src
/components/JSONFormatter/JSONFormatter.tsx
View file @
7d32caea
import
React
,
{
PureComponent
,
createRef
}
from
'react'
;
import
{
JsonExplorer
}
from
'
app/core/core
'
;
// We have made some monkey-patching of json-formatter-js so we can't switch right now
import
React
,
{
PureComponent
,
createRef
}
from
'react'
;
import
{
JsonExplorer
}
from
'
./json_explorer/json_explorer
'
;
// We have made some monkey-patching of json-formatter-js so we can't switch right now
interface
Props
{
className
?:
string
;
...
...
@@ -31,10 +31,13 @@ export class JSONFormatter extends PureComponent<Props> {
const
{
json
,
config
,
open
,
onDidRender
}
=
this
.
props
;
const
wrapperEl
=
this
.
wrapperRef
.
current
;
const
formatter
=
new
JsonExplorer
(
json
,
open
,
config
);
// @ts-ignore
const
hasChildren
:
boolean
=
wrapperEl
.
hasChildNodes
();
if
(
hasChildren
)
{
// @ts-ignore
wrapperEl
.
replaceChild
(
formatter
.
render
(),
wrapperEl
.
lastChild
);
}
else
{
// @ts-ignore
wrapperEl
.
appendChild
(
formatter
.
render
());
}
...
...
p
ublic/app/core/components
/json_explorer/helpers.ts
→
p
ackages/grafana-ui/src/components/JSONFormatter
/json_explorer/helpers.ts
View file @
7d32caea
File moved
p
ublic/app/core/components
/json_explorer/json_explorer.ts
→
p
ackages/grafana-ui/src/components/JSONFormatter
/json_explorer/json_explorer.ts
View file @
7d32caea
...
...
@@ -28,7 +28,6 @@ export interface JsonExplorerConfig {
const
_defaultConfig
:
JsonExplorerConfig
=
{
animateOpen
:
true
,
animateClose
:
true
,
theme
:
null
,
};
/**
...
...
@@ -39,10 +38,10 @@ const _defaultConfig: JsonExplorerConfig = {
*/
export
class
JsonExplorer
{
// Hold the open state after the toggler is used
private
_isOpen
:
boolean
=
null
;
private
_isOpen
:
boolean
|
null
=
null
;
// A reference to the element that we render to
private
element
:
Element
;
private
element
:
Element
|
null
=
null
;
private
skipChildren
=
false
;
...
...
@@ -366,7 +365,7 @@ export class JsonExplorer {
* Animated option is used when user triggers this via a click
*/
appendChildren
(
animated
=
false
)
{
const
children
=
this
.
element
.
querySelector
(
`div.
${
cssClass
(
'children'
)}
`
);
const
children
=
this
.
element
&&
this
.
element
.
querySelector
(
`div.
${
cssClass
(
'children'
)}
`
);
if
(
!
children
||
this
.
isEmpty
)
{
return
;
...
...
@@ -404,7 +403,8 @@ export class JsonExplorer {
* Animated option is used when user triggers this via a click
*/
removeChildren
(
animated
=
false
)
{
const
childrenElement
=
this
.
element
.
querySelector
(
`div.
${
cssClass
(
'children'
)}
`
)
as
HTMLDivElement
;
const
childrenElement
=
this
.
element
&&
(
this
.
element
.
querySelector
(
`div.
${
cssClass
(
'children'
)}
`
)
as
HTMLDivElement
);
if
(
animated
)
{
let
childrenRemoved
=
0
;
...
...
packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx
View file @
7d32caea
...
...
@@ -2,7 +2,7 @@
import
React
,
{
FunctionComponent
}
from
'react'
;
interface
Props
{
title
?:
string
;
title
?:
string
|
JSX
.
Element
;
onClose
?:
()
=>
void
;
children
:
JSX
.
Element
|
JSX
.
Element
[]
|
boolean
;
onAdd
?:
()
=>
void
;
...
...
packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx
0 → 100644
View file @
7d32caea
import
React
,
{
useContext
}
from
'react'
;
import
{
FilterFieldsByNameTransformerOptions
,
DataTransformerID
,
dataTransformers
,
KeyValue
}
from
'@grafana/data'
;
import
{
TransformerUIProps
,
TransformerUIRegistyItem
}
from
'./types'
;
import
{
ThemeContext
}
from
'../../themes/ThemeContext'
;
import
{
css
,
cx
}
from
'emotion'
;
import
{
InlineList
}
from
'../List/InlineList'
;
interface
FilterByNameTransformerEditorProps
extends
TransformerUIProps
<
FilterFieldsByNameTransformerOptions
>
{}
interface
FilterByNameTransformerEditorState
{
include
:
string
;
options
:
FieldNameInfo
[];
selected
:
string
[];
}
interface
FieldNameInfo
{
name
:
string
;
count
:
number
;
}
export
class
FilterByNameTransformerEditor
extends
React
.
PureComponent
<
FilterByNameTransformerEditorProps
,
FilterByNameTransformerEditorState
>
{
constructor
(
props
:
FilterByNameTransformerEditorProps
)
{
super
(
props
);
this
.
state
=
{
include
:
props
.
options
.
include
||
''
,
options
:
[],
selected
:
[],
};
}
componentDidMount
()
{
this
.
initOptions
();
}
private
initOptions
()
{
const
{
input
,
options
}
=
this
.
props
;
const
configuredOptions
=
options
.
include
?
options
.
include
.
split
(
'|'
)
:
[];
const
allNames
:
FieldNameInfo
[]
=
[];
const
byName
:
KeyValue
<
FieldNameInfo
>
=
{};
for
(
const
frame
of
input
)
{
for
(
const
field
of
frame
.
fields
)
{
let
v
=
byName
[
field
.
name
];
if
(
!
v
)
{
v
=
byName
[
field
.
name
]
=
{
name
:
field
.
name
,
count
:
0
,
};
allNames
.
push
(
v
);
}
v
.
count
++
;
}
}
if
(
configuredOptions
.
length
)
{
const
options
:
FieldNameInfo
[]
=
[];
const
selected
:
FieldNameInfo
[]
=
[];
for
(
const
v
of
allNames
)
{
if
(
configuredOptions
.
includes
(
v
.
name
))
{
selected
.
push
(
v
);
}
options
.
push
(
v
);
}
this
.
setState
({
options
,
selected
:
selected
.
map
(
s
=>
s
.
name
),
});
}
else
{
this
.
setState
({
options
:
allNames
,
selected
:
[]
});
}
}
onFieldToggle
=
(
fieldName
:
string
)
=>
{
const
{
selected
}
=
this
.
state
;
if
(
selected
.
indexOf
(
fieldName
)
>
-
1
)
{
this
.
onChange
(
selected
.
filter
(
s
=>
s
!==
fieldName
));
}
else
{
this
.
onChange
([...
selected
,
fieldName
]);
}
};
onChange
=
(
selected
:
string
[])
=>
{
this
.
setState
({
selected
});
this
.
props
.
onChange
({
...
this
.
props
.
options
,
include
:
selected
.
join
(
'|'
),
});
};
render
()
{
const
{
options
,
selected
}
=
this
.
state
;
return
(
<>
<
InlineList
items=
{
options
}
renderItem=
{
(
o
,
i
)
=>
{
const
label
=
`${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`
;
return
(
<
span
className=
{
css
`
margin-right: ${i === options.length - 1 ? '0' : '10px'};
`
}
>
<
FilterPill
onClick=
{
()
=>
{
this
.
onFieldToggle
(
o
.
name
);
}
}
label=
{
label
}
selected=
{
selected
.
indexOf
(
o
.
name
)
>
-
1
}
/>
</
span
>
);
}
}
/>
</>
);
}
}
interface
FilterPillProps
{
selected
:
boolean
;
label
:
string
;
onClick
:
React
.
MouseEventHandler
<
HTMLElement
>
;
}
const
FilterPill
:
React
.
FC
<
FilterPillProps
>
=
({
label
,
selected
,
onClick
})
=>
{
const
theme
=
useContext
(
ThemeContext
);
return
(
<
div
className=
{
css
`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
color: white;
background: ${selected ? theme.colors.blueLight : theme.colors.blueShade};
border-radius: 16px;
display: inline-block;
cursor: pointer;
`
}
onClick=
{
onClick
}
>
{
selected
&&
(
<
i
className=
{
cx
(
'fa fa-check'
,
css
`
margin-right: 4px;
`
)
}
/>
)
}
{
label
}
</
div
>
);
};
export
const
filterFieldsByNameTransformRegistryItem
:
TransformerUIRegistyItem
<
FilterFieldsByNameTransformerOptions
>
=
{
id
:
DataTransformerID
.
filterFieldsByName
,
component
:
FilterByNameTransformerEditor
,
transformer
:
dataTransformers
.
get
(
DataTransformerID
.
filterFieldsByName
),
name
:
'Filter by name'
,
description
:
'UI for filter by name transformation'
,
};
packages/grafana-ui/src/components/TransformersUI/ReduceTransformerEditor.tsx
0 → 100644
View file @
7d32caea
import
React
from
'react'
;
import
{
StatsPicker
}
from
'../StatsPicker/StatsPicker'
;
import
{
ReduceTransformerOptions
,
DataTransformerID
,
ReducerID
}
from
'@grafana/data'
;
import
{
TransformerUIRegistyItem
,
TransformerUIProps
}
from
'./types'
;
import
{
dataTransformers
}
from
'@grafana/data'
;
// TODO: Minimal implementation, needs some <3
export
const
ReduceTransformerEditor
:
React
.
FC
<
TransformerUIProps
<
ReduceTransformerOptions
>>
=
({
options
,
onChange
,
input
,
})
=>
{
return
(
<
StatsPicker
width=
{
12
}
placeholder=
"Choose Stat"
allowMultiple
stats=
{
options
.
reducers
||
[]
}
onChange=
{
stats
=>
{
onChange
({
...
options
,
reducers
:
stats
as
ReducerID
[],
});
}
}
/>
);
};
export
const
reduceTransformRegistryItem
:
TransformerUIRegistyItem
<
ReduceTransformerOptions
>
=
{
id
:
DataTransformerID
.
reduce
,
component
:
ReduceTransformerEditor
,
transformer
:
dataTransformers
.
get
(
DataTransformerID
.
reduce
),
name
:
'Reduce'
,
description
:
'UI for reduce transformation'
,
};
packages/grafana-ui/src/components/TransformersUI/TransformationRow.tsx
0 → 100644
View file @
7d32caea
import
React
,
{
useContext
,
useState
}
from
'react'
;
import
{
ThemeContext
}
from
'../../themes/ThemeContext'
;
import
{
css
}
from
'emotion'
;
import
{
DataFrame
}
from
'@grafana/data'
;
import
{
JSONFormatter
}
from
'../JSONFormatter/JSONFormatter'
;
import
{
GrafanaTheme
}
from
'../../types/theme'
;
interface
TransformationRowProps
{
name
:
string
;
description
:
string
;
editor
?:
JSX
.
Element
;
onRemove
:
()
=>
void
;
input
:
DataFrame
[];
}
const
getStyles
=
(
theme
:
GrafanaTheme
)
=>
({
title
:
css
`
display: flex;
padding: 4px 8px 4px 8px;
position: relative;
height: 35px;
background:
${
theme
.
colors
.
textFaint
}
;
border-radius: 4px 4px 0 0;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
`
,
name
:
css
`
font-weight:
${
theme
.
typography
.
weight
.
semibold
}
;
color:
${
theme
.
colors
.
blue
}
;
`
,
iconRow
:
css
`
display: flex;
`
,
icon
:
css
`
background: transparent;
border: none;
box-shadow: none;
cursor: pointer;
color:
${
theme
.
colors
.
textWeak
}
;
margin-left:
${
theme
.
spacing
.
sm
}
;
&:hover {
color:
${
theme
.
colors
.
text
}
;
}
`
,
editor
:
css
`
border: 2px dashed
${
theme
.
colors
.
textFaint
}
;
border-top: none;
border-radius: 0 0 4px 4px;
padding: 8px;
`
,
});
export
const
TransformationRow
=
({
onRemove
,
editor
,
name
,
input
}:
TransformationRowProps
)
=>
{
const
theme
=
useContext
(
ThemeContext
);
const
[
viewDebug
,
setViewDebug
]
=
useState
(
false
);
const
styles
=
getStyles
(
theme
);
return
(
<
div
className=
{
css
`
margin-bottom: 10px;
`
}
>
<
div
className=
{
styles
.
title
}
>
<
div
className=
{
styles
.
name
}
>
{
name
}
</
div
>
<
div
className=
{
styles
.
iconRow
}
>
<
div
onClick=
{
()
=>
setViewDebug
(
!
viewDebug
)
}
className=
{
styles
.
icon
}
>
<
i
className=
"fa fa-fw fa-bug"
/>
</
div
>
<
div
onClick=
{
onRemove
}
className=
{
styles
.
icon
}
>
<
i
className=
"fa fa-fw fa-trash"
/>
</
div
>
</
div
>
</
div
>
<
div
className=
{
styles
.
editor
}
>
{
editor
}
{
viewDebug
&&
(
<
div
>
<
JSONFormatter
json=
{
input
}
/>
</
div
>
)
}
</
div
>
</
div
>
);
};
packages/grafana-ui/src/components/TransformersUI/TransformationsEditor.tsx
0 → 100644
View file @
7d32caea
import
{
DataTransformerID
,
DataTransformerConfig
,
DataFrame
,
transformDataFrame
}
from
'@grafana/data'
;
import
{
Select
}
from
'../Select/Select'
;
import
{
transformersUIRegistry
}
from
'./transformers'
;
import
React
from
'react'
;
import
{
TransformationRow
}
from
'./TransformationRow'
;
import
{
Button
}
from
'../Button/Button'
;
import
{
css
}
from
'emotion'
;
interface
TransformationsEditorState
{
updateCounter
:
number
;
}
interface
TransformationsEditorProps
{
onChange
:
(
transformations
:
DataTransformerConfig
[])
=>
void
;
transformations
:
DataTransformerConfig
[];
getCurrentData
:
(
applyTransformations
?:
boolean
)
=>
DataFrame
[];
}
export
class
TransformationsEditor
extends
React
.
PureComponent
<
TransformationsEditorProps
,
TransformationsEditorState
>
{
state
=
{
updateCounter
:
0
};
onTransformationAdd
=
()
=>
{
const
{
transformations
,
onChange
}
=
this
.
props
;
onChange
([
...
transformations
,
{
id
:
DataTransformerID
.
noop
,
options
:
{},
},
]);
this
.
setState
({
updateCounter
:
this
.
state
.
updateCounter
+
1
});
};
onTransformationChange
=
(
idx
:
number
,
config
:
DataTransformerConfig
)
=>
{
const
{
transformations
,
onChange
}
=
this
.
props
;
transformations
[
idx
]
=
config
;
onChange
(
transformations
);
this
.
setState
({
updateCounter
:
this
.
state
.
updateCounter
+
1
});
};
onTransformationRemove
=
(
idx
:
number
)
=>
{
const
{
transformations
,
onChange
}
=
this
.
props
;
transformations
.
splice
(
idx
,
1
);
onChange
(
transformations
);
this
.
setState
({
updateCounter
:
this
.
state
.
updateCounter
+
1
});
};
renderTransformationEditors
=
()
=>
{
const
{
transformations
,
getCurrentData
}
=
this
.
props
;
const
hasTransformations
=
transformations
.
length
>
0
;
const
preTransformData
=
getCurrentData
(
false
);
if
(
!
hasTransformations
)
{
return
undefined
;
}
const
availableTransformers
=
transformersUIRegistry
.
list
().
map
(
t
=>
{
return
{
value
:
t
.
transformer
.
id
,
label
:
t
.
transformer
.
name
,
};
});
return
(
<>
{
transformations
.
map
((
t
,
i
)
=>
{
let
editor
,
input
;
if
(
t
.
id
===
DataTransformerID
.
noop
)
{
return
(
<
Select
className=
{
css
`
margin-bottom: 10px;
`
}
key=
{
`${t.id}-${i}`
}
options=
{
availableTransformers
}
placeholder=
"Select transformation"
onChange=
{
v
=>
{
this
.
onTransformationChange
(
i
,
{
id
:
v
.
value
as
string
,
options
:
{},
});
}
}
/>
);
}
const
transformationUI
=
transformersUIRegistry
.
getIfExists
(
t
.
id
);
input
=
transformDataFrame
(
transformations
.
slice
(
0
,
i
),
preTransformData
);
if
(
transformationUI
)
{
editor
=
React
.
createElement
(
transformationUI
.
component
,
{
options
:
{
...
transformationUI
.
transformer
.
defaultOptions
,
...
t
.
options
},
input
,
onChange
:
(
options
:
any
)
=>
{
this
.
onTransformationChange
(
i
,
{
id
:
t
.
id
,
options
,
});
},
});
}
return
(
<
TransformationRow
key=
{
`${t.id}-${i}`
}
input=
{
input
||
[]
}
onRemove=
{
()
=>
this
.
onTransformationRemove
(
i
)
}
editor=
{
editor
}
name=
{
transformationUI
?
transformationUI
.
name
:
''
}
description=
{
transformationUI
?
transformationUI
.
description
:
''
}
/>
);
})
}
</>
);
};
render
()
{
return
(
<>
{
this
.
renderTransformationEditors
()
}
<
Button
variant=
"inverse"
icon=
"fa fa-plus"
onClick=
{
this
.
onTransformationAdd
}
>
Add transformation
</
Button
>
</>
);
}
}
packages/grafana-ui/src/components/TransformersUI/transformers.ts
0 → 100644
View file @
7d32caea
import
{
Registry
}
from
'@grafana/data'
;
import
{
reduceTransformRegistryItem
}
from
'./ReduceTransformerEditor'
;
import
{
filterFieldsByNameTransformRegistryItem
}
from
'./FilterByNameTransformerEditor'
;
import
{
TransformerUIRegistyItem
}
from
'./types'
;
export
const
transformersUIRegistry
=
new
Registry
<
TransformerUIRegistyItem
<
any
>>
(()
=>
{
return
[
reduceTransformRegistryItem
,
filterFieldsByNameTransformRegistryItem
];
});
packages/grafana-ui/src/components/TransformersUI/types.ts
0 → 100644
View file @
7d32caea
import
React
from
'react'
;
import
{
DataFrame
,
RegistryItem
,
DataTransformerInfo
}
from
'@grafana/data'
;
export
interface
TransformerUIRegistyItem
<
TOptions
>
extends
RegistryItem
{
component
:
React
.
ComponentType
<
TransformerUIProps
<
TOptions
>>
;
transformer
:
DataTransformerInfo
<
TOptions
>
;
}
export
interface
TransformerUIProps
<
T
>
{
// Transformer configuration, persisted on panel's model
options
:
T
;
// Pre-transformation data frame
input
:
DataFrame
[];
onChange
:
(
options
:
T
)
=>
void
;
}
packages/grafana-ui/src/components/index.ts
View file @
7d32caea
...
...
@@ -78,5 +78,10 @@ export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggesti
export
{
DataLinksEditor
}
from
'./DataLinks/DataLinksEditor'
;
export
{
DataLinksContextMenu
}
from
'./DataLinks/DataLinksContextMenu'
;
export
{
SeriesIcon
}
from
'./Legend/SeriesIcon'
;
export
{
transformersUIRegistry
}
from
'./TransformersUI/transformers'
;
export
{
TransformationRow
}
from
'./TransformersUI/TransformationRow'
;
export
{
TransformationsEditor
}
from
'./TransformersUI/TransformationsEditor'
;
export
{
JSONFormatter
}
from
'./JSONFormatter/JSONFormatter'
;
export
{
JsonExplorer
}
from
'./JSONFormatter/json_explorer/json_explorer'
;
export
{
ErrorBoundary
,
ErrorBoundaryAlert
}
from
'./ErrorBoundary/ErrorBoundary'
;
export
{
AlphaNotice
}
from
'./AlphaNotice/AlphaNotice'
;
pkg/api/frontendsettings.go
View file @
7d32caea
...
...
@@ -195,6 +195,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"env"
:
setting
.
Env
,
"isEnterprise"
:
setting
.
IsEnterprise
,
},
"featureToggles"
:
hs
.
Cfg
.
FeatureToggles
,
}
return
jsonObj
,
nil
...
...
pkg/setting/setting.go
View file @
7d32caea
...
...
@@ -266,6 +266,8 @@ type Cfg struct {
EditorsCanAdmin
bool
ApiKeyMaxSecondsToLive
int64
FeatureToggles
map
[
string
]
bool
}
type
CommandLineArgs
struct
{
...
...
@@ -941,6 +943,17 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg
.
PluginsEnableAlpha
=
pluginsSection
.
Key
(
"enable_alpha"
)
.
MustBool
(
false
)
cfg
.
PluginsAppsSkipVerifyTLS
=
pluginsSection
.
Key
(
"app_tls_skip_verify_insecure"
)
.
MustBool
(
false
)
// Read and populate feature toggles list
featureTogglesSection
:=
iniFile
.
Section
(
"feature_toggles"
)
cfg
.
FeatureToggles
=
make
(
map
[
string
]
bool
)
featuresTogglesStr
,
err
:=
valueAsString
(
featureTogglesSection
,
"enable"
,
""
)
if
err
!=
nil
{
return
err
}
for
_
,
feature
:=
range
util
.
SplitString
(
featuresTogglesStr
)
{
cfg
.
FeatureToggles
[
feature
]
=
true
}
// check old location for this option
if
panelsSection
.
Key
(
"enable_alpha"
)
.
MustBool
(
false
)
{
cfg
.
PluginsEnableAlpha
=
true
...
...
public/app/core/components/jsontree/jsontree.ts
View file @
7d32caea
import
coreModule
from
'app/core/core_module'
;
import
{
JsonExplorer
}
from
'
../json_explorer/json_explorer
'
;
import
{
JsonExplorer
}
from
'
@grafana/ui
'
;
coreModule
.
directive
(
'jsonTree'
,
[
function
jsonTreeDirective
()
{
...
...
public/app/core/core.ts
View file @
7d32caea
...
...
@@ -16,7 +16,7 @@ import './utils/outline';
import
'./components/colorpicker/spectrum_picker'
;
import
'./services/search_srv'
;
import
'./services/ng_react'
;
import
{
colors
}
from
'@grafana/ui/'
;
import
{
colors
,
JsonExplorer
}
from
'@grafana/ui/'
;
import
{
searchDirective
}
from
'./components/search/search'
;
import
{
infoPopover
}
from
'./components/info_popover'
;
...
...
@@ -38,7 +38,6 @@ import { assignModelProperties } from './utils/model_utils';
import
{
contextSrv
}
from
'./services/context_srv'
;
import
{
KeybindingSrv
}
from
'./services/keybindingSrv'
;
import
{
helpModal
}
from
'./components/help/help'
;
import
{
JsonExplorer
}
from
'./components/json_explorer/json_explorer'
;
import
{
NavModelSrv
}
from
'./nav_model_srv'
;
import
{
geminiScrollbar
}
from
'./components/scroll/scroll'
;
import
{
orgSwitcher
}
from
'./components/org_switcher'
;
...
...
public/app/features/alerting/TestRuleResult.tsx
View file @
7d32caea
import
React
,
{
PureComponent
}
from
'react'
;
import
{
JSONFormatter
}
from
'app/core/components/JSONFormatter/JSONFormatter'
;
import
appEvents
from
'app/core/app_events'
;
import
{
CopyToClipboard
}
from
'app/core/components/CopyToClipboard/CopyToClipboard'
;
import
{
getBackendSrv
}
from
'@grafana/runtime'
;
import
{
DashboardModel
}
from
'../dashboard/state/DashboardModel'
;
import
{
LoadingPlaceholder
}
from
'@grafana/ui'
;
import
{
LoadingPlaceholder
,
JSONFormatter
}
from
'@grafana/ui'
;
export
interface
Props
{
panelId
:
number
;
...
...
public/app/features/dashboard/dashgrid/PanelChrome.tsx
View file @
7d32caea
...
...
@@ -172,7 +172,6 @@ export class PanelChrome extends PureComponent<Props, State> {
if
(
!
this
.
querySubscription
)
{
this
.
querySubscription
=
queryRunner
.
subscribe
(
this
.
panelDataObserver
);
}
queryRunner
.
run
({
datasource
:
panel
.
datasource
,
queries
:
panel
.
targets
,
...
...
@@ -186,6 +185,7 @@ export class PanelChrome extends PureComponent<Props, State> {
minInterval
:
panel
.
interval
,
scopedVars
:
panel
.
scopedVars
,
cacheTimeout
:
panel
.
cacheTimeout
,
transformations
:
panel
.
transformations
,
});
}
};
...
...
public/app/features/dashboard/panel_editor/QueriesTab.tsx
View file @
7d32caea
// Libraries
import
React
,
{
PureComponent
}
from
'react'
;
import
_
from
'lodash'
;
import
{
css
}
from
'emotion'
;
// Components
import
{
EditorTabBody
,
EditorToolbarView
}
from
'./EditorTabBody'
;
import
{
DataSourcePicker
}
from
'app/core/components/Select/DataSourcePicker'
;
import
{
QueryInspector
}
from
'./QueryInspector'
;
import
{
QueryOptions
}
from
'./QueryOptions'
;
import
{
PanelOptionsGroup
}
from
'@grafana/ui'
;
import
{
PanelOptionsGroup
,
TransformationsEditor
}
from
'@grafana/ui'
;
import
{
QueryEditorRow
}
from
'./QueryEditorRow'
;
// Services
...
...
@@ -18,8 +19,8 @@ import config from 'app/core/config';
// Types
import
{
PanelModel
}
from
'../state/PanelModel'
;
import
{
DashboardModel
}
from
'../state/DashboardModel'
;
import
{
DataQuery
,
DataSourceSelectItem
,
PanelData
}
from
'@grafana/ui'
;
import
{
LoadingState
}
from
'@grafana/data'
;
import
{
DataQuery
,
DataSourceSelectItem
,
PanelData
,
AlphaNotice
,
PluginState
}
from
'@grafana/ui'
;
import
{
LoadingState
,
DataTransformerConfig
}
from
'@grafana/data'
;
import
{
PluginHelp
}
from
'app/core/components/PluginHelp/PluginHelp'
;
import
{
PanelQueryRunnerFormat
}
from
'../state/PanelQueryRunner'
;
import
{
Unsubscribable
}
from
'rxjs'
;
...
...
@@ -215,15 +216,24 @@ export class QueriesTab extends PureComponent<Props, State> {
this
.
forceUpdate
();
};
onTransformersChange
=
(
transformers
:
DataTransformerConfig
[])
=>
{
this
.
props
.
panel
.
setTransformations
(
transformers
);
this
.
forceUpdate
();
};
setScrollTop
=
(
event
:
React
.
MouseEvent
<
HTMLElement
>
)
=>
{
const
target
=
event
.
target
as
HTMLElement
;
this
.
setState
({
scrollTop
:
target
.
scrollTop
});
};
getCurrentData
=
(
applyTransformations
=
true
)
=>
{
const
queryRunner
=
this
.
props
.
panel
.
getQueryRunner
();
return
queryRunner
.
getCurrentData
(
applyTransformations
).
series
;
};
render
()
{
const
{
panel
,
dashboard
}
=
this
.
props
;
const
{
currentDS
,
scrollTop
,
data
}
=
this
.
state
;
const
queryInspector
:
EditorToolbarView
=
{
title
:
'Query Inspector'
,
render
:
this
.
renderQueryInspector
,
...
...
@@ -235,6 +245,8 @@ export class QueriesTab extends PureComponent<Props, State> {
render
:
this
.
renderHelp
,
};
const
enableTransformations
=
config
.
featureToggles
.
transformations
;
return
(
<
EditorTabBody
heading=
"Query"
...
...
@@ -243,6 +255,7 @@ export class QueriesTab extends PureComponent<Props, State> {
setScrollTop=
{
this
.
setScrollTop
}
scrollTop=
{
scrollTop
}
>
<>
{
isSharedDashboardQuery
(
currentDS
.
name
)
?
(
<
DashboardQueryEditor
panel=
{
panel
}
panelData=
{
data
}
onChange=
{
query
=>
this
.
onQueryChange
(
query
,
0
)
}
/>
)
:
(
...
...
@@ -269,6 +282,31 @@ export class QueriesTab extends PureComponent<Props, State> {
</
PanelOptionsGroup
>
</>
)
}
{
enableTransformations
&&
(
<
PanelOptionsGroup
title=
{
<>
Transform query results
<
AlphaNotice
state=
{
PluginState
.
alpha
}
className=
{
css
`
margin-left: 16px;
`
}
/>
</>
}
>
{
this
.
state
.
data
.
state
!==
LoadingState
.
NotStarted
&&
(
<
TransformationsEditor
transformations=
{
this
.
props
.
panel
.
transformations
||
[]
}
onChange=
{
this
.
onTransformersChange
}
getCurrentData=
{
this
.
getCurrentData
}
/>
)
}
</
PanelOptionsGroup
>
)
}
</>
</
EditorTabBody
>
);
}
...
...
public/app/features/dashboard/panel_editor/QueryInspector.tsx
View file @
7d32caea
import
React
,
{
PureComponent
}
from
'react'
;
import
{
JSONFormatter
}
from
'app/core/components/JSONFormatter/JSONFormatter'
;
import
appEvents
from
'app/core/app_events'
;
import
{
CopyToClipboard
}
from
'app/core/components/CopyToClipboard/CopyToClipboard'
;
import
{
LoadingPlaceholder
}
from
'@grafana/ui'
;
import
{
LoadingPlaceholder
,
JSONFormatter
}
from
'@grafana/ui'
;
interface
DsQuery
{
isLoading
:
boolean
;
...
...
public/app/features/dashboard/state/PanelModel.ts
View file @
7d32caea
...
...
@@ -7,7 +7,7 @@ import { getNextRefIdChar } from 'app/core/utils/query';
// Types
import
{
DataQuery
,
ScopedVars
,
DataQueryResponseData
,
PanelPlugin
}
from
'@grafana/ui'
;
import
{
DataLink
}
from
'@grafana/data'
;
import
{
DataLink
,
DataTransformerConfig
}
from
'@grafana/data'
;
import
config
from
'app/core/config'
;
...
...
@@ -66,6 +66,7 @@ const mustKeepProps: { [str: string]: boolean } = {
transparent
:
true
,
pluginVersion
:
true
,
queryRunner
:
true
,
transformations
:
true
,
};
const
defaults
:
any
=
{
...
...
@@ -93,6 +94,7 @@ export class PanelModel {
panels
?:
any
;
soloMode
?:
boolean
;
targets
:
DataQuery
[];
transformations
?:
DataTransformerConfig
[];
datasource
:
string
;
thresholds
?:
any
;
pluginVersion
?:
string
;
...
...
@@ -290,7 +292,6 @@ export class PanelModel {
}
else
if
(
oldOptions
&&
oldOptions
.
options
)
{
old
=
oldOptions
.
options
;
}
this
.
options
=
this
.
options
||
{};
Object
.
assign
(
this
.
options
,
newPlugin
.
onPanelTypeChanged
(
this
.
options
,
oldPluginId
,
old
));
}
...
...
@@ -344,6 +345,14 @@ export class PanelModel {
this
.
queryRunner
=
null
;
}
}
setTransformations
(
transformations
:
DataTransformerConfig
[])
{
// save for persistence
this
.
transformations
=
transformations
;
// update query runner transformers
this
.
getQueryRunner
().
setTransform
(
transformations
);
}
}
function
getPluginVersion
(
plugin
:
PanelPlugin
):
string
{
...
...
public/app/features/dashboard/state/PanelQueryRunner.ts
View file @
7d32caea
...
...
@@ -13,7 +13,8 @@ import { isSharedDashboardQuery, SharedQueryRunner } from 'app/plugins/datasourc
// Types
import
{
PanelData
,
DataQuery
,
ScopedVars
,
DataQueryRequest
,
DataSourceApi
,
DataSourceJsonData
}
from
'@grafana/ui'
;
import
{
TimeRange
}
from
'@grafana/data'
;
import
{
TimeRange
,
DataTransformerConfig
,
transformDataFrame
,
toLegacyResponseData
}
from
'@grafana/data'
;
import
config
from
'app/core/config'
;
export
interface
QueryRunnerOptions
<
TQuery
extends
DataQuery
=
DataQuery
,
...
...
@@ -32,6 +33,7 @@ export interface QueryRunnerOptions<
scopedVars
?:
ScopedVars
;
cacheTimeout
?:
string
;
delayStateNotification
?:
number
;
// default 100ms.
transformations
?:
DataTransformerConfig
[];
}
export
enum
PanelQueryRunnerFormat
{
...
...
@@ -49,6 +51,7 @@ export class PanelQueryRunner {
private
subject
?:
Subject
<
PanelData
>
;
private
state
=
new
PanelQueryState
();
private
transformations
?:
DataTransformerConfig
[];
// Listen to another panel for changes
private
sharedQueryRunner
:
SharedQueryRunner
;
...
...
@@ -63,6 +66,27 @@ export class PanelQueryRunner {
}
/**
* Get the last result -- optionally skip the transformation
*/
// TODO: add tests
getCurrentData
(
transform
=
true
):
PanelData
{
const
v
=
this
.
state
.
validateStreamsAndGetPanelData
();
const
transformData
=
config
.
featureToggles
.
transformations
&&
transform
;
const
hasTransformations
=
this
.
transformations
&&
this
.
transformations
.
length
;
if
(
transformData
&&
hasTransformations
)
{
const
processed
=
transformDataFrame
(
this
.
transformations
,
v
.
series
);
return
{
...
v
,
series
:
processed
,
legacy
:
processed
.
map
(
p
=>
toLegacyResponseData
(
p
)),
};
}
return
v
;
}
/**
* Listen for updates to the PanelData. If a query has already run for this panel,
* the results will be immediatly passed to the observer
*/
...
...
@@ -78,7 +102,9 @@ export class PanelQueryRunner {
// Send the last result
if
(
this
.
state
.
isStarted
())
{
observer
.
next
(
this
.
state
.
getDataAfterCheckingFormats
());
// Force check formats again?
this
.
state
.
getDataAfterCheckingFormats
();
observer
.
next
(
this
.
getCurrentData
());
// transformed
}
return
this
.
subject
.
subscribe
(
observer
);
...
...
@@ -98,9 +124,17 @@ export class PanelQueryRunner {
return
this
.
subscribe
(
runner
.
subject
,
format
);
}
getCurrentData
():
PanelData
{
return
this
.
state
.
validateStreamsAndGetPanelData
();
/**
* Change the current transformation and notify all listeners
* Should be used only by panel editor to update the transformers
*/
setTransform
=
(
transformations
?:
DataTransformerConfig
[])
=>
{
this
.
transformations
=
transformations
;
if
(
this
.
state
.
isStarted
())
{
this
.
onStreamingDataUpdated
();
}
};
async
run
(
options
:
QueryRunnerOptions
):
Promise
<
PanelData
>
{
const
{
state
}
=
this
;
...
...
@@ -200,13 +234,14 @@ export class PanelQueryRunner {
}
},
delayStateNotification
||
500
);
const
data
=
await
state
.
execute
(
ds
,
request
)
;
this
.
transformations
=
options
.
transformations
;
const
data
=
await
state
.
execute
(
ds
,
request
);
// Clear the delayed loading state timeout
clearTimeout
(
loadingStateTimeoutId
);
// Broadcast results
this
.
subject
.
next
(
data
);
this
.
subject
.
next
(
this
.
getCurrentData
()
);
return
data
;
}
catch
(
err
)
{
clearTimeout
(
loadingStateTimeoutId
);
...
...
@@ -223,7 +258,7 @@ export class PanelQueryRunner {
*/
onStreamingDataUpdated
=
throttle
(
()
=>
{
this
.
subject
.
next
(
this
.
state
.
validateStreamsAndGetPanel
Data
());
this
.
subject
.
next
(
this
.
getCurrent
Data
());
},
50
,
{
trailing
:
true
,
leading
:
true
}
...
...
@@ -241,6 +276,14 @@ export class PanelQueryRunner {
// Will cancel and disconnect any open requets
this
.
state
.
cancel
(
'destroy'
);
}
setState
=
(
state
:
PanelQueryState
)
=>
{
this
.
state
=
state
;
};
getState
=
()
=>
{
return
this
.
state
;
};
}
async
function
getDataSource
(
...
...
public/app/features/panel/metrics_panel_ctrl.ts
View file @
7d32caea
...
...
@@ -212,6 +212,7 @@ class MetricsPanelCtrl extends PanelCtrl {
minInterval
:
panel
.
interval
,
scopedVars
:
panel
.
scopedVars
,
cacheTimeout
:
panel
.
cacheTimeout
,
transformations
:
panel
.
transformations
,
});
}
...
...
public/app/features/plugins/PluginStateInfo.tsx
View file @
7d32caea
import
React
,
{
FC
,
useContext
}
from
'react'
;
import
React
,
{
FC
}
from
'react'
;
import
{
PluginState
,
AlphaNotice
}
from
'@grafana/ui'
;
import
{
css
}
from
'emotion'
;
import
{
PluginState
,
Tooltip
,
ThemeContext
}
from
'@grafana/ui'
;
import
{
PopoverContent
}
from
'@grafana/ui/src/components/Tooltip/Tooltip'
;
interface
Props
{
state
?:
PluginState
;
}
function
getPluginStateInfoText
(
state
?:
PluginState
):
PopoverCont
ent
|
null
{
function
getPluginStateInfoText
(
state
?:
PluginState
):
JSX
.
Elem
ent
|
null
{
switch
(
state
)
{
case
PluginState
.
alpha
:
return
(
...
...
@@ -30,30 +29,15 @@ function getPluginStateInfoText(state?: PluginState): PopoverContent | null {
const
PluginStateinfo
:
FC
<
Props
>
=
props
=>
{
const
text
=
getPluginStateInfoText
(
props
.
state
);
if
(
!
text
)
{
return
null
;
}
const
theme
=
useContext
(
ThemeContext
);
const
styles
=
css
`
background: linear-gradient(to bottom,
${
theme
.
colors
.
blueBase
}
,
${
theme
.
colors
.
blueShade
}
);
color:
${
theme
.
colors
.
gray7
}
;
white-space: nowrap;
border-radius: 3px;
text-shadow: none;
font-size: 13px;
padding: 4px 8px;
margin-left: 16px;
cursor: help;
`
;
return
(
<
Tooltip
content=
{
text
}
theme=
{
'info'
}
placement=
{
'top'
}
>
<
div
className=
{
styles
}
>
<
i
className=
"fa fa-warning"
/>
{
props
.
state
}
</
div
>
</
Tooltip
>
<
AlphaNotice
state=
{
props
.
state
}
text=
{
text
}
className=
{
css
`
margin-left: 16px;
`
}
/>
);
};
...
...
public/app/plugins/datasource/dashboard/SharedQueryRunner.ts
View file @
7d32caea
...
...
@@ -57,6 +57,7 @@ export class SharedQueryRunner {
this
.
listenToPanelId
=
panelId
;
this
.
listenToRunner
=
this
.
listenToPanel
.
getQueryRunner
();
this
.
subscription
=
this
.
listenToRunner
.
chain
(
this
.
runner
);
this
.
runner
.
setState
(
this
.
listenToRunner
.
getState
());
console
.
log
(
'Connecting panel: '
,
this
.
containerPanel
.
id
,
'to:'
,
this
.
listenToPanelId
);
}
...
...
public/app/plugins/panel/graph/module.ts
View file @
7d32caea
...
...
@@ -149,7 +149,7 @@ class GraphCtrl extends MetricsPanelCtrl {
this
.
events
.
on
(
'render'
,
this
.
onRender
.
bind
(
this
));
this
.
events
.
on
(
'data-received'
,
this
.
onDataReceived
.
bind
(
this
));
this
.
events
.
on
(
'data-frames-received'
,
this
.
onDataReceived
.
bind
(
this
));
this
.
events
.
on
(
'data-frames-received'
,
this
.
onData
Frames
Received
.
bind
(
this
));
this
.
events
.
on
(
'data-error'
,
this
.
onDataError
.
bind
(
this
));
this
.
events
.
on
(
'data-snapshot-load'
,
this
.
onDataSnapshotLoad
.
bind
(
this
));
this
.
events
.
on
(
'init-edit-mode'
,
this
.
onInitEditMode
.
bind
(
this
));
...
...
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