mirror of https://github.com/grafana/grafana
feat(Explore): make sure Loki labels are up to date (#16131)
* Migrated loki syntax and labels logic to useLokiSyntax hook * Enable loki labels refresh after specified interval has passed * Enable periodic loki labels refresh when labels selector is opened * Fix prettier * Add react-hooks-testing-library and disable lib check on typecheck * Add tests for loki syntax/label hooks * Move tsc's skipLibCheck option to tsconfig for webpack to pick it up * Set log labels refresh marker variable when log labels fetch start * Fix prettier issues * Fix type on activeOption in useLokiLabel hook * Typo fixes and types in useLokiSyntax hook test fixes * Make sure effect's setState is not performed on unmounted component * Extract logic for checking if is component mounted to a separate hookpull/16200/head
parent
cf7a5b552b
commit
c2e9daad1e
@ -0,0 +1,12 @@ |
||||
import { useRef, useEffect } from 'react'; |
||||
|
||||
export const useRefMounted = () => { |
||||
const refMounted = useRef(false); |
||||
useEffect(() => { |
||||
refMounted.current = true; |
||||
return () => { |
||||
refMounted.current = false; |
||||
}; |
||||
}); |
||||
return refMounted; |
||||
}; |
@ -1,266 +1,26 @@ |
||||
// Libraries
|
||||
import React from 'react'; |
||||
import Cascader from 'rc-cascader'; |
||||
import PluginPrism from 'slate-prism'; |
||||
import Prism from 'prismjs'; |
||||
|
||||
// Components
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; |
||||
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; |
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces'; |
||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; |
||||
|
||||
// Types
|
||||
import { LokiQuery } from '../types'; |
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; |
||||
import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise'; |
||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui'; |
||||
|
||||
const PRISM_SYNTAX = 'promql'; |
||||
|
||||
function getChooserText(hasSytax, hasLogLabels) { |
||||
if (!hasSytax) { |
||||
return 'Loading labels...'; |
||||
} |
||||
if (!hasLogLabels) { |
||||
return '(No labels found)'; |
||||
} |
||||
return 'Log labels'; |
||||
} |
||||
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { |
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) { |
||||
case 'context-labels': { |
||||
const nextChar = getNextCharacter(); |
||||
if (!nextChar || nextChar === '}' || nextChar === ',') { |
||||
suggestion += '='; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'context-label-values': { |
||||
// Always add quotes and remove existing ones instead
|
||||
if (!typeaheadText.match(/^(!?=~?"|")/)) { |
||||
suggestion = `"${suggestion}`; |
||||
} |
||||
if (getNextCharacter() !== '"') { |
||||
suggestion = `${suggestion}"`; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
default: |
||||
} |
||||
return suggestion; |
||||
} |
||||
|
||||
interface CascaderOption { |
||||
label: string; |
||||
value: string; |
||||
children?: CascaderOption[]; |
||||
disabled?: boolean; |
||||
} |
||||
|
||||
interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> { |
||||
history: HistoryItem[]; |
||||
} |
||||
|
||||
interface LokiQueryFieldState { |
||||
logLabelOptions: any[]; |
||||
syntaxLoaded: boolean; |
||||
} |
||||
|
||||
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> { |
||||
plugins: any[]; |
||||
pluginsSearch: any[]; |
||||
languageProvider: any; |
||||
modifiedSearch: string; |
||||
modifiedQuery: string; |
||||
languageProviderInitializationPromise: CancelablePromise<any>; |
||||
|
||||
constructor(props: LokiQueryFieldProps, context) { |
||||
super(props, context); |
||||
|
||||
if (props.datasource.languageProvider) { |
||||
this.languageProvider = props.datasource.languageProvider; |
||||
} |
||||
|
||||
this.plugins = [ |
||||
BracesPlugin(), |
||||
RunnerPlugin({ handler: props.onExecuteQuery }), |
||||
PluginPrism({ |
||||
onlyIn: node => node.type === 'code_block', |
||||
getSyntax: node => 'promql', |
||||
}), |
||||
]; |
||||
|
||||
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })]; |
||||
|
||||
this.state = { |
||||
logLabelOptions: [], |
||||
syntaxLoaded: false, |
||||
}; |
||||
} |
||||
|
||||
componentDidMount() { |
||||
if (this.languageProvider) { |
||||
this.languageProviderInitializationPromise = makePromiseCancelable(this.languageProvider.start()); |
||||
|
||||
this.languageProviderInitializationPromise.promise |
||||
.then(remaining => { |
||||
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {})); |
||||
}) |
||||
.then(() => this.onUpdateLanguage()) |
||||
.catch(({ isCanceled }) => { |
||||
if (isCanceled) { |
||||
console.warn('LokiQueryField has unmounted, language provider intialization was canceled'); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
if (this.languageProviderInitializationPromise) { |
||||
this.languageProviderInitializationPromise.cancel(); |
||||
} |
||||
} |
||||
|
||||
loadOptions = (selectedOptions: CascaderOption[]) => { |
||||
const targetOption = selectedOptions[selectedOptions.length - 1]; |
||||
|
||||
this.setState(state => { |
||||
const nextOptions = state.logLabelOptions.map(option => { |
||||
if (option.value === targetOption.value) { |
||||
return { |
||||
...option, |
||||
loading: true, |
||||
}; |
||||
} |
||||
return option; |
||||
}); |
||||
return { logLabelOptions: nextOptions }; |
||||
}); |
||||
|
||||
this.languageProvider |
||||
.fetchLabelValues(targetOption.value) |
||||
.then(this.onUpdateLanguage) |
||||
.catch(() => {}); |
||||
}; |
||||
|
||||
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => { |
||||
if (selectedOptions.length === 2) { |
||||
const key = selectedOptions[0].value; |
||||
const value = selectedOptions[1].value; |
||||
const query = `{${key}="${value}"}`; |
||||
this.onChangeQuery(query, true); |
||||
} |
||||
}; |
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => { |
||||
// Send text change to parent
|
||||
const { query, onQueryChange, onExecuteQuery } = this.props; |
||||
if (onQueryChange) { |
||||
const nextQuery = { ...query, expr: value }; |
||||
onQueryChange(nextQuery); |
||||
|
||||
if (override && onExecuteQuery) { |
||||
onExecuteQuery(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
onClickHintFix = () => { |
||||
const { hint, onExecuteHint } = this.props; |
||||
if (onExecuteHint && hint && hint.fix) { |
||||
onExecuteHint(hint.fix.action); |
||||
} |
||||
}; |
||||
|
||||
onUpdateLanguage = () => { |
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax(); |
||||
const { logLabelOptions } = this.languageProvider; |
||||
this.setState({ |
||||
logLabelOptions, |
||||
syntaxLoaded: true, |
||||
}); |
||||
}; |
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { |
||||
if (!this.languageProvider) { |
||||
return { suggestions: [] }; |
||||
} |
||||
|
||||
const { history } = this.props; |
||||
const { prefix, text, value, wrapperNode } = typeahead; |
||||
|
||||
// Get DOM-dependent context
|
||||
const wrapperClasses = Array.from(wrapperNode.classList); |
||||
const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); |
||||
const labelKey = labelKeyNode && labelKeyNode.textContent; |
||||
const nextChar = getNextCharacter(); |
||||
|
||||
const result = this.languageProvider.provideCompletionItems( |
||||
{ text, value, prefix, wrapperClasses, labelKey }, |
||||
{ history } |
||||
); |
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
render() { |
||||
const { error, hint, query } = this.props; |
||||
const { logLabelOptions, syntaxLoaded } = this.state; |
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; |
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0; |
||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels); |
||||
|
||||
return ( |
||||
<> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels} loadData={this.loadOptions}> |
||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}> |
||||
{chooserText} <i className="fa fa-caret-down" /> |
||||
</button> |
||||
</Cascader> |
||||
</div> |
||||
<div className="gf-form gf-form--grow"> |
||||
<QueryField |
||||
additionalPlugins={this.plugins} |
||||
cleanText={cleanText} |
||||
initialQuery={query.expr} |
||||
onTypeahead={this.onTypeahead} |
||||
onWillApplySuggestion={willApplySuggestion} |
||||
onQueryChange={this.onChangeQuery} |
||||
onExecuteQuery={this.props.onExecuteQuery} |
||||
placeholder="Enter a Loki query" |
||||
portalOrigin="loki" |
||||
syntaxLoaded={syntaxLoaded} |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
{error ? <div className="prom-query-field-info text-error">{error}</div> : null} |
||||
{hint ? ( |
||||
<div className="prom-query-field-info text-warning"> |
||||
{hint.label}{' '} |
||||
{hint.fix ? ( |
||||
<a className="text-link muted" onClick={this.onClickHintFix}> |
||||
{hint.fix.label} |
||||
</a> |
||||
) : null} |
||||
</div> |
||||
) : null} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
import React, { FunctionComponent } from 'react'; |
||||
import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm'; |
||||
import { useLokiSyntax } from './useLokiSyntax'; |
||||
|
||||
const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ datasource, ...otherProps }) => { |
||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(datasource.languageProvider); |
||||
|
||||
return ( |
||||
<LokiQueryFieldForm |
||||
datasource={datasource} |
||||
syntaxLoaded={isSyntaxReady} |
||||
/** |
||||
* setActiveOption name is intentional. Because of the way rc-cascader requests additional data |
||||
* https://github.com/react-component/cascader/blob/master/src/Cascader.jsx#L165
|
||||
* we are notyfing useLokiSyntax hook, what the active option is, and then it's up to the hook logic |
||||
* to fetch data of options that aren't fetched yet |
||||
*/ |
||||
onLoadOptions={setActiveOption} |
||||
onLabelsRefresh={refreshLabels} |
||||
{...syntaxProps} |
||||
{...otherProps} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default LokiQueryField; |
||||
|
@ -0,0 +1,217 @@ |
||||
// Libraries
|
||||
import React from 'react'; |
||||
import Cascader from 'rc-cascader'; |
||||
import PluginPrism from 'slate-prism'; |
||||
|
||||
// Components
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; |
||||
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; |
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces'; |
||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; |
||||
|
||||
// Types
|
||||
import { LokiQuery } from '../types'; |
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; |
||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui'; |
||||
|
||||
function getChooserText(hasSytax, hasLogLabels) { |
||||
if (!hasSytax) { |
||||
return 'Loading labels...'; |
||||
} |
||||
if (!hasLogLabels) { |
||||
return '(No labels found)'; |
||||
} |
||||
return 'Log labels'; |
||||
} |
||||
|
||||
function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { |
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) { |
||||
case 'context-labels': { |
||||
const nextChar = getNextCharacter(); |
||||
if (!nextChar || nextChar === '}' || nextChar === ',') { |
||||
suggestion += '='; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'context-label-values': { |
||||
// Always add quotes and remove existing ones instead
|
||||
if (!typeaheadText.match(/^(!?=~?"|")/)) { |
||||
suggestion = `"${suggestion}`; |
||||
} |
||||
if (getNextCharacter() !== '"') { |
||||
suggestion = `${suggestion}"`; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
default: |
||||
} |
||||
return suggestion; |
||||
} |
||||
|
||||
export interface CascaderOption { |
||||
label: string; |
||||
value: string; |
||||
children?: CascaderOption[]; |
||||
disabled?: boolean; |
||||
} |
||||
|
||||
export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> { |
||||
history: HistoryItem[]; |
||||
syntax: any; |
||||
logLabelOptions: any[]; |
||||
syntaxLoaded: any; |
||||
onLoadOptions: (selectedOptions: CascaderOption[]) => void; |
||||
onLabelsRefresh?: () => void; |
||||
} |
||||
|
||||
export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> { |
||||
plugins: any[]; |
||||
pluginsSearch: any[]; |
||||
modifiedSearch: string; |
||||
modifiedQuery: string; |
||||
|
||||
constructor(props: LokiQueryFieldFormProps, context) { |
||||
super(props, context); |
||||
|
||||
this.plugins = [ |
||||
BracesPlugin(), |
||||
RunnerPlugin({ handler: props.onExecuteQuery }), |
||||
PluginPrism({ |
||||
onlyIn: node => node.type === 'code_block', |
||||
getSyntax: node => 'promql', |
||||
}), |
||||
]; |
||||
|
||||
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })]; |
||||
} |
||||
|
||||
loadOptions = (selectedOptions: CascaderOption[]) => { |
||||
this.props.onLoadOptions(selectedOptions); |
||||
}; |
||||
|
||||
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => { |
||||
if (selectedOptions.length === 2) { |
||||
const key = selectedOptions[0].value; |
||||
const value = selectedOptions[1].value; |
||||
const query = `{${key}="${value}"}`; |
||||
this.onChangeQuery(query, true); |
||||
} |
||||
}; |
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => { |
||||
// Send text change to parent
|
||||
const { query, onQueryChange, onExecuteQuery } = this.props; |
||||
if (onQueryChange) { |
||||
const nextQuery = { ...query, expr: value }; |
||||
onQueryChange(nextQuery); |
||||
|
||||
if (override && onExecuteQuery) { |
||||
onExecuteQuery(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
onClickHintFix = () => { |
||||
const { hint, onExecuteHint } = this.props; |
||||
if (onExecuteHint && hint && hint.fix) { |
||||
onExecuteHint(hint.fix.action); |
||||
} |
||||
}; |
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { |
||||
const { datasource } = this.props; |
||||
if (!datasource.languageProvider) { |
||||
return { suggestions: [] }; |
||||
} |
||||
|
||||
const { history } = this.props; |
||||
const { prefix, text, value, wrapperNode } = typeahead; |
||||
|
||||
// Get DOM-dependent context
|
||||
const wrapperClasses = Array.from(wrapperNode.classList); |
||||
const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); |
||||
const labelKey = labelKeyNode && labelKeyNode.textContent; |
||||
const nextChar = getNextCharacter(); |
||||
|
||||
const result = datasource.languageProvider.provideCompletionItems( |
||||
{ text, value, prefix, wrapperClasses, labelKey }, |
||||
{ history } |
||||
); |
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
render() { |
||||
const { |
||||
error, |
||||
hint, |
||||
query, |
||||
syntaxLoaded, |
||||
logLabelOptions, |
||||
onLoadOptions, |
||||
onLabelsRefresh, |
||||
datasource, |
||||
} = this.props; |
||||
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined; |
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0; |
||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels); |
||||
|
||||
return ( |
||||
<> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<Cascader |
||||
options={logLabelOptions} |
||||
onChange={this.onChangeLogLabels} |
||||
loadData={onLoadOptions} |
||||
onPopupVisibleChange={isVisible => { |
||||
if (isVisible && onLabelsRefresh) { |
||||
onLabelsRefresh(); |
||||
} |
||||
}} |
||||
> |
||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}> |
||||
{chooserText} <i className="fa fa-caret-down" /> |
||||
</button> |
||||
</Cascader> |
||||
</div> |
||||
<div className="gf-form gf-form--grow"> |
||||
<QueryField |
||||
additionalPlugins={this.plugins} |
||||
cleanText={cleanText} |
||||
initialQuery={query.expr} |
||||
onTypeahead={this.onTypeahead} |
||||
onWillApplySuggestion={willApplySuggestion} |
||||
onQueryChange={this.onChangeQuery} |
||||
onExecuteQuery={this.props.onExecuteQuery} |
||||
placeholder="Enter a Loki query" |
||||
portalOrigin="loki" |
||||
syntaxLoaded={syntaxLoaded} |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
{error ? <div className="prom-query-field-info text-error">{error}</div> : null} |
||||
{hint ? ( |
||||
<div className="prom-query-field-info text-warning"> |
||||
{hint.label}{' '} |
||||
{hint.fix ? ( |
||||
<a className="text-link muted" onClick={this.onClickHintFix}> |
||||
{hint.fix.label} |
||||
</a> |
||||
) : null} |
||||
</div> |
||||
) : null} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
import { renderHook, act } from 'react-hooks-testing-library'; |
||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider'; |
||||
import { useLokiLabels } from './useLokiLabels'; |
||||
|
||||
describe('useLokiLabels hook', () => { |
||||
const datasource = { |
||||
metadataRequest: () => ({ data: { data: [] } }), |
||||
}; |
||||
const languageProvider = new LanguageProvider(datasource); |
||||
const logLabelOptionsMock = ['Holy mock!']; |
||||
|
||||
languageProvider.refreshLogLabels = () => { |
||||
languageProvider.logLabelOptions = logLabelOptionsMock; |
||||
return Promise.resolve(); |
||||
}; |
||||
|
||||
it('should refresh labels', async () => { |
||||
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, [])); |
||||
act(() => result.current.refreshLabels()); |
||||
expect(result.current.logLabelOptions).toEqual([]); |
||||
await waitForNextUpdate(); |
||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock); |
||||
}); |
||||
}); |
@ -0,0 +1,79 @@ |
||||
import { useState, useEffect } from 'react'; |
||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider'; |
||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; |
||||
import { useRefMounted } from 'app/core/hooks/useRefMounted'; |
||||
|
||||
/** |
||||
* |
||||
* @param languageProvider |
||||
* @param languageProviderInitialised |
||||
* @param activeOption rc-cascader provided option used to fetch option's values that hasn't been loaded yet |
||||
* |
||||
* @description Fetches missing labels and enables labels refresh |
||||
*/ |
||||
export const useLokiLabels = ( |
||||
languageProvider: LokiLanguageProvider, |
||||
languageProviderInitialised: boolean, |
||||
activeOption: CascaderOption[] |
||||
) => { |
||||
const mounted = useRefMounted(); |
||||
|
||||
// State
|
||||
const [logLabelOptions, setLogLabelOptions] = useState([]); |
||||
const [shouldTryRefreshLabels, setRefreshLabels] = useState(false); |
||||
|
||||
// Async
|
||||
const fetchOptionValues = async option => { |
||||
await languageProvider.fetchLabelValues(option); |
||||
if (mounted.current) { |
||||
setLogLabelOptions(languageProvider.logLabelOptions); |
||||
} |
||||
}; |
||||
|
||||
const tryLabelsRefresh = async () => { |
||||
await languageProvider.refreshLogLabels(); |
||||
if (mounted.current) { |
||||
setRefreshLabels(false); |
||||
setLogLabelOptions(languageProvider.logLabelOptions); |
||||
} |
||||
}; |
||||
|
||||
// Effects
|
||||
|
||||
// This effect performs loading of options that hasn't been loaded yet
|
||||
// It's a subject of activeOption state change only. This is because of specific behavior or rc-cascader
|
||||
// https://github.com/react-component/cascader/blob/master/src/Cascader.jsx#L165
|
||||
useEffect(() => { |
||||
if (languageProviderInitialised) { |
||||
const targetOption = activeOption[activeOption.length - 1]; |
||||
if (targetOption) { |
||||
const nextOptions = logLabelOptions.map(option => { |
||||
if (option.value === targetOption.value) { |
||||
return { |
||||
...option, |
||||
loading: true, |
||||
}; |
||||
} |
||||
return option; |
||||
}); |
||||
setLogLabelOptions(nextOptions); // to set loading
|
||||
fetchOptionValues(targetOption.value); |
||||
} |
||||
} |
||||
}, [activeOption]); |
||||
|
||||
// This effect is performed on shouldTryRefreshLabels state change only.
|
||||
// Since shouldTryRefreshLabels is reset AFTER the labels are refreshed we are secured in case of trying to refresh
|
||||
// when previous refresh hasn't finished yet
|
||||
useEffect(() => { |
||||
if (shouldTryRefreshLabels) { |
||||
tryLabelsRefresh(); |
||||
} |
||||
}, [shouldTryRefreshLabels]); |
||||
|
||||
return { |
||||
logLabelOptions, |
||||
setLogLabelOptions, |
||||
refreshLabels: () => setRefreshLabels(true), |
||||
}; |
||||
}; |
@ -0,0 +1,66 @@ |
||||
import { renderHook, act } from 'react-hooks-testing-library'; |
||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider'; |
||||
import { useLokiSyntax } from './useLokiSyntax'; |
||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; |
||||
|
||||
describe('useLokiSyntax hook', () => { |
||||
const datasource = { |
||||
metadataRequest: () => ({ data: { data: [] } }), |
||||
}; |
||||
const languageProvider = new LanguageProvider(datasource); |
||||
const logLabelOptionsMock = ['Holy mock!']; |
||||
const logLabelOptionsMock2 = ['Mock the hell?!']; |
||||
const logLabelOptionsMock3 = ['Oh my mock!']; |
||||
|
||||
languageProvider.refreshLogLabels = () => { |
||||
languageProvider.logLabelOptions = logLabelOptionsMock; |
||||
return Promise.resolve(); |
||||
}; |
||||
|
||||
languageProvider.fetchLogLabels = () => { |
||||
languageProvider.logLabelOptions = logLabelOptionsMock2; |
||||
return Promise.resolve([]); |
||||
}; |
||||
|
||||
const activeOptionMock: CascaderOption = { |
||||
label: '', |
||||
value: '', |
||||
}; |
||||
|
||||
it('should provide Loki syntax when used', async () => { |
||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider)); |
||||
expect(result.current.syntax).toEqual(null); |
||||
|
||||
await waitForNextUpdate(); |
||||
|
||||
expect(result.current.syntax).toEqual(languageProvider.getSyntax()); |
||||
}); |
||||
|
||||
it('should fetch labels on first call', async () => { |
||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider)); |
||||
expect(result.current.isSyntaxReady).toBeFalsy(); |
||||
expect(result.current.logLabelOptions).toEqual([]); |
||||
|
||||
await waitForNextUpdate(); |
||||
|
||||
expect(result.current.isSyntaxReady).toBeTruthy(); |
||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2); |
||||
}); |
||||
|
||||
it('should try to fetch missing options when active option changes', async () => { |
||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider)); |
||||
await waitForNextUpdate(); |
||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2); |
||||
|
||||
languageProvider.fetchLabelValues = (key: string) => { |
||||
languageProvider.logLabelOptions = logLabelOptionsMock3; |
||||
return Promise.resolve(); |
||||
}; |
||||
|
||||
act(() => result.current.setActiveOption([activeOptionMock])); |
||||
|
||||
await waitForNextUpdate(); |
||||
|
||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock3); |
||||
}); |
||||
}); |
@ -0,0 +1,57 @@ |
||||
import { useState, useEffect } from 'react'; |
||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider'; |
||||
import Prism from 'prismjs'; |
||||
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels'; |
||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; |
||||
import { useRefMounted } from 'app/core/hooks/useRefMounted'; |
||||
|
||||
const PRISM_SYNTAX = 'promql'; |
||||
|
||||
/** |
||||
* |
||||
* @param languageProvider |
||||
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values |
||||
*/ |
||||
export const useLokiSyntax = (languageProvider: LokiLanguageProvider) => { |
||||
const mounted = useRefMounted(); |
||||
// State
|
||||
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false); |
||||
const [syntax, setSyntax] = useState(null); |
||||
|
||||
/** |
||||
* Holds information about currently selected option from rc-cascader to perform effect |
||||
* that loads option values not fetched yet. Based on that useLokiLabels hook decides whether or not |
||||
* the option requires additional data fetching |
||||
*/ |
||||
const [activeOption, setActiveOption] = useState<CascaderOption[]>(); |
||||
|
||||
const { logLabelOptions, setLogLabelOptions, refreshLabels } = useLokiLabels( |
||||
languageProvider, |
||||
languageProviderInitialized, |
||||
activeOption |
||||
); |
||||
|
||||
// Async
|
||||
const initializeLanguageProvider = async () => { |
||||
await languageProvider.start(); |
||||
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax(); |
||||
if (mounted.current) { |
||||
setLogLabelOptions(languageProvider.logLabelOptions); |
||||
setSyntax(languageProvider.getSyntax()); |
||||
setLanguageProviderInitilized(true); |
||||
} |
||||
}; |
||||
|
||||
// Effects
|
||||
useEffect(() => { |
||||
initializeLanguageProvider(); |
||||
}, []); |
||||
|
||||
return { |
||||
isSyntaxReady: languageProviderInitialized, |
||||
syntax, |
||||
logLabelOptions, |
||||
setActiveOption, |
||||
refreshLabels, |
||||
}; |
||||
}; |
Loading…
Reference in new issue