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
bc4ba64a
Unverified
Commit
bc4ba64a
authored
Sep 12, 2019
by
Andrej Ocenas
Committed by
GitHub
Sep 12, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Explore: Fix auto completion on label values for Loki (#18988)
parent
0607189e
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
139 additions
and
87 deletions
+139
-87
public/app/plugins/datasource/loki/components/useLokiLabels.test.ts
+4
-9
public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts
+2
-3
public/app/plugins/datasource/loki/language_provider.test.ts
+80
-70
public/app/plugins/datasource/loki/language_provider.ts
+26
-5
public/app/plugins/datasource/loki/mocks.ts
+27
-0
No files found.
public/app/plugins/datasource/loki/components/useLokiLabels.test.ts
View file @
bc4ba64a
...
...
@@ -3,12 +3,11 @@ import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
import
{
useLokiLabels
}
from
'./useLokiLabels'
;
import
{
DataSourceStatus
}
from
'@grafana/ui/src/types/datasource'
;
import
{
AbsoluteTimeRange
}
from
'@grafana/data'
;
import
{
makeMockLokiDatasource
}
from
'../mocks'
;
describe
(
'useLokiLabels hook'
,
()
=>
{
it
(
'should refresh labels'
,
async
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
any
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
languageProvider
=
new
LanguageProvider
(
datasource
);
const
logLabelOptionsMock
=
[
'Holy mock!'
];
const
rangeMock
:
AbsoluteTimeRange
=
{
...
...
@@ -31,9 +30,7 @@ describe('useLokiLabels hook', () => {
});
it
(
'should force refresh labels after a disconnect'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
any
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
rangeMock
:
AbsoluteTimeRange
=
{
from
:
1560153109000
,
...
...
@@ -52,9 +49,7 @@ describe('useLokiLabels hook', () => {
});
it
(
'should not force refresh labels after a connect'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
any
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
rangeMock
:
AbsoluteTimeRange
=
{
from
:
1560153109000
,
...
...
public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts
View file @
bc4ba64a
...
...
@@ -5,11 +5,10 @@ import { AbsoluteTimeRange } from '@grafana/data';
import
LanguageProvider
from
'app/plugins/datasource/loki/language_provider'
;
import
{
useLokiSyntax
}
from
'./useLokiSyntax'
;
import
{
CascaderOption
}
from
'app/plugins/datasource/loki/components/LokiQueryFieldForm'
;
import
{
makeMockLokiDatasource
}
from
'../mocks'
;
describe
(
'useLokiSyntax hook'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
any
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
languageProvider
=
new
LanguageProvider
(
datasource
);
const
logLabelOptionsMock
=
[
'Holy mock!'
];
const
logLabelOptionsMock2
=
[
'Mock the hell?!'
];
...
...
public/app/plugins/datasource/loki/language_provider.test.ts
View file @
bc4ba64a
// @ts-ignore
import
Plain
from
'slate-plain-serializer'
;
import
LanguageProvider
,
{
LABEL_REFRESH_INTERVAL
,
rangeToParams
}
from
'./language_provider'
;
import
LanguageProvider
,
{
LABEL_REFRESH_INTERVAL
,
LokiHistoryItem
,
rangeToParams
}
from
'./language_provider'
;
import
{
AbsoluteTimeRange
}
from
'@grafana/data'
;
import
{
advanceTo
,
clear
,
advanceBy
}
from
'jest-date-mock'
;
import
{
beforeEach
}
from
'test/lib/common'
;
import
{
DataQueryResponseData
}
from
'@grafana/ui'
;
import
{
DataSourceApi
}
from
'@grafana/ui'
;
import
{
TypeaheadInput
}
from
'../../../types'
;
import
{
makeMockLokiDatasource
}
from
'./mocks'
;
describe
(
'Language completion provider'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
rangeMock
:
AbsoluteTimeRange
=
{
from
:
1560153109000
,
...
...
@@ -30,9 +30,10 @@ describe('Language completion provider', () => {
it
(
'returns default suggestions with history on empty context when history was provided'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
);
const
value
=
Plain
.
deserialize
(
''
);
const
history
=
[
const
history
:
LokiHistoryItem
[]
=
[
{
query
:
{
refId
:
'1'
,
expr
:
'{app="foo"}'
},
ts
:
1
,
},
];
const
result
=
instance
.
provideCompletionItems
(
...
...
@@ -55,25 +56,14 @@ describe('Language completion provider', () => {
it
(
'returns no suggestions within regexp'
,
()
=>
{
const
instance
=
new
LanguageProvider
(
datasource
);
const
value
=
Plain
.
deserialize
(
'{} ()'
);
const
range
=
value
.
selection
.
merge
({
anchorOffset
:
4
,
});
const
valueWithSelection
=
value
.
change
().
select
(
range
).
value
;
const
history
=
[
const
input
=
createTypeaheadInput
(
'{} ()'
,
''
,
undefined
,
4
,
[]);
const
history
:
LokiHistoryItem
[]
=
[
{
query
:
{
refId
:
'1'
,
expr
:
'{app="foo"}'
},
ts
:
1
,
},
];
const
result
=
instance
.
provideCompletionItems
(
{
text
:
''
,
prefix
:
''
,
value
:
valueWithSelection
,
wrapperClasses
:
[],
},
{
history
}
);
const
result
=
instance
.
provideCompletionItems
(
input
,
{
history
});
expect
(
result
.
context
).
toBeUndefined
();
expect
(
result
.
refresher
).
toBeUndefined
();
expect
(
result
.
suggestions
.
length
).
toEqual
(
0
);
...
...
@@ -83,23 +73,35 @@ describe('Language completion provider', () => {
describe
(
'label suggestions'
,
()
=>
{
it
(
'returns default label suggestions on label context'
,
()
=>
{
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
,
},
{
absoluteRange
:
rangeMock
}
);
const
input
=
createTypeaheadInput
(
'{}'
,
''
);
const
result
=
instance
.
provideCompletionItems
(
input
,
{
absoluteRange
:
rangeMock
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'job'
},
{
label
:
'namespace'
}],
label
:
'Labels'
}]);
});
it
(
'returns label suggestions from Loki'
,
async
()
=>
{
const
datasource
=
makeMockLokiDatasource
({
label1
:
[],
label2
:
[]
});
const
provider
=
await
getLanguageProvider
(
datasource
);
const
input
=
createTypeaheadInput
(
'{}'
,
''
);
const
result
=
provider
.
provideCompletionItems
(
input
,
{
absoluteRange
:
rangeMock
});
expect
(
result
.
context
).
toBe
(
'context-labels'
);
expect
(
result
.
suggestions
).
toEqual
([{
items
:
[{
label
:
'label1'
},
{
label
:
'label2'
}],
label
:
'Labels'
}]);
});
it
(
'returns label values suggestions from Loki'
,
async
()
=>
{
const
datasource
=
makeMockLokiDatasource
({
label1
:
[
'label1_val1'
,
'label1_val2'
],
label2
:
[]
});
const
provider
=
await
getLanguageProvider
(
datasource
);
const
input
=
createTypeaheadInput
(
'{label1=}'
,
'='
,
'label1'
);
let
result
=
provider
.
provideCompletionItems
(
input
,
{
absoluteRange
:
rangeMock
});
// The values for label are loaded adhoc and there is a promise returned that we have to wait for
expect
(
result
.
refresher
).
toBeDefined
();
await
result
.
refresher
;
result
=
provider
.
provideCompletionItems
(
input
,
{
absoluteRange
:
rangeMock
});
expect
(
result
.
context
).
toBe
(
'context-label-values'
);
expect
(
result
.
suggestions
).
toEqual
([
{
items
:
[{
label
:
'label1_val1'
},
{
label
:
'label1_val2'
}],
label
:
'Label values for "label1"'
},
]);
});
});
});
...
...
@@ -110,17 +112,8 @@ describe('Request URL', () => {
to
:
1560163909000
,
};
const
datasourceWithLabels
=
{
metadataRequest
:
(
url
:
string
)
=>
{
if
(
url
.
slice
(
0
,
15
)
===
'/api/prom/label'
)
{
return
{
data
:
{
data
:
[
'other'
]
}
};
}
else
{
return
{
data
:
{
data
:
[]
}
};
}
},
};
const
datasourceSpy
=
jest
.
spyOn
(
datasourceWithLabels
,
'metadataRequest'
);
const
datasourceWithLabels
=
makeMockLokiDatasource
({
other
:
[]
});
const
datasourceSpy
=
jest
.
spyOn
(
datasourceWithLabels
as
any
,
'metadataRequest'
);
const
instance
=
new
LanguageProvider
(
datasourceWithLabels
,
{
initialRange
:
rangeMock
});
await
instance
.
refreshLogLabels
(
rangeMock
,
true
);
...
...
@@ -130,9 +123,7 @@ describe('Request URL', () => {
});
describe
(
'Query imports'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
rangeMock
:
AbsoluteTimeRange
=
{
from
:
1560153109000
,
...
...
@@ -153,36 +144,21 @@ describe('Query imports', () => {
});
it
(
'returns empty query from selector query if label is not available'
,
async
()
=>
{
const
datasourceWithLabels
=
{
metadataRequest
:
(
url
:
string
)
=>
url
.
slice
(
0
,
15
)
===
'/api/prom/label'
?
{
data
:
{
data
:
[
'other'
]
}
}
:
{
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
},
};
const
datasourceWithLabels
=
makeMockLokiDatasource
({
other
:
[]
});
const
instance
=
new
LanguageProvider
(
datasourceWithLabels
,
{
initialRange
:
rangeMock
});
const
result
=
await
instance
.
importPrometheusQuery
(
'{foo="bar"}'
);
expect
(
result
).
toEqual
(
'{}'
);
});
it
(
'returns selector query from selector query with common labels'
,
async
()
=>
{
const
datasourceWithLabels
=
{
metadataRequest
:
(
url
:
string
)
=>
url
.
slice
(
0
,
15
)
===
'/api/prom/label'
?
{
data
:
{
data
:
[
'foo'
]
}
}
:
{
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
},
};
const
datasourceWithLabels
=
makeMockLokiDatasource
({
foo
:
[]
});
const
instance
=
new
LanguageProvider
(
datasourceWithLabels
,
{
initialRange
:
rangeMock
});
const
result
=
await
instance
.
importPrometheusQuery
(
'metric{foo="bar",baz="42"}'
);
expect
(
result
).
toEqual
(
'{foo="bar"}'
);
});
it
(
'returns selector query from selector query with all labels if logging label list is empty'
,
async
()
=>
{
const
datasourceWithLabels
=
{
metadataRequest
:
(
url
:
string
)
=>
url
.
slice
(
0
,
15
)
===
'/api/prom/label'
?
{
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
}
:
{
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
},
};
const
datasourceWithLabels
=
makeMockLokiDatasource
({});
const
instance
=
new
LanguageProvider
(
datasourceWithLabels
,
{
initialRange
:
rangeMock
});
const
result
=
await
instance
.
importPrometheusQuery
(
'metric{foo="bar",baz="42"}'
);
expect
(
result
).
toEqual
(
'{baz="42",foo="bar"}'
);
...
...
@@ -191,9 +167,7 @@ describe('Query imports', () => {
});
describe
(
'Labels refresh'
,
()
=>
{
const
datasource
=
{
metadataRequest
:
()
=>
({
data
:
{
data
:
[]
as
DataQueryResponseData
[]
}
}),
};
const
datasource
=
makeMockLokiDatasource
({});
const
instance
=
new
LanguageProvider
(
datasource
);
const
rangeMock
:
AbsoluteTimeRange
=
{
...
...
@@ -226,3 +200,39 @@ describe('Labels refresh', () => {
expect
(
instance
.
fetchLogLabels
).
toBeCalled
();
});
});
async
function
getLanguageProvider
(
datasource
:
DataSourceApi
)
{
const
instance
=
new
LanguageProvider
(
datasource
);
instance
.
initialRange
=
{
from
:
Date
.
now
()
-
10000
,
to
:
Date
.
now
(),
};
await
instance
.
start
();
return
instance
;
}
/**
* @param value Value of the full input
* @param text Last piece of text (not sure but in case of {label=} this would be just '=')
* @param labelKey Label by which to search for values. Cutting corners a bit here as this should be inferred from value
*/
function
createTypeaheadInput
(
value
:
string
,
text
:
string
,
labelKey
?:
string
,
anchorOffset
?:
number
,
wrapperClasses
?:
string
[]
):
TypeaheadInput
{
const
deserialized
=
Plain
.
deserialize
(
value
);
const
range
=
deserialized
.
selection
.
merge
({
anchorOffset
:
anchorOffset
||
1
,
});
const
valueWithSelection
=
deserialized
.
change
().
select
(
range
).
value
;
return
{
text
,
prefix
:
''
,
wrapperClasses
:
wrapperClasses
||
[
'context-labels'
],
value
:
valueWithSelection
,
labelKey
,
};
}
public/app/plugins/datasource/loki/language_provider.ts
View file @
bc4ba64a
...
...
@@ -17,6 +17,7 @@ import {
import
{
LokiQuery
}
from
'./types'
;
import
{
dateTime
,
AbsoluteTimeRange
}
from
'@grafana/data'
;
import
{
PromQuery
}
from
'../prometheus/types'
;
import
{
DataSourceApi
}
from
'@grafana/ui'
;
const
DEFAULT_KEYS
=
[
'job'
,
'namespace'
];
const
EMPTY_SELECTOR
=
'{}'
;
...
...
@@ -28,7 +29,12 @@ export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
const
wrapLabel
=
(
label
:
string
)
=>
({
label
});
export
const
rangeToParams
=
(
range
:
AbsoluteTimeRange
)
=>
({
start
:
range
.
from
*
NS_IN_MS
,
end
:
range
.
to
*
NS_IN_MS
});
type
LokiHistoryItem
=
HistoryItem
<
LokiQuery
>
;
export
type
LokiHistoryItem
=
HistoryItem
<
LokiQuery
>
;
type
TypeaheadContext
=
{
history
?:
LokiHistoryItem
[];
absoluteRange
?:
AbsoluteTimeRange
;
};
export
function
addHistoryMetadata
(
item
:
CompletionItem
,
history
:
LokiHistoryItem
[]):
CompletionItem
{
const
cutoffTs
=
Date
.
now
()
-
HISTORY_COUNT_CUTOFF
;
...
...
@@ -54,7 +60,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
started
:
boolean
;
initialRange
:
AbsoluteTimeRange
;
constructor
(
datasource
:
any
,
initialValues
?:
any
)
{
constructor
(
datasource
:
DataSourceApi
,
initialValues
?:
any
)
{
super
();
this
.
datasource
=
datasource
;
...
...
@@ -74,6 +80,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
return
this
.
datasource
.
metadataRequest
(
url
,
params
);
};
/**
* Initialise the language provider by fetching set of labels. Without this initialisation the provider would return
* just a set of hardcoded default labels on provideCompletionItems or a recent queries from history.
*/
start
=
()
=>
{
if
(
!
this
.
startTask
)
{
this
.
startTask
=
this
.
fetchLogLabels
(
this
.
initialRange
);
...
...
@@ -81,14 +91,22 @@ export default class LokiLanguageProvider extends LanguageProvider {
return
this
.
startTask
;
};
// Keep this DOM-free for testing
provideCompletionItems
({
prefix
,
wrapperClasses
,
text
,
value
}:
TypeaheadInput
,
context
?:
any
):
TypeaheadOutput
{
/**
* Return suggestions based on input that can be then plugged into a typeahead dropdown.
* Keep this DOM-free for testing
* @param input
* @param context Is optional in types but is required in case we are doing getLabelCompletionItems
* @param context.absoluteRange Required in case we are doing getLabelCompletionItems
* @param context.history Optional used only in getEmptyCompletionItems
*/
provideCompletionItems
(
input
:
TypeaheadInput
,
context
?:
TypeaheadContext
):
TypeaheadOutput
{
const
{
wrapperClasses
,
value
}
=
input
;
// Local text properties
const
empty
=
value
.
document
.
text
.
length
===
0
;
// Determine candidates by CSS context
if
(
_
.
includes
(
wrapperClasses
,
'context-labels'
))
{
// Suggestions for {|} and {foo=|}
return
this
.
getLabelCompletionItems
.
apply
(
this
,
arguments
);
return
this
.
getLabelCompletionItems
(
input
,
context
);
}
else
if
(
empty
)
{
return
this
.
getEmptyCompletionItems
(
context
||
{});
}
...
...
@@ -245,6 +263,9 @@ export default class LokiLanguageProvider extends LanguageProvider {
...
this
.
labelKeys
,
[
EMPTY_SELECTOR
]:
labelKeys
,
};
this
.
labelValues
=
{
[
EMPTY_SELECTOR
]:
{},
};
this
.
logLabelOptions
=
labelKeys
.
map
((
key
:
string
)
=>
({
label
:
key
,
value
:
key
,
isLeaf
:
false
}));
}
catch
(
e
)
{
console
.
error
(
e
);
...
...
public/app/plugins/datasource/loki/mocks.ts
0 → 100644
View file @
bc4ba64a
import
{
DataSourceApi
}
from
'@grafana/ui'
;
export
function
makeMockLokiDatasource
(
labelsAndValues
:
{
[
label
:
string
]:
string
[]
}):
DataSourceApi
{
const
labels
=
Object
.
keys
(
labelsAndValues
);
return
{
metadataRequest
:
(
url
:
string
)
=>
{
let
responseData
;
if
(
url
===
'/api/prom/label'
)
{
responseData
=
labels
;
}
else
{
const
match
=
url
.
match
(
/^
\/
api
\/
prom
\/
label
\/(\w
*
)\/
values/
);
if
(
match
)
{
responseData
=
labelsAndValues
[
match
[
1
]];
}
}
if
(
responseData
)
{
return
{
data
:
{
data
:
responseData
,
},
};
}
else
{
throw
new
Error
(
`Unexpected url error,
${
url
}
`
);
}
},
}
as
any
;
}
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