Commit 39f7cff7 by Andrej Ocenas Committed by GitHub

Loki: Refactor editor and syntax hooks (#21687)

parent b28eac26
......@@ -3,7 +3,7 @@ import React, { memo } from 'react';
// Types
import { LokiQuery } from '../types';
import { useLokiSyntax } from './useLokiSyntax';
import { useLokiSyntaxAndLabels } from './useLokiSyntaxAndLabels';
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
import LokiDatasource from '../datasource';
......@@ -22,7 +22,7 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
to: Date.now(),
};
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
const { isSyntaxReady, setActiveOption, refreshLabels, syntax, logLabelOptions } = useLokiSyntaxAndLabels(
datasource.languageProvider,
absolute
);
......@@ -43,9 +43,10 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
data={null}
onLoadOptions={setActiveOption}
onLabelsRefresh={refreshLabels}
syntaxLoaded={isSyntaxReady}
absoluteRange={absolute}
{...syntaxProps}
syntax={syntax}
syntaxLoaded={isSyntaxReady}
logLabelOptions={logLabelOptions}
/>
</div>
);
......
......@@ -6,7 +6,6 @@ import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
import { LokiQueryField } from './LokiQueryField';
import { useLokiSyntax } from './useLokiSyntax';
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
......@@ -27,11 +26,6 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
};
}
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
datasource.languageProvider,
absolute
);
return (
<div>
<LokiQueryField
......@@ -41,11 +35,7 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
onRunQuery={onRunQuery}
history={[]}
data={data}
onLoadOptions={setActiveOption}
onLabelsRefresh={refreshLabels}
syntaxLoaded={isSyntaxReady}
absoluteRange={absolute}
{...syntaxProps}
/>
</div>
);
......
import React, { FunctionComponent } from 'react';
import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm';
import { useLokiSyntax } from './useLokiSyntax';
import { useLokiSyntaxAndLabels } from './useLokiSyntaxAndLabels';
import LokiLanguageProvider from '../language_provider';
export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ datasource, ...otherProps }) => {
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
type LokiQueryFieldProps = Omit<
LokiQueryFieldFormProps,
'syntax' | 'syntaxLoaded' | 'onLoadOptions' | 'onLabelsRefresh' | 'logLabelOptions'
>;
export const LokiQueryField: FunctionComponent<LokiQueryFieldProps> = props => {
const { datasource, absoluteRange, ...otherProps } = props;
const { isSyntaxReady, setActiveOption, refreshLabels, syntax, logLabelOptions } = useLokiSyntaxAndLabels(
datasource.languageProvider as LokiLanguageProvider,
otherProps.absoluteRange
absoluteRange
);
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
......@@ -21,7 +26,10 @@ export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ dat
*/
onLoadOptions={setActiveOption}
onLabelsRefresh={refreshLabels}
{...syntaxProps}
absoluteRange={absoluteRange}
syntax={syntax}
syntaxLoaded={isSyntaxReady}
logLabelOptions={logLabelOptions}
{...otherProps}
/>
);
......
......@@ -73,8 +73,6 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiData
export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> {
plugins: Plugin[];
modifiedSearch: string;
modifiedQuery: string;
constructor(props: LokiQueryFieldFormProps, context: React.Context<any>) {
super(props, context);
......
......@@ -14,14 +14,16 @@ describe('useLokiLabels hook', () => {
to: 1560153109000,
};
languageProvider.logLabelOptions = ['initial'];
languageProvider.refreshLogLabels = () => {
languageProvider.logLabelOptions = logLabelOptionsMock;
return Promise.resolve();
};
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, [], rangeMock));
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, rangeMock));
expect(result.current.logLabelOptions).toEqual(['initial']);
act(() => result.current.refreshLabels());
expect(result.current.logLabelOptions).toEqual([]);
await waitForNextUpdate();
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock);
});
......
......@@ -9,14 +9,13 @@ 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
* @param absoluteRange
*
* @description Fetches missing labels and enables labels refresh
*/
export const useLokiLabels = (
languageProvider: LokiLanguageProvider,
languageProviderInitialised: boolean,
activeOption: CascaderOption[],
absoluteRange: AbsoluteTimeRange
) => {
const mounted = useRefMounted();
......@@ -24,7 +23,12 @@ export const useLokiLabels = (
// State
const [logLabelOptions, setLogLabelOptions] = useState([]);
const [shouldTryRefreshLabels, setRefreshLabels] = useState(false);
const [shouldForceRefreshLabels, setForceRefreshLabels] = useState(false);
/**
* 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[]>([]);
// Async
const fetchOptionValues = async (option: string) => {
......@@ -35,11 +39,10 @@ export const useLokiLabels = (
};
const tryLabelsRefresh = async () => {
await languageProvider.refreshLogLabels(absoluteRange, shouldForceRefreshLabels);
await languageProvider.refreshLogLabels(absoluteRange);
if (mounted.current) {
setRefreshLabels(false);
setForceRefreshLabels(false);
setLogLabelOptions(languageProvider.logLabelOptions);
}
};
......@@ -68,18 +71,25 @@ export const useLokiLabels = (
}
}, [activeOption]);
// This effect is performed on shouldTryRefreshLabels or shouldForceRefreshLabels state change only.
// 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 || shouldForceRefreshLabels) {
if (shouldTryRefreshLabels) {
tryLabelsRefresh();
}
}, [shouldTryRefreshLabels, shouldForceRefreshLabels]);
}, [shouldTryRefreshLabels]);
// Initialize labels from the provider after it gets initialized (it's initialisation happens outside of this hook)
useEffect(() => {
if (languageProviderInitialised) {
setLogLabelOptions(languageProvider.logLabelOptions);
}
}, [languageProviderInitialised]);
return {
logLabelOptions,
setLogLabelOptions,
refreshLabels: () => setRefreshLabels(true),
setActiveOption,
};
};
......@@ -4,7 +4,7 @@ import { CascaderOption } from '@grafana/ui';
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiSyntax } from './useLokiSyntax';
import { useLokiSyntaxAndLabels } from './useLokiSyntaxAndLabels';
import { makeMockLokiDatasource } from '../mocks';
describe('useLokiSyntax hook', () => {
......@@ -35,7 +35,7 @@ describe('useLokiSyntax hook', () => {
};
it('should provide Loki syntax when used', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntaxAndLabels(languageProvider, rangeMock));
expect(result.current.syntax).toEqual(null);
await waitForNextUpdate();
......@@ -44,7 +44,7 @@ describe('useLokiSyntax hook', () => {
});
it('should fetch labels on first call', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntaxAndLabels(languageProvider, rangeMock));
expect(result.current.isSyntaxReady).toBeFalsy();
expect(result.current.logLabelOptions).toEqual([]);
......@@ -55,7 +55,7 @@ describe('useLokiSyntax hook', () => {
});
it('should try to fetch missing options when active option changes', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntaxAndLabels(languageProvider, rangeMock));
await waitForNextUpdate();
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
......
import { useState, useEffect } from 'react';
import Prism from 'prismjs';
import Prism, { Grammar } from 'prismjs';
import { AbsoluteTimeRange } from '@grafana/data';
import { CascaderOption } from '@grafana/ui';
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
import { useRefMounted } from 'app/core/hooks/useRefMounted';
......@@ -9,49 +8,69 @@ 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
* Initialise the language provider. Returns a languageProviderInitialized boolean cause there does not seem other way
* to know if the provider is already initialised or not. By the initialisation it modifies the provided
* languageProvider directly.
*/
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, absoluteRange: AbsoluteTimeRange) => {
const useInitLanguageProvider = (languageProvider: LokiLanguageProvider, absoluteRange: AbsoluteTimeRange) => {
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,
absoluteRange
);
const [languageProviderInitialized, setLanguageProviderInitialized] = useState(false);
// Async
const initializeLanguageProvider = async () => {
languageProvider.initialRange = absoluteRange;
await languageProvider.start();
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax();
if (mounted.current) {
setLogLabelOptions(languageProvider.logLabelOptions);
setSyntax(languageProvider.getSyntax());
setLanguageProviderInitilized(true);
setLanguageProviderInitialized(true);
}
};
// Effects
useEffect(() => {
initializeLanguageProvider();
}, []);
return languageProviderInitialized;
};
/**
* Returns syntax from languageProvider and initialises global Prism syntax. Waits until languageProvider itself is
* initialised (outside of this hook).
*/
const useLokiSyntax = (languageProvider: LokiLanguageProvider, languageProviderInitialized: boolean) => {
// State
const [syntax, setSyntax] = useState<Grammar>(null);
// Effects
useEffect(() => {
if (languageProviderInitialized) {
const syntax = languageProvider.getSyntax();
Prism.languages[PRISM_SYNTAX] = syntax;
setSyntax(syntax);
}
}, [languageProviderInitialized, languageProvider]);
return {
isSyntaxReady: !!syntax,
syntax,
};
};
/**
* Initializes given language provider, exposes Loki syntax and enables loading label option values
*/
export const useLokiSyntaxAndLabels = (languageProvider: LokiLanguageProvider, absoluteRange: AbsoluteTimeRange) => {
const languageProviderInitialized = useInitLanguageProvider(languageProvider, absoluteRange);
const { logLabelOptions, refreshLabels, setActiveOption } = useLokiLabels(
languageProvider,
languageProviderInitialized,
absoluteRange
);
const { isSyntaxReady, syntax } = useLokiSyntax(languageProvider, languageProviderInitialized);
return {
isSyntaxReady: languageProviderInitialized,
isSyntaxReady,
syntax,
logLabelOptions,
setActiveOption,
......
......@@ -13,6 +13,7 @@ import { RATE_RANGES } from '../prometheus/promql';
import LokiDatasource from './datasource';
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import { Grammar } from 'prismjs';
const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}';
......@@ -70,7 +71,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
// Strip syntax chars
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
getSyntax() {
getSyntax(): Grammar {
return syntax;
}
......
......@@ -188,9 +188,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
remaining.map((task: Promise<any>) => task.then(this.onUpdateLanguage).catch(() => {}));
})
.then(() => this.onUpdateLanguage())
.catch(({ isCanceled }) => {
if (isCanceled) {
console.warn('PromQueryField has unmounted, language provider intialization was canceled');
.catch(err => {
if (!err.isCanceled) {
throw err;
}
});
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment