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
6f2315d5
Commit
6f2315d5
authored
Oct 25, 2018
by
David Kaltschmidt
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Moved prom language features to datasource language provider
parent
54a3e2d1
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
685 additions
and
679 deletions
+685
-679
public/app/features/explore/Explore.tsx
+1
-6
public/app/features/explore/PromQueryField.test.tsx
+1
-228
public/app/features/explore/PromQueryField.tsx
+31
-347
public/app/features/explore/QueryField.tsx
+7
-81
public/app/features/explore/QueryRows.tsx
+3
-3
public/app/features/explore/Typeahead.tsx
+10
-10
public/app/plugins/datasource/prometheus/datasource.ts
+3
-0
public/app/plugins/datasource/prometheus/language_provider.ts
+334
-0
public/app/plugins/datasource/prometheus/language_utils.ts
+0
-3
public/app/plugins/datasource/prometheus/promql.ts
+0
-0
public/app/plugins/datasource/prometheus/specs/language_provider.test.ts
+202
-0
public/app/plugins/datasource/prometheus/specs/language_utils.test.ts
+1
-1
public/app/types/explore.ts
+92
-0
No files found.
public/app/features/explore/Explore.tsx
View file @
6f2315d5
...
...
@@ -695,11 +695,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
});
}
request
=
url
=>
{
const
{
datasource
}
=
this
.
state
;
return
datasource
.
metadataRequest
(
url
);
};
cloneState
():
ExploreState
{
// Copy state, but copy queries including modifications
return
{
...
...
@@ -831,9 +826,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
{
datasource
&&
!
datasourceError
?
(
<
div
className=
"explore-container"
>
<
QueryRows
datasource=
{
datasource
}
history=
{
history
}
queries=
{
queries
}
request=
{
this
.
request
}
onAddQueryRow=
{
this
.
onAddQueryRow
}
onChangeQuery=
{
this
.
onChangeQuery
}
onClickHintFix=
{
this
.
onModifyQueries
}
...
...
public/app/features/explore/PromQueryField.test.tsx
View file @
6f2315d5
import
React
from
'react'
;
import
Enzyme
,
{
shallow
}
from
'enzyme'
;
import
Adapter
from
'enzyme-adapter-react-16'
;
import
Plain
from
'slate-plain-serializer'
;
import
PromQueryField
,
{
groupMetricsByPrefix
,
RECORDING_RULES_GROUP
}
from
'./PromQueryField'
;
Enzyme
.
configure
({
adapter
:
new
Adapter
()
});
describe
(
'PromQueryField typeahead handling'
,
()
=>
{
const
defaultProps
=
{
request
:
()
=>
({
data
:
{
data
:
[]
}
}),
};
it
(
'returns default suggestions on emtpty context'
,
()
=>
{
const
instance
=
shallow
(<
PromQueryField
{
...
defaultProps
}
/>).
instance
()
as
PromQueryField
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
describe
(
'range suggestions'
,
()
=>
{
it
(
'returns range suggestions in range context'
,
()
=>
{
const
instance
=
shallow
(<
PromQueryField
{
...
defaultProps
}
/>).
instance
()
as
PromQueryField
;
const
result
=
instance
.
getTypeahead
({
text
:
'1'
,
prefix
:
'1'
,
wrapperClasses
:
[
'context-range'
]
});
expect
(
result
.
context
).
toBe
(
'context-range'
);
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
).
toEqual
([
{
items
:
[{
label
:
'1m'
},
{
label
:
'5m'
},
{
label
:
'10m'
},
{
label
:
'30m'
},
{
label
:
'1h'
}],
label
:
'Range vector'
,
},
]);
});
});
describe
(
'metric suggestions'
,
()
=>
{
it
(
'returns metrics suggestions by default'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
metrics=
{
[
'foo'
,
'bar'
]
}
/>
).
instance
()
as
PromQueryField
;
const
result
=
instance
.
getTypeahead
({
text
:
'a'
,
prefix
:
'a'
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
it
(
'returns default suggestions after a binary operator'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
metrics=
{
[
'foo'
,
'bar'
]
}
/>
).
instance
()
as
PromQueryField
;
const
result
=
instance
.
getTypeahead
({
text
:
'*'
,
prefix
:
''
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
});
describe
(
'label suggestions'
,
()
=>
{
it
(
'returns default label suggestions on label context and no metric'
,
()
=>
{
const
instance
=
shallow
(<
PromQueryField
{
...
defaultProps
}
/>).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
1
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'job'
},
{
label
:
'instance'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on label context and metric'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{__name__="metric"}'
:
[
'bar'
]
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'metric{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
7
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on label context but leaves out labels that already exist'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{job1="foo",job2!="foo",job3=~"foo"}'
:
[
'bar'
,
'job1'
,
'job2'
,
'job3'
]
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'{job1="foo",job2!="foo",job3=~"foo",}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
36
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label value suggestions inside a label value context after a negated matching operator'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{}'
:
[
'label'
]
}
}
labelValues=
{
{
'{}'
:
{
label
:
[
'a'
,
'b'
,
'c'
]
}
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'{label!=}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
8
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
'!='
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
labelKey
:
'label'
,
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-label-values'
);
expect
(
result
.
suggestions
).
toEqual
([
{
items
:
[{
label
:
'a'
},
{
label
:
'b'
},
{
label
:
'c'
}],
label
:
'Label values for "label"'
,
},
]);
});
it
(
'returns a refresher on label context and unavailable metric'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{__name__="foo"}'
:
[
'bar'
]
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'metric{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
7
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeInstanceOf
(
Promise
);
expect
(
result
.
suggestions
).
toEqual
([]);
});
it
(
'returns label values on label context when given a metric and a label key'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{__name__="metric"}'
:
[
'bar'
]
}
}
labelValues=
{
{
'{__name__="metric"}'
:
{
bar
:
[
'baz'
]
}
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'metric{bar=ba}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
13
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
'=ba'
,
prefix
:
'ba'
,
wrapperClasses
:
[
'context-labels'
],
labelKey
:
'bar'
,
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-label-values'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'baz'
}],
label
:
'Label values for "bar"'
}]);
});
it
(
'returns label suggestions on aggregation context and metric w/ selector'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{__name__="metric",foo="xx"}'
:
[
'bar'
]
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'sum(metric{foo="xx"}) by ()'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
26
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-aggregation'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-aggregation'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on aggregation context and metric w/o selector'
,
()
=>
{
const
instance
=
shallow
(
<
PromQueryField
{
...
defaultProps
}
labelKeys=
{
{
'{__name__="metric"}'
:
[
'bar'
]
}
}
/>
).
instance
()
as
PromQueryField
;
const
value
=
Plain
.
deserialize
(
'sum(metric) by ()'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
16
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
getTypeahead
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-aggregation'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-aggregation'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
});
});
import
{
groupMetricsByPrefix
,
RECORDING_RULES_GROUP
}
from
'./PromQueryField'
;
describe
(
'groupMetricsByPrefix()'
,
()
=>
{
it
(
'returns an empty group for no metrics'
,
()
=>
{
...
...
public/app/features/explore/PromQueryField.tsx
View file @
6f2315d5
import
_
from
'lodash'
;
import
moment
from
'moment'
;
import
React
from
'react'
;
import
{
Value
}
from
'slate'
;
import
Cascader
from
'rc-cascader'
;
import
PluginPrism
from
'slate-prism'
;
import
Prism
from
'prismjs'
;
import
{
TypeaheadOutput
}
from
'app/types/explore'
;
// dom also includes Element polyfills
import
{
getNextCharacter
,
getPreviousCousin
}
from
'./utils/dom'
;
import
PrismPromql
,
{
FUNCTIONS
}
from
'./slate-plugins/prism/promql'
;
import
BracesPlugin
from
'./slate-plugins/braces'
;
import
RunnerPlugin
from
'./slate-plugins/runner'
;
import
{
processLabels
,
RATE_RANGES
,
cleanText
,
parseSelector
}
from
'./utils/prometheus'
;
import
TypeaheadField
,
{
Suggestion
,
SuggestionGroup
,
TypeaheadInput
,
TypeaheadFieldState
,
TypeaheadOutput
,
}
from
'./QueryField'
;
const
DEFAULT_KEYS
=
[
'job'
,
'instance'
];
const
EMPTY_SELECTOR
=
'{}'
;
import
TypeaheadField
,
{
TypeaheadInput
,
TypeaheadFieldState
}
from
'./QueryField'
;
const
HISTOGRAM_GROUP
=
'__histograms__'
;
const
HISTOGRAM_SELECTOR
=
'{le!=""}'
;
// Returns all timeseries for histograms
const
HISTORY_ITEM_COUNT
=
5
;
const
HISTORY_COUNT_CUTOFF
=
1000
*
60
*
60
*
24
;
// 24h
const
METRIC_MARK
=
'metric'
;
const
PRISM_SYNTAX
=
'promql'
;
export
const
RECORDING_RULES_GROUP
=
'__recording_rules__'
;
export
const
wrapLabel
=
(
label
:
string
)
=>
({
label
});
export
const
setFunctionMove
=
(
suggestion
:
Suggestion
):
Suggestion
=>
{
suggestion
.
move
=
-
1
;
return
suggestion
;
};
// Syntax highlighting
Prism
.
languages
[
PRISM_SYNTAX
]
=
PrismPromql
;
function
setPrismTokens
(
language
,
field
,
values
,
alias
=
'variable'
)
{
Prism
.
languages
[
language
][
field
]
=
{
alias
,
pattern
:
new
RegExp
(
`(?:^|\\s)(
${
values
.
join
(
'|'
)}
)(?:$|\\s)`
),
};
}
export
function
addHistoryMetadata
(
item
:
Suggestion
,
history
:
any
[]):
Suggestion
{
const
cutoffTs
=
Date
.
now
()
-
HISTORY_COUNT_CUTOFF
;
const
historyForItem
=
history
.
filter
(
h
=>
h
.
ts
>
cutoffTs
&&
h
.
query
===
item
.
label
);
const
count
=
historyForItem
.
length
;
const
recent
=
historyForItem
[
0
];
let
hint
=
`Queried
${
count
}
times in the last 24h.`
;
if
(
recent
)
{
const
lastQueried
=
moment
(
recent
.
ts
).
fromNow
();
hint
=
`
${
hint
}
Last queried
${
lastQueried
}
.`
;
}
return
{
...
item
,
documentation
:
hint
,
};
}
export
function
groupMetricsByPrefix
(
metrics
:
string
[],
delimiter
=
'_'
):
CascaderOption
[]
{
// Filter out recording rules and insert as first option
const
ruleRegex
=
/:
\w
+:/
;
...
...
@@ -133,48 +89,36 @@ interface CascaderOption {
}
interface
PromQueryFieldProps
{
datasource
:
any
;
error
?:
string
;
hint
?:
any
;
histogramMetrics
?:
string
[];
history
?:
any
[];
initialQuery
?:
string
|
null
;
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
metrics
?:
string
[];
metricsByPrefix
?:
CascaderOption
[];
onClickHintFix
?:
(
action
:
any
)
=>
void
;
onPressEnter
?:
()
=>
void
;
onQueryChange
?:
(
value
:
string
,
override
?:
boolean
)
=>
void
;
portalOrigin
?:
string
;
request
?:
(
url
:
string
)
=>
any
;
supportsLogs
?:
boolean
;
// To be removed after Logging gets its own query field
}
interface
PromQueryFieldState
{
histogramMetrics
:
string
[];
labelKeys
:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
logLabelOptions
:
any
[];
metrics
:
string
[];
metricsOptions
:
any
[];
metricsByPrefix
:
CascaderOption
[];
syntaxLoaded
:
boolean
;
}
interface
PromTypeaheadInput
{
text
:
string
;
prefix
:
string
;
wrapperClasses
:
string
[];
labelKey
?:
string
;
value
?:
Value
;
}
class
PromQueryField
extends
React
.
PureComponent
<
PromQueryFieldProps
,
PromQueryFieldState
>
{
plugins
:
any
[];
languageProvider
:
any
;
constructor
(
props
:
PromQueryFieldProps
,
context
)
{
super
(
props
,
context
);
if
(
props
.
datasource
.
languageProvider
)
{
this
.
languageProvider
=
props
.
datasource
.
languageProvider
;
}
this
.
plugins
=
[
BracesPlugin
(),
RunnerPlugin
({
handler
:
props
.
onPressEnter
}),
...
...
@@ -185,26 +129,16 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
];
this
.
state
=
{
histogramMetrics
:
props
.
histogramMetrics
||
[],
labelKeys
:
props
.
labelKeys
||
{},
labelValues
:
props
.
labelValues
||
{},
logLabelOptions
:
[],
metrics
:
props
.
metrics
||
[],
metricsByPrefix
:
props
.
metricsByPrefix
||
[],
metricsByPrefix
:
[],
metricsOptions
:
[],
syntaxLoaded
:
false
,
};
}
componentDidMount
()
{
// Temporarily reused by logging
const
{
supportsLogs
}
=
this
.
props
;
if
(
supportsLogs
)
{
this
.
fetchLogLabels
();
}
else
{
// Usual actions
this
.
fetchMetricNames
();
this
.
fetchHistogramMetrics
();
if
(
this
.
languageProvider
)
{
this
.
languageProvider
.
start
().
then
(()
=>
this
.
onReceiveMetrics
());
}
}
...
...
@@ -262,15 +196,19 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
onReceiveMetrics
=
()
=>
{
const
{
histogramMetrics
,
metrics
,
metricsByPrefix
}
=
this
.
state
;
const
{
histogramMetrics
,
metrics
}
=
this
.
languageProvider
;
if
(
!
metrics
)
{
return
;
}
// Update global prism config
setPrismTokens
(
PRISM_SYNTAX
,
METRIC_MARK
,
metrics
);
Prism
.
languages
[
PRISM_SYNTAX
]
=
this
.
languageProvider
.
getSyntax
();
Prism
.
languages
[
PRISM_SYNTAX
][
METRIC_MARK
]
=
{
alias
:
'variable'
,
pattern
:
new
RegExp
(
`(?:^|\\s)(
${
metrics
.
join
(
'|'
)}
)(?:$|\\s)`
),
};
// Build metrics tree
const
metricsByPrefix
=
groupMetricsByPrefix
(
metrics
);
const
histogramOptions
=
histogramMetrics
.
map
(
hm
=>
({
label
:
hm
,
value
:
hm
}));
const
metricsOptions
=
[
{
label
:
'Histograms'
,
value
:
HISTOGRAM_GROUP
,
children
:
histogramOptions
},
...
...
@@ -281,6 +219,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
onTypeahead
=
(
typeahead
:
TypeaheadInput
):
TypeaheadOutput
=>
{
if
(
!
this
.
languageProvider
)
{
return
{
suggestions
:
[]
};
}
const
{
history
}
=
this
.
props
;
const
{
prefix
,
text
,
value
,
wrapperNode
}
=
typeahead
;
// Get DOM-dependent context
...
...
@@ -289,279 +232,20 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
const
labelKey
=
labelKeyNode
&&
labelKeyNode
.
textContent
;
const
nextChar
=
getNextCharacter
();
const
result
=
this
.
getTypeahead
({
text
,
value
,
prefix
,
wrapperClasses
,
labelKey
});
const
result
=
this
.
languageProvider
.
provideCompletionItems
(
{
text
,
value
,
prefix
,
wrapperClasses
,
labelKey
},
{
history
}
);
console
.
log
(
'handleTypeahead'
,
wrapperClasses
,
text
,
prefix
,
nextChar
,
labelKey
,
result
.
context
);
return
result
;
};
// Keep this DOM-free for testing
getTypeahead
({
prefix
,
wrapperClasses
,
text
}:
PromTypeaheadInput
):
TypeaheadOutput
{
// Syntax spans have 3 classes by default. More indicate a recognized token
const
tokenRecognized
=
wrapperClasses
.
length
>
3
;
// Determine candidates by CSS context
if
(
_
.
includes
(
wrapperClasses
,
'context-range'
))
{
// Suggestions for metric[|]
return
this
.
getRangeTypeahead
();
}
else
if
(
_
.
includes
(
wrapperClasses
,
'context-labels'
))
{
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
return
this
.
getLabelTypeahead
.
apply
(
this
,
arguments
);
}
else
if
(
_
.
includes
(
wrapperClasses
,
'context-aggregation'
))
{
return
this
.
getAggregationTypeahead
.
apply
(
this
,
arguments
);
}
else
if
(
// Show default suggestions in a couple of scenarios
(
prefix
&&
!
tokenRecognized
)
||
// Non-empty prefix, but not inside known token
(
prefix
===
''
&&
!
text
.
match
(
/^
[\]
})
\s]
+$/
))
||
// Empty prefix, but not following a closing brace
text
.
match
(
/
[
+
\-
*
/
^%
]
/
)
// Anything after binary operator
)
{
return
this
.
getEmptyTypeahead
();
}
return
{
suggestions
:
[],
};
}
getEmptyTypeahead
():
TypeaheadOutput
{
const
{
history
}
=
this
.
props
;
const
{
metrics
}
=
this
.
state
;
const
suggestions
:
SuggestionGroup
[]
=
[];
if
(
history
&&
history
.
length
>
0
)
{
const
historyItems
=
_
.
chain
(
history
)
.
uniqBy
(
'query'
)
.
take
(
HISTORY_ITEM_COUNT
)
.
map
(
h
=>
h
.
query
)
.
map
(
wrapLabel
)
.
map
(
item
=>
addHistoryMetadata
(
item
,
history
))
.
value
();
suggestions
.
push
({
prefixMatch
:
true
,
skipSort
:
true
,
label
:
'History'
,
items
:
historyItems
,
});
}
suggestions
.
push
({
prefixMatch
:
true
,
label
:
'Functions'
,
items
:
FUNCTIONS
.
map
(
setFunctionMove
),
});
if
(
metrics
)
{
suggestions
.
push
({
label
:
'Metrics'
,
items
:
metrics
.
map
(
wrapLabel
),
});
}
return
{
suggestions
};
}
getRangeTypeahead
():
TypeaheadOutput
{
return
{
context
:
'context-range'
,
suggestions
:
[
{
label
:
'Range vector'
,
items
:
[...
RATE_RANGES
].
map
(
wrapLabel
),
},
],
};
}
getAggregationTypeahead
({
value
}:
PromTypeaheadInput
):
TypeaheadOutput
{
let
refresher
:
Promise
<
any
>
=
null
;
const
suggestions
:
SuggestionGroup
[]
=
[];
// sum(foo{bar="1"}) by (|)
const
line
=
value
.
anchorBlock
.
getText
();
const
cursorOffset
:
number
=
value
.
anchorOffset
;
// sum(foo{bar="1"}) by (
const
leftSide
=
line
.
slice
(
0
,
cursorOffset
);
const
openParensAggregationIndex
=
leftSide
.
lastIndexOf
(
'('
);
const
openParensSelectorIndex
=
leftSide
.
slice
(
0
,
openParensAggregationIndex
).
lastIndexOf
(
'('
);
const
closeParensSelectorIndex
=
leftSide
.
slice
(
openParensSelectorIndex
).
indexOf
(
')'
)
+
openParensSelectorIndex
;
// foo{bar="1"}
const
selectorString
=
leftSide
.
slice
(
openParensSelectorIndex
+
1
,
closeParensSelectorIndex
);
const
selector
=
parseSelector
(
selectorString
,
selectorString
.
length
-
2
).
selector
;
const
labelKeys
=
this
.
state
.
labelKeys
[
selector
];
if
(
labelKeys
)
{
suggestions
.
push
({
label
:
'Labels'
,
items
:
labelKeys
.
map
(
wrapLabel
)
});
}
else
{
refresher
=
this
.
fetchSeriesLabels
(
selector
);
}
return
{
refresher
,
suggestions
,
context
:
'context-aggregation'
,
};
}
getLabelTypeahead
({
text
,
wrapperClasses
,
labelKey
,
value
}:
PromTypeaheadInput
):
TypeaheadOutput
{
let
context
:
string
;
let
refresher
:
Promise
<
any
>
=
null
;
const
suggestions
:
SuggestionGroup
[]
=
[];
const
line
=
value
.
anchorBlock
.
getText
();
const
cursorOffset
:
number
=
value
.
anchorOffset
;
// Get normalized selector
let
selector
;
let
parsedSelector
;
try
{
parsedSelector
=
parseSelector
(
line
,
cursorOffset
);
selector
=
parsedSelector
.
selector
;
}
catch
{
selector
=
EMPTY_SELECTOR
;
}
const
containsMetric
=
selector
.
indexOf
(
'__name__='
)
>
-
1
;
const
existingKeys
=
parsedSelector
?
parsedSelector
.
labelKeys
:
[];
if
((
text
&&
text
.
match
(
/^!
?
=~
?
/
))
||
_
.
includes
(
wrapperClasses
,
'attr-value'
))
{
// Label values
if
(
labelKey
&&
this
.
state
.
labelValues
[
selector
]
&&
this
.
state
.
labelValues
[
selector
][
labelKey
])
{
const
labelValues
=
this
.
state
.
labelValues
[
selector
][
labelKey
];
context
=
'context-label-values'
;
suggestions
.
push
({
label
:
`Label values for "
${
labelKey
}
"`
,
items
:
labelValues
.
map
(
wrapLabel
),
});
}
}
else
{
// Label keys
const
labelKeys
=
this
.
state
.
labelKeys
[
selector
]
||
(
containsMetric
?
null
:
DEFAULT_KEYS
);
if
(
labelKeys
)
{
const
possibleKeys
=
_
.
difference
(
labelKeys
,
existingKeys
);
if
(
possibleKeys
.
length
>
0
)
{
context
=
'context-labels'
;
suggestions
.
push
({
label
:
`Labels`
,
items
:
possibleKeys
.
map
(
wrapLabel
)
});
}
}
}
// Query labels for selector
// Temporarily add skip for logging
if
(
selector
&&
!
this
.
state
.
labelValues
[
selector
]
&&
!
this
.
props
.
supportsLogs
)
{
if
(
selector
===
EMPTY_SELECTOR
)
{
// Query label values for default labels
refresher
=
Promise
.
all
(
DEFAULT_KEYS
.
map
(
key
=>
this
.
fetchLabelValues
(
key
)));
}
else
{
refresher
=
this
.
fetchSeriesLabels
(
selector
,
!
containsMetric
);
}
}
return
{
context
,
refresher
,
suggestions
};
}
request
=
url
=>
{
if
(
this
.
props
.
request
)
{
return
this
.
props
.
request
(
url
);
}
return
fetch
(
url
);
};
fetchHistogramMetrics
()
{
this
.
fetchSeriesLabels
(
HISTOGRAM_SELECTOR
,
true
,
()
=>
{
const
histogramSeries
=
this
.
state
.
labelValues
[
HISTOGRAM_SELECTOR
];
if
(
histogramSeries
&&
histogramSeries
[
'__name__'
])
{
const
histogramMetrics
=
histogramSeries
[
'__name__'
].
slice
().
sort
();
this
.
setState
({
histogramMetrics
},
this
.
onReceiveMetrics
);
}
});
}
// Temporarily here while reusing this field for logging
async
fetchLogLabels
()
{
const
url
=
'/api/prom/label'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
labelKeys
=
body
.
data
.
slice
().
sort
();
const
labelKeysBySelector
=
{
...
this
.
state
.
labelKeys
,
[
EMPTY_SELECTOR
]:
labelKeys
,
};
const
labelValuesByKey
=
{};
const
logLabelOptions
=
[];
for
(
const
key
of
labelKeys
)
{
const
valuesUrl
=
`/api/prom/label/
${
key
}
/values`
;
const
res
=
await
this
.
request
(
valuesUrl
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
values
=
body
.
data
.
slice
().
sort
();
labelValuesByKey
[
key
]
=
values
;
logLabelOptions
.
push
({
label
:
key
,
value
:
key
,
children
:
values
.
map
(
value
=>
({
label
:
value
,
value
})),
});
}
const
labelValues
=
{
[
EMPTY_SELECTOR
]:
labelValuesByKey
};
this
.
setState
({
labelKeys
:
labelKeysBySelector
,
labelValues
,
logLabelOptions
});
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchLabelValues
(
key
:
string
)
{
const
url
=
`/api/v1/label/
${
key
}
/values`
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
exisingValues
=
this
.
state
.
labelValues
[
EMPTY_SELECTOR
];
const
values
=
{
...
exisingValues
,
[
key
]:
body
.
data
,
};
const
labelValues
=
{
...
this
.
state
.
labelValues
,
[
EMPTY_SELECTOR
]:
values
,
};
this
.
setState
({
labelValues
});
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchSeriesLabels
(
name
:
string
,
withName
?:
boolean
,
callback
?:
()
=>
void
)
{
const
url
=
`/api/v1/series?match[]=
${
name
}
`
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
{
keys
,
values
}
=
processLabels
(
body
.
data
,
withName
);
const
labelKeys
=
{
...
this
.
state
.
labelKeys
,
[
name
]:
keys
,
};
const
labelValues
=
{
...
this
.
state
.
labelValues
,
[
name
]:
values
,
};
this
.
setState
({
labelKeys
,
labelValues
},
callback
);
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchMetricNames
()
{
const
url
=
'/api/v1/label/__name__/values'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
metrics
=
body
.
data
;
const
metricsByPrefix
=
groupMetricsByPrefix
(
metrics
);
this
.
setState
({
metrics
,
metricsByPrefix
},
this
.
onReceiveMetrics
);
}
catch
(
error
)
{
console
.
error
(
error
);
}
}
render
()
{
const
{
error
,
hint
,
initialQuery
,
supportsLogs
}
=
this
.
props
;
const
{
logLabelOptions
,
metricsOptions
,
syntaxLoaded
}
=
this
.
state
;
const
cleanText
=
this
.
languageProvider
?
this
.
languageProvider
.
cleanText
:
undefined
;
return
(
<
div
className=
"prom-query-field"
>
...
...
public/app/features/explore/QueryField.tsx
View file @
6f2315d5
...
...
@@ -5,6 +5,8 @@ import { Change, Value } from 'slate';
import
{
Editor
}
from
'slate-react'
;
import
Plain
from
'slate-plain-serializer'
;
import
{
CompletionItem
,
CompletionItemGroup
,
TypeaheadOutput
}
from
'app/types/explore'
;
import
ClearPlugin
from
'./slate-plugins/clear'
;
import
NewlinePlugin
from
'./slate-plugins/newline'
;
...
...
@@ -13,87 +15,17 @@ import { makeFragment, makeValue } from './Value';
export
const
TYPEAHEAD_DEBOUNCE
=
100
;
function
getSuggestionByIndex
(
suggestions
:
SuggestionGroup
[],
index
:
number
):
Suggestion
{
function
getSuggestionByIndex
(
suggestions
:
CompletionItemGroup
[],
index
:
number
):
CompletionItem
{
// Flatten suggestion groups
const
flattenedSuggestions
=
suggestions
.
reduce
((
acc
,
g
)
=>
acc
.
concat
(
g
.
items
),
[]);
const
correctedIndex
=
Math
.
max
(
index
,
0
)
%
flattenedSuggestions
.
length
;
return
flattenedSuggestions
[
correctedIndex
];
}
function
hasSuggestions
(
suggestions
:
Suggestion
Group
[]):
boolean
{
function
hasSuggestions
(
suggestions
:
CompletionItem
Group
[]):
boolean
{
return
suggestions
&&
suggestions
.
length
>
0
;
}
export
interface
Suggestion
{
/**
* The label of this completion item. By default
* this is also the text that is inserted when selecting
* this completion.
*/
label
:
string
;
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
kind
?:
string
;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*/
detail
?:
string
;
/**
* A human-readable string, can be Markdown, that represents a doc-comment.
*/
documentation
?:
string
;
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the `label` is used.
*/
sortText
?:
string
;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the `label` is used.
*/
filterText
?:
string
;
/**
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the `label` is used.
*/
insertText
?:
string
;
/**
* Delete number of characters before the caret position,
* by default the letters from the beginning of the word.
*/
deleteBackwards
?:
number
;
/**
* Number of steps to move after the insertion, can be negative.
*/
move
?:
number
;
}
export
interface
SuggestionGroup
{
/**
* Label that will be displayed for all entries of this group.
*/
label
:
string
;
/**
* List of suggestions of this group.
*/
items
:
Suggestion
[];
/**
* If true, match only by prefix (and not mid-word).
*/
prefixMatch
?:
boolean
;
/**
* If true, do not filter items in this group based on the search.
*/
skipFilter
?:
boolean
;
/**
* If true, do not sort items.
*/
skipSort
?:
boolean
;
}
interface
TypeaheadFieldProps
{
additionalPlugins
?:
any
[];
cleanText
?:
(
text
:
string
)
=>
string
;
...
...
@@ -110,7 +42,7 @@ interface TypeaheadFieldProps {
}
export
interface
TypeaheadFieldState
{
suggestions
:
Suggestion
Group
[];
suggestions
:
CompletionItem
Group
[];
typeaheadContext
:
string
|
null
;
typeaheadIndex
:
number
;
typeaheadPrefix
:
string
;
...
...
@@ -127,12 +59,6 @@ export interface TypeaheadInput {
wrapperNode
:
Element
;
}
export
interface
TypeaheadOutput
{
context
?:
string
;
refresher
?:
Promise
<
{}
>
;
suggestions
:
SuggestionGroup
[];
}
class
QueryField
extends
React
.
PureComponent
<
TypeaheadFieldProps
,
TypeaheadFieldState
>
{
menuEl
:
HTMLElement
|
null
;
plugins
:
any
[];
...
...
@@ -293,7 +219,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
}
},
TYPEAHEAD_DEBOUNCE
);
applyTypeahead
(
change
:
Change
,
suggestion
:
Suggestion
):
Change
{
applyTypeahead
(
change
:
Change
,
suggestion
:
CompletionItem
):
Change
{
const
{
cleanText
,
onWillApplySuggestion
,
syntax
}
=
this
.
props
;
const
{
typeaheadPrefix
,
typeaheadText
}
=
this
.
state
;
let
suggestionText
=
suggestion
.
insertText
||
suggestion
.
label
;
...
...
@@ -422,7 +348,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
}
};
onClickMenu
=
(
item
:
Suggestion
)
=>
{
onClickMenu
=
(
item
:
CompletionItem
)
=>
{
// Manually triggering change
const
change
=
this
.
applyTypeahead
(
this
.
state
.
value
.
change
(),
item
);
this
.
onChange
(
change
);
...
...
public/app/features/explore/QueryRows.tsx
View file @
6f2315d5
...
...
@@ -24,8 +24,8 @@ interface QueryRowEventHandlers {
interface
QueryRowCommonProps
{
className
?:
string
;
datasource
:
any
;
history
:
HistoryItem
[];
request
:
(
url
:
string
)
=>
Promise
<
any
>
;
// Temporarily
supportsLogs
?:
boolean
;
transactions
:
QueryTransaction
[];
...
...
@@ -78,7 +78,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
};
render
()
{
const
{
history
,
query
,
request
,
supportsLogs
,
transactions
}
=
this
.
props
;
const
{
datasource
,
history
,
query
,
supportsLogs
,
transactions
}
=
this
.
props
;
const
transactionWithError
=
transactions
.
find
(
t
=>
t
.
error
!==
undefined
);
const
hint
=
getFirstHintFromTransactions
(
transactions
);
const
queryError
=
transactionWithError
?
transactionWithError
.
error
:
null
;
...
...
@@ -89,6 +89,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
</
div
>
<
div
className=
"query-row-field"
>
<
QueryField
datasource=
{
datasource
}
error=
{
queryError
}
hint=
{
hint
}
initialQuery=
{
query
}
...
...
@@ -96,7 +97,6 @@ class QueryRow extends PureComponent<QueryRowProps> {
onClickHintFix=
{
this
.
onClickHintFix
}
onPressEnter=
{
this
.
onPressEnter
}
onQueryChange=
{
this
.
onChangeQuery
}
request=
{
request
}
supportsLogs=
{
supportsLogs
}
/>
</
div
>
...
...
public/app/features/explore/Typeahead.tsx
View file @
6f2315d5
import
React
from
'react'
;
import
Highlighter
from
'react-highlight-words'
;
import
{
Suggestion
,
SuggestionGroup
}
from
'./QueryField
'
;
import
{
CompletionItem
,
CompletionItemGroup
}
from
'app/types/explore
'
;
function
scrollIntoView
(
el
:
HTMLElement
)
{
if
(
!
el
||
!
el
.
offsetParent
)
{
...
...
@@ -15,12 +15,12 @@ function scrollIntoView(el: HTMLElement) {
interface
TypeaheadItemProps
{
isSelected
:
boolean
;
item
:
Suggestion
;
item
:
CompletionItem
;
onClickItem
:
(
Suggestion
)
=>
void
;
prefix
?:
string
;
}
class
TypeaheadItem
extends
React
.
PureComponent
<
TypeaheadItemProps
,
{}
>
{
class
TypeaheadItem
extends
React
.
PureComponent
<
TypeaheadItemProps
>
{
el
:
HTMLElement
;
componentDidUpdate
(
prevProps
)
{
...
...
@@ -53,14 +53,14 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
}
interface
TypeaheadGroupProps
{
items
:
Suggestion
[];
items
:
CompletionItem
[];
label
:
string
;
onClickItem
:
(
Suggestion
)
=>
void
;
selected
:
Suggestion
;
onClickItem
:
(
CompletionItem
)
=>
void
;
selected
:
CompletionItem
;
prefix
?:
string
;
}
class
TypeaheadGroup
extends
React
.
PureComponent
<
TypeaheadGroupProps
,
{}
>
{
class
TypeaheadGroup
extends
React
.
PureComponent
<
TypeaheadGroupProps
>
{
render
()
{
const
{
items
,
label
,
selected
,
onClickItem
,
prefix
}
=
this
.
props
;
return
(
...
...
@@ -85,13 +85,13 @@ class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
}
interface
TypeaheadProps
{
groupedItems
:
Suggestion
Group
[];
groupedItems
:
CompletionItem
Group
[];
menuRef
:
any
;
selectedItem
:
Suggestion
|
null
;
selectedItem
:
CompletionItem
|
null
;
onClickItem
:
(
Suggestion
)
=>
void
;
prefix
?:
string
;
}
class
Typeahead
extends
React
.
PureComponent
<
TypeaheadProps
,
{}
>
{
class
Typeahead
extends
React
.
PureComponent
<
TypeaheadProps
>
{
render
()
{
const
{
groupedItems
,
menuRef
,
selectedItem
,
onClickItem
,
prefix
}
=
this
.
props
;
return
(
...
...
public/app/plugins/datasource/prometheus/datasource.ts
View file @
6f2315d5
...
...
@@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn';
import
*
as
dateMath
from
'app/core/utils/datemath'
;
import
PrometheusMetricFindQuery
from
'./metric_find_query'
;
import
{
ResultTransformer
}
from
'./result_transformer'
;
import
PrometheusLanguageProvider
from
'./language_provider'
;
import
{
BackendSrv
}
from
'app/core/services/backend_srv'
;
import
addLabelToQuery
from
'./add_label_to_query'
;
...
...
@@ -60,6 +61,7 @@ export class PrometheusDatasource {
interval: string;
queryTimeout: string;
httpMethod: string;
languageProvider: PrometheusLanguageProvider;
resultTransformer: ResultTransformer;
/** @ngInject */
...
...
@@ -76,6 +78,7 @@ export class PrometheusDatasource {
this.httpMethod = instanceSettings.jsonData.httpMethod || '
GET
';
this.resultTransformer = new ResultTransformer(templateSrv);
this.ruleMappings = {};
this.languageProvider = new PrometheusLanguageProvider(this);
}
init() {
...
...
public/app/plugins/datasource/prometheus/language_provider.ts
0 → 100644
View file @
6f2315d5
import
_
from
'lodash'
;
import
moment
from
'moment'
;
import
{
CompletionItem
,
CompletionItemGroup
,
LanguageProvider
,
TypeaheadInput
,
TypeaheadOutput
,
}
from
'app/types/explore'
;
import
{
parseSelector
,
processLabels
,
RATE_RANGES
}
from
'./language_utils'
;
import
PromqlSyntax
,
{
FUNCTIONS
}
from
'./promql'
;
const
DEFAULT_KEYS
=
[
'job'
,
'instance'
];
const
EMPTY_SELECTOR
=
'{}'
;
const
HISTOGRAM_SELECTOR
=
'{le!=""}'
;
// Returns all timeseries for histograms
const
HISTORY_ITEM_COUNT
=
5
;
const
HISTORY_COUNT_CUTOFF
=
1000
*
60
*
60
*
24
;
// 24h
const
wrapLabel
=
(
label
:
string
)
=>
({
label
});
const
setFunctionMove
=
(
suggestion
:
CompletionItem
):
CompletionItem
=>
{
suggestion
.
move
=
-
1
;
return
suggestion
;
};
export
function
addHistoryMetadata
(
item
:
CompletionItem
,
history
:
any
[]):
CompletionItem
{
const
cutoffTs
=
Date
.
now
()
-
HISTORY_COUNT_CUTOFF
;
const
historyForItem
=
history
.
filter
(
h
=>
h
.
ts
>
cutoffTs
&&
h
.
query
===
item
.
label
);
const
count
=
historyForItem
.
length
;
const
recent
=
historyForItem
[
0
];
let
hint
=
`Queried
${
count
}
times in the last 24h.`
;
if
(
recent
)
{
const
lastQueried
=
moment
(
recent
.
ts
).
fromNow
();
hint
=
`
${
hint
}
Last queried
${
lastQueried
}
.`
;
}
return
{
...
item
,
documentation
:
hint
,
};
}
export
default
class
PromQlLanguageProvider
extends
LanguageProvider
{
histogramMetrics
?:
string
[];
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
metrics
?:
string
[];
logLabelOptions
:
any
[];
supportsLogs
?:
boolean
;
started
:
boolean
;
constructor
(
datasource
:
any
,
initialValues
?:
any
)
{
super
();
this
.
datasource
=
datasource
;
this
.
histogramMetrics
=
[];
this
.
labelKeys
=
{};
this
.
labelValues
=
{};
this
.
metrics
=
[];
this
.
supportsLogs
=
false
;
this
.
started
=
false
;
Object
.
assign
(
this
,
initialValues
);
}
// Strip syntax chars
cleanText
=
s
=>
s
.
replace
(
/
[
{}[
\]
="(),!~+
\-
*
/
^%
]
/g
,
''
).
trim
();
getSyntax
()
{
return
PromqlSyntax
;
}
request
=
url
=>
{
return
this
.
datasource
.
metadataRequest
(
url
);
};
start
=
()
=>
{
if
(
!
this
.
started
)
{
this
.
started
=
true
;
return
Promise
.
all
([
this
.
fetchMetricNames
(),
this
.
fetchHistogramMetrics
()]);
}
return
Promise
.
resolve
([]);
};
// Keep this DOM-free for testing
provideCompletionItems
({
prefix
,
wrapperClasses
,
text
}:
TypeaheadInput
,
context
?:
any
):
TypeaheadOutput
{
// Syntax spans have 3 classes by default. More indicate a recognized token
const
tokenRecognized
=
wrapperClasses
.
length
>
3
;
// Determine candidates by CSS context
if
(
_
.
includes
(
wrapperClasses
,
'context-range'
))
{
// Suggestions for metric[|]
return
this
.
getRangeCompletionItems
();
}
else
if
(
_
.
includes
(
wrapperClasses
,
'context-labels'
))
{
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
return
this
.
getLabelCompletionItems
.
apply
(
this
,
arguments
);
}
else
if
(
_
.
includes
(
wrapperClasses
,
'context-aggregation'
))
{
return
this
.
getAggregationCompletionItems
.
apply
(
this
,
arguments
);
}
else
if
(
// Show default suggestions in a couple of scenarios
(
prefix
&&
!
tokenRecognized
)
||
// Non-empty prefix, but not inside known token
(
prefix
===
''
&&
!
text
.
match
(
/^
[\]
})
\s]
+$/
))
||
// Empty prefix, but not following a closing brace
text
.
match
(
/
[
+
\-
*
/
^%
]
/
)
// Anything after binary operator
)
{
return
this
.
getEmptyCompletionItems
(
context
||
{});
}
return
{
suggestions
:
[],
};
}
getEmptyCompletionItems
(
context
:
any
):
TypeaheadOutput
{
const
{
history
}
=
context
;
const
{
metrics
}
=
this
;
const
suggestions
:
CompletionItemGroup
[]
=
[];
if
(
history
&&
history
.
length
>
0
)
{
const
historyItems
=
_
.
chain
(
history
)
.
uniqBy
(
'query'
)
.
take
(
HISTORY_ITEM_COUNT
)
.
map
(
h
=>
h
.
query
)
.
map
(
wrapLabel
)
.
map
(
item
=>
addHistoryMetadata
(
item
,
history
))
.
value
();
suggestions
.
push
({
prefixMatch
:
true
,
skipSort
:
true
,
label
:
'History'
,
items
:
historyItems
,
});
}
suggestions
.
push
({
prefixMatch
:
true
,
label
:
'Functions'
,
items
:
FUNCTIONS
.
map
(
setFunctionMove
),
});
if
(
metrics
)
{
suggestions
.
push
({
label
:
'Metrics'
,
items
:
metrics
.
map
(
wrapLabel
),
});
}
return
{
suggestions
};
}
getRangeCompletionItems
():
TypeaheadOutput
{
return
{
context
:
'context-range'
,
suggestions
:
[
{
label
:
'Range vector'
,
items
:
[...
RATE_RANGES
].
map
(
wrapLabel
),
},
],
};
}
getAggregationCompletionItems
({
value
}:
TypeaheadInput
):
TypeaheadOutput
{
let
refresher
:
Promise
<
any
>
=
null
;
const
suggestions
:
CompletionItemGroup
[]
=
[];
// sum(foo{bar="1"}) by (|)
const
line
=
value
.
anchorBlock
.
getText
();
const
cursorOffset
:
number
=
value
.
anchorOffset
;
// sum(foo{bar="1"}) by (
const
leftSide
=
line
.
slice
(
0
,
cursorOffset
);
const
openParensAggregationIndex
=
leftSide
.
lastIndexOf
(
'('
);
const
openParensSelectorIndex
=
leftSide
.
slice
(
0
,
openParensAggregationIndex
).
lastIndexOf
(
'('
);
const
closeParensSelectorIndex
=
leftSide
.
slice
(
openParensSelectorIndex
).
indexOf
(
')'
)
+
openParensSelectorIndex
;
// foo{bar="1"}
const
selectorString
=
leftSide
.
slice
(
openParensSelectorIndex
+
1
,
closeParensSelectorIndex
);
const
selector
=
parseSelector
(
selectorString
,
selectorString
.
length
-
2
).
selector
;
const
labelKeys
=
this
.
labelKeys
[
selector
];
if
(
labelKeys
)
{
suggestions
.
push
({
label
:
'Labels'
,
items
:
labelKeys
.
map
(
wrapLabel
)
});
}
else
{
refresher
=
this
.
fetchSeriesLabels
(
selector
);
}
return
{
refresher
,
suggestions
,
context
:
'context-aggregation'
,
};
}
getLabelCompletionItems
({
text
,
wrapperClasses
,
labelKey
,
value
}:
TypeaheadInput
):
TypeaheadOutput
{
let
context
:
string
;
let
refresher
:
Promise
<
any
>
=
null
;
const
suggestions
:
CompletionItemGroup
[]
=
[];
const
line
=
value
.
anchorBlock
.
getText
();
const
cursorOffset
:
number
=
value
.
anchorOffset
;
// Get normalized selector
let
selector
;
let
parsedSelector
;
try
{
parsedSelector
=
parseSelector
(
line
,
cursorOffset
);
selector
=
parsedSelector
.
selector
;
}
catch
{
selector
=
EMPTY_SELECTOR
;
}
const
containsMetric
=
selector
.
indexOf
(
'__name__='
)
>
-
1
;
const
existingKeys
=
parsedSelector
?
parsedSelector
.
labelKeys
:
[];
if
((
text
&&
text
.
match
(
/^!
?
=~
?
/
))
||
_
.
includes
(
wrapperClasses
,
'attr-value'
))
{
// Label values
if
(
labelKey
&&
this
.
labelValues
[
selector
]
&&
this
.
labelValues
[
selector
][
labelKey
])
{
const
labelValues
=
this
.
labelValues
[
selector
][
labelKey
];
context
=
'context-label-values'
;
suggestions
.
push
({
label
:
`Label values for "
${
labelKey
}
"`
,
items
:
labelValues
.
map
(
wrapLabel
),
});
}
}
else
{
// Label keys
const
labelKeys
=
this
.
labelKeys
[
selector
]
||
(
containsMetric
?
null
:
DEFAULT_KEYS
);
if
(
labelKeys
)
{
const
possibleKeys
=
_
.
difference
(
labelKeys
,
existingKeys
);
if
(
possibleKeys
.
length
>
0
)
{
context
=
'context-labels'
;
suggestions
.
push
({
label
:
`Labels`
,
items
:
possibleKeys
.
map
(
wrapLabel
)
});
}
}
}
// Query labels for selector
// Temporarily add skip for logging
if
(
selector
&&
!
this
.
labelValues
[
selector
]
&&
!
this
.
supportsLogs
)
{
if
(
selector
===
EMPTY_SELECTOR
)
{
// Query label values for default labels
refresher
=
Promise
.
all
(
DEFAULT_KEYS
.
map
(
key
=>
this
.
fetchLabelValues
(
key
)));
}
else
{
refresher
=
this
.
fetchSeriesLabels
(
selector
,
!
containsMetric
);
}
}
return
{
context
,
refresher
,
suggestions
};
}
async
fetchMetricNames
()
{
const
url
=
'/api/v1/label/__name__/values'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
this
.
metrics
=
body
.
data
;
}
catch
(
error
)
{
console
.
error
(
error
);
}
}
async
fetchHistogramMetrics
()
{
await
this
.
fetchSeriesLabels
(
HISTOGRAM_SELECTOR
,
true
);
const
histogramSeries
=
this
.
labelValues
[
HISTOGRAM_SELECTOR
];
if
(
histogramSeries
&&
histogramSeries
[
'__name__'
])
{
this
.
histogramMetrics
=
histogramSeries
[
'__name__'
].
slice
().
sort
();
}
}
// Temporarily here while reusing this field for logging
async
fetchLogLabels
()
{
const
url
=
'/api/prom/label'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
labelKeys
=
body
.
data
.
slice
().
sort
();
const
labelKeysBySelector
=
{
...
this
.
labelKeys
,
[
EMPTY_SELECTOR
]:
labelKeys
,
};
const
labelValuesByKey
=
{};
this
.
logLabelOptions
=
[];
for
(
const
key
of
labelKeys
)
{
const
valuesUrl
=
`/api/prom/label/
${
key
}
/values`
;
const
res
=
await
this
.
request
(
valuesUrl
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
values
=
body
.
data
.
slice
().
sort
();
labelValuesByKey
[
key
]
=
values
;
this
.
logLabelOptions
.
push
({
label
:
key
,
value
:
key
,
children
:
values
.
map
(
value
=>
({
label
:
value
,
value
})),
});
}
this
.
labelValues
=
{
[
EMPTY_SELECTOR
]:
labelValuesByKey
};
this
.
labelKeys
=
labelKeysBySelector
;
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchLabelValues
(
key
:
string
)
{
const
url
=
`/api/v1/label/
${
key
}
/values`
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
exisingValues
=
this
.
labelValues
[
EMPTY_SELECTOR
];
const
values
=
{
...
exisingValues
,
[
key
]:
body
.
data
,
};
this
.
labelValues
=
{
...
this
.
labelValues
,
[
EMPTY_SELECTOR
]:
values
,
};
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchSeriesLabels
(
name
:
string
,
withName
?:
boolean
)
{
const
url
=
`/api/v1/series?match[]=
${
name
}
`
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
{
keys
,
values
}
=
processLabels
(
body
.
data
,
withName
);
this
.
labelKeys
=
{
...
this
.
labelKeys
,
[
name
]:
keys
,
};
this
.
labelValues
=
{
...
this
.
labelValues
,
[
name
]:
values
,
};
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
}
public/app/
features/explore/utils/prometheu
s.ts
→
public/app/
plugins/datasource/prometheus/language_util
s.ts
View file @
6f2315d5
...
...
@@ -23,9 +23,6 @@ export function processLabels(labels, withName = false) {
return
{
values
,
keys
:
Object
.
keys
(
values
)
};
}
// Strip syntax chars
export
const
cleanText
=
s
=>
s
.
replace
(
/
[
{}[
\]
="(),!~+
\-
*
/
^%
]
/g
,
''
).
trim
();
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
const
selectorRegexp
=
/
\{[^
}
]
*
?\}
/
;
const
labelRegexp
=
/
\b(\w
+
)(
!
?
=~
?)(
"
[^
"
\n]
*
?
"
)
/g
;
...
...
public/app/
features/explore/slate-plugins/prism
/promql.ts
→
public/app/
plugins/datasource/prometheus
/promql.ts
View file @
6f2315d5
File moved
public/app/plugins/datasource/prometheus/specs/language_provider.test.ts
0 → 100644
View file @
6f2315d5
import
Plain
from
'slate-plain-serializer'
;
import
LanguageProvider
from
'../language_provider'
;
describe
(
'Language completion provider'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
}
}),
};
it
(
'returns default suggestions on emtpty context'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
);
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
describe
(
'range suggestions'
,
()
=>
{
it
(
'returns range suggestions in range context'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
);
const
result
=
instance
.
provideCompletionItems
({
text
:
'1'
,
prefix
:
'1'
,
wrapperClasses
:
[
'context-range'
]
});
expect
(
result
.
context
).
toBe
(
'context-range'
);
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
).
toEqual
([
{
items
:
[{
label
:
'1m'
},
{
label
:
'5m'
},
{
label
:
'10m'
},
{
label
:
'30m'
},
{
label
:
'1h'
}],
label
:
'Range vector'
,
},
]);
});
});
describe
(
'metric suggestions'
,
()
=>
{
it
(
'returns metrics suggestions by default'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
metrics
:
[
'foo'
,
'bar'
]
});
const
result
=
instance
.
provideCompletionItems
({
text
:
'a'
,
prefix
:
'a'
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
it
(
'returns default suggestions after a binary operator'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
metrics
:
[
'foo'
,
'bar'
]
});
const
result
=
instance
.
provideCompletionItems
({
text
:
'*'
,
prefix
:
''
,
wrapperClasses
:
[]
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
2
);
});
});
describe
(
'label suggestions'
,
()
=>
{
it
(
'returns default label suggestions on label context and no metric'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
);
const
value
=
Plain
.
deserialize
(
'{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
1
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'job'
},
{
label
:
'instance'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on label context and metric'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{__name__="metric"}'
:
[
'bar'
]
}
});
const
value
=
Plain
.
deserialize
(
'metric{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
7
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on label context but leaves out labels that already exist'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{job1="foo",job2!="foo",job3=~"foo"}'
:
[
'bar'
,
'job1'
,
'job2'
,
'job3'
]
},
});
const
value
=
Plain
.
deserialize
(
'{job1="foo",job2!="foo",job3=~"foo",}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
36
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label value suggestions inside a label value context after a negated matching operator'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{}'
:
[
'label'
]
},
labelValues
:
{
'{}'
:
{
label
:
[
'a'
,
'b'
,
'c'
]
}
},
});
const
value
=
Plain
.
deserialize
(
'{label!=}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
8
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
'!='
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
labelKey
:
'label'
,
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-label-values'
);
expect
(
result
.
suggestions
).
toEqual
([
{
items
:
[{
label
:
'a'
},
{
label
:
'b'
},
{
label
:
'c'
}],
label
:
'Label values for "label"'
,
},
]);
});
it
(
'returns a refresher on label context and unavailable metric'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{__name__="foo"}'
:
[
'bar'
]
}
});
const
value
=
Plain
.
deserialize
(
'metric{}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
7
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-labels'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeInstanceOf
(
Promise
);
expect
(
result
.
suggestions
).
toEqual
([]);
});
it
(
'returns label values on label context when given a metric and a label key'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{__name__="metric"}'
:
[
'bar'
]
},
labelValues
:
{
'{__name__="metric"}'
:
{
bar
:
[
'baz'
]
}
},
});
const
value
=
Plain
.
deserialize
(
'metric{bar=ba}'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
13
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
'=ba'
,
prefix
:
'ba'
,
wrapperClasses
:
[
'context-labels'
],
labelKey
:
'bar'
,
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-label-values'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'baz'
}],
label
:
'Label values for "bar"'
}]);
});
it
(
'returns label suggestions on aggregation context and metric w/ selector'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{__name__="metric",foo="xx"}'
:
[
'bar'
]
}
});
const
value
=
Plain
.
deserialize
(
'sum(metric{foo="xx"}) by ()'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
26
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-aggregation'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-aggregation'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions on aggregation context and metric w/o selector'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
,
{
labelKeys
:
{
'{__name__="metric"}'
:
[
'bar'
]
}
});
const
value
=
Plain
.
deserialize
(
'sum(metric) by ()'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
16
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
result
=
instance
.
provideCompletionItems
({
text
:
''
,
prefix
:
''
,
wrapperClasses
:
[
'context-aggregation'
],
value
:
valueWithSelection
,
});
expect
(
result
.
context
).
toBe
(
'context-aggregation'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'bar'
}],
label
:
'Labels'
}]);
});
});
});
public/app/
features/explore/utils/prometheu
s.test.ts
→
public/app/
plugins/datasource/prometheus/specs/language_util
s.test.ts
View file @
6f2315d5
import
{
parseSelector
}
from
'.
/prometheu
s'
;
import
{
parseSelector
}
from
'.
./language_util
s'
;
describe
(
'parseSelector()'
,
()
=>
{
let
parsed
;
...
...
public/app/types/explore.ts
View file @
6f2315d5
import
{
Value
}
from
'slate'
;
export
interface
CompletionItem
{
/**
* The label of this completion item. By default
* this is also the text that is inserted when selecting
* this completion.
*/
label
:
string
;
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
kind
?:
string
;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*/
detail
?:
string
;
/**
* A human-readable string, can be Markdown, that represents a doc-comment.
*/
documentation
?:
string
;
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the `label` is used.
*/
sortText
?:
string
;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the `label` is used.
*/
filterText
?:
string
;
/**
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the `label` is used.
*/
insertText
?:
string
;
/**
* Delete number of characters before the caret position,
* by default the letters from the beginning of the word.
*/
deleteBackwards
?:
number
;
/**
* Number of steps to move after the insertion, can be negative.
*/
move
?:
number
;
}
export
interface
CompletionItemGroup
{
/**
* Label that will be displayed for all entries of this group.
*/
label
:
string
;
/**
* List of suggestions of this group.
*/
items
:
CompletionItem
[];
/**
* If true, match only by prefix (and not mid-word).
*/
prefixMatch
?:
boolean
;
/**
* If true, do not filter items in this group based on the search.
*/
skipFilter
?:
boolean
;
/**
* If true, do not sort items.
*/
skipSort
?:
boolean
;
}
interface
ExploreDatasource
{
value
:
string
;
label
:
string
;
...
...
@@ -8,6 +80,26 @@ export interface HistoryItem {
query
:
string
;
}
export
abstract
class
LanguageProvider
{
datasource
:
any
;
request
:
(
url
)
=>
Promise
<
any
>
;
start
:
()
=>
Promise
<
any
>
;
}
export
interface
TypeaheadInput
{
text
:
string
;
prefix
:
string
;
wrapperClasses
:
string
[];
labelKey
?:
string
;
value
?:
Value
;
}
export
interface
TypeaheadOutput
{
context
?:
string
;
refresher
?:
Promise
<
{}
>
;
suggestions
:
CompletionItemGroup
[];
}
export
interface
Range
{
from
:
string
;
to
:
string
;
...
...
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