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