mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
influxdb: switch the raw influxql editor from angular to react (#31860)
* influxdb: switch the raw influxql editor from angular to react * influxdb: raw-influxql: better callback-naming * influxdb: raw-influxql: use custom hook * influxdb: flux: raw-editor: add unit tests
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { select } from 'react-select-event';
|
||||
import { RawInfluxQLEditor } from './RawInfluxQLEditor';
|
||||
import { InfluxQuery } from '../types';
|
||||
|
||||
const query: InfluxQuery = {
|
||||
refId: 'A',
|
||||
query: 'test query 1',
|
||||
resultFormat: 'table',
|
||||
alias: 'alias42',
|
||||
};
|
||||
|
||||
describe('RawInfluxQLEditor', () => {
|
||||
it('should render', () => {
|
||||
render(<RawInfluxQLEditor onRunQuery={() => null} onChange={() => null} query={query} />);
|
||||
const queryTextarea = screen.getByLabelText('query');
|
||||
const aliasInput = screen.getByLabelText('Alias by');
|
||||
const formatSelect = screen.getByLabelText('Format as');
|
||||
|
||||
expect(formatSelect).toBeInTheDocument();
|
||||
expect(queryTextarea).toBeInTheDocument();
|
||||
expect(aliasInput).toBeInTheDocument();
|
||||
|
||||
expect(queryTextarea).toHaveValue('test query 1');
|
||||
expect(aliasInput).toHaveValue('alias42');
|
||||
|
||||
// the only way to validate the text-displayed on the select-box
|
||||
expect(screen.getByText('Table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle no-alias, no-query, no-resultFormat', () => {
|
||||
const emptyQuery = { refId: 'B' };
|
||||
render(<RawInfluxQLEditor onRunQuery={() => null} onChange={() => null} query={emptyQuery} />);
|
||||
|
||||
const queryTextarea = screen.getByLabelText('query');
|
||||
const aliasInput = screen.getByLabelText('Alias by');
|
||||
|
||||
const formatSelect = screen.getByLabelText('Format as');
|
||||
expect(formatSelect).toBeInTheDocument();
|
||||
|
||||
expect(queryTextarea).toBeInTheDocument();
|
||||
expect(aliasInput).toBeInTheDocument();
|
||||
|
||||
expect(queryTextarea).toHaveValue('');
|
||||
expect(aliasInput).toHaveValue('');
|
||||
|
||||
// the only way to validate the text-displayed on the select-box
|
||||
expect(screen.getByText('Time series')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onChange immediately when resultFormat change', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<RawInfluxQLEditor onRunQuery={() => null} onChange={onChange} query={query} />);
|
||||
|
||||
const formatSelect = screen.getByLabelText('Format as');
|
||||
expect(formatSelect).toBeInTheDocument();
|
||||
|
||||
await select(formatSelect, 'Time series');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ ...query, resultFormat: 'time_series' });
|
||||
});
|
||||
|
||||
it('should only call onChange on blur when query changes', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<RawInfluxQLEditor onRunQuery={() => null} onChange={onChange} query={query} />);
|
||||
|
||||
const queryTextarea = screen.getByLabelText('query');
|
||||
expect(queryTextarea).toBeInTheDocument();
|
||||
const aliasInput = screen.getByLabelText('Alias by');
|
||||
expect(aliasInput).toBeInTheDocument();
|
||||
|
||||
// value before
|
||||
expect(queryTextarea).toHaveValue('test query 1');
|
||||
|
||||
userEvent.type(queryTextarea, 'new changes');
|
||||
|
||||
// the field should have a new value, but no onChange yet.
|
||||
expect(queryTextarea).toHaveValue('test query 1new changes');
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
aliasInput.focus(); // this should trigger blur on queryTextarea
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith({ ...query, query: 'test query 1new changes' });
|
||||
});
|
||||
it('should only call onChange on blur when alias changes', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<RawInfluxQLEditor onRunQuery={() => null} onChange={onChange} query={query} />);
|
||||
|
||||
const queryTextarea = screen.getByLabelText('query');
|
||||
expect(queryTextarea).toBeInTheDocument();
|
||||
const aliasInput = screen.getByLabelText('Alias by');
|
||||
expect(aliasInput).toBeInTheDocument();
|
||||
|
||||
// value before
|
||||
expect(aliasInput).toHaveValue('alias42');
|
||||
|
||||
userEvent.type(aliasInput, 'new changes');
|
||||
|
||||
// the field should have a new value, but no onChange yet.
|
||||
expect(aliasInput).toHaveValue('alias42new changes');
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
queryTextarea.focus(); // this should trigger blur on queryTextarea
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith({ ...query, alias: 'alias42new changes' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { FC } from 'react';
|
||||
import { TextArea, InlineFormLabel, Input, Select, HorizontalGroup } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ResultFormat, InfluxQuery } from '../types';
|
||||
import { useShadowedState } from './useShadowedState';
|
||||
import { useUniqueId } from './useUniqueId';
|
||||
|
||||
const RESULT_FORMATS: Array<SelectableValue<ResultFormat>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Logs', value: 'logs' },
|
||||
];
|
||||
|
||||
const DEFAULT_RESULT_FORMAT: ResultFormat = 'time_series';
|
||||
|
||||
type Props = {
|
||||
query: InfluxQuery;
|
||||
onChange: (query: InfluxQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
};
|
||||
|
||||
// we handle 3 fields: "query", "alias", "resultFormat"
|
||||
// "resultFormat" changes are applied immediately
|
||||
// "query" and "alias" changes only happen on onblur
|
||||
export const RawInfluxQLEditor: FC<Props> = ({ query, onChange, onRunQuery }) => {
|
||||
const [currentQuery, setCurrentQuery] = useShadowedState(query.query);
|
||||
const [currentAlias, setCurrentAlias] = useShadowedState(query.alias);
|
||||
const aliasElementId = useUniqueId();
|
||||
const selectElementId = useUniqueId();
|
||||
|
||||
const applyDelayedChangesAndRunQuery = () => {
|
||||
onChange({
|
||||
...query,
|
||||
query: currentQuery,
|
||||
alias: currentAlias,
|
||||
});
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextArea
|
||||
aria-label="query"
|
||||
rows={3}
|
||||
spellCheck={false}
|
||||
placeholder="InfluxDB Query"
|
||||
onBlur={applyDelayedChangesAndRunQuery}
|
||||
onChange={(e) => {
|
||||
setCurrentQuery(e.currentTarget.value);
|
||||
}}
|
||||
value={currentQuery ?? ''}
|
||||
/>
|
||||
<HorizontalGroup>
|
||||
<InlineFormLabel htmlFor={selectElementId}>Format as</InlineFormLabel>
|
||||
<Select
|
||||
inputId={selectElementId}
|
||||
onChange={(v) => {
|
||||
onChange({ ...query, resultFormat: v.value });
|
||||
onRunQuery();
|
||||
}}
|
||||
value={query.resultFormat ?? DEFAULT_RESULT_FORMAT}
|
||||
options={RESULT_FORMATS}
|
||||
/>
|
||||
<InlineFormLabel htmlFor={aliasElementId}>Alias by</InlineFormLabel>
|
||||
<Input
|
||||
id={aliasElementId}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
placeholder="Naming pattern"
|
||||
onBlur={applyDelayedChangesAndRunQuery}
|
||||
onChange={(e) => {
|
||||
setCurrentAlias(e.currentTarget.value);
|
||||
}}
|
||||
value={currentAlias ?? ''}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useShadowedState } from './useShadowedState';
|
||||
|
||||
describe('useShadowedState', () => {
|
||||
it('should work correctly', () => {
|
||||
const { result, rerender } = renderHook(({ outsideVal }) => useShadowedState(outsideVal), {
|
||||
initialProps: { outsideVal: '42' },
|
||||
});
|
||||
|
||||
// first we verify it has the initial value
|
||||
expect(result.current[0]).toBe('42');
|
||||
|
||||
// then we change it
|
||||
act(() => {
|
||||
result.current[1]('53');
|
||||
});
|
||||
|
||||
// and verify it has the changed value
|
||||
expect(result.current[0]).toBe('53');
|
||||
|
||||
// then we change the value from the outside
|
||||
rerender({ outsideVal: '71' });
|
||||
|
||||
// and verify the now has the value from the outside
|
||||
expect(result.current[0]).toBe('71');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export function useShadowedState<T>(outsideVal: T): [T, (newVal: T) => void] {
|
||||
const [currentVal, setCurrentVal] = useState(outsideVal);
|
||||
const prevOutsideVal = useRef(outsideVal);
|
||||
|
||||
useEffect(() => {
|
||||
// if the value changes from the outside, we accept it
|
||||
if (prevOutsideVal.current !== outsideVal && currentVal !== outsideVal) {
|
||||
prevOutsideVal.current = outsideVal;
|
||||
setCurrentVal(outsideVal);
|
||||
}
|
||||
}, [outsideVal, currentVal]);
|
||||
|
||||
return [currentVal, setCurrentVal];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useUniqueId } from './useUniqueId';
|
||||
|
||||
describe('useUniqueId', () => {
|
||||
it('should work correctly', () => {
|
||||
const { result: resultA, rerender: rerenderA } = renderHook(() => useUniqueId());
|
||||
const { result: resultB, rerender: rerenderB } = renderHook(() => useUniqueId());
|
||||
|
||||
// the values of the separate hooks should be different
|
||||
expect(resultA.current).not.toBe(resultB.current);
|
||||
|
||||
// we copy the current values after the first render
|
||||
const firstValueA = resultA.current;
|
||||
const firstValueB = resultB.current;
|
||||
|
||||
rerenderA();
|
||||
rerenderB();
|
||||
|
||||
// we check that the value did not change
|
||||
expect(resultA.current).toBe(firstValueA);
|
||||
expect(resultB.current).toBe(firstValueB);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useRef } from 'react';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
export function useUniqueId(): string {
|
||||
// we need to lazy-init this ref.
|
||||
// otherwise we would call `uniqueId`
|
||||
// on every render. unfortunately
|
||||
// useRef does not have lazy-init builtin,
|
||||
// like useState does. we do it manually.
|
||||
const idRefLazy = useRef<string | null>(null);
|
||||
|
||||
if (idRefLazy.current == null) {
|
||||
idRefLazy.current = uniqueId();
|
||||
}
|
||||
|
||||
return idRefLazy.current;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import VariableQueryEditor from './components/VariableQueryEditor';
|
||||
// This adds a directive that is used in the query editor
|
||||
import './components/FluxQueryEditor';
|
||||
|
||||
// This adds a directive that is used in the query editor
|
||||
import './registerRawInfluxQLEditor';
|
||||
|
||||
class InfluxAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
||||
|
||||
@@ -9,44 +9,11 @@
|
||||
|
||||
<query-editor-row ng-if="!ctrl.datasource.isFlux" query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
|
||||
<div ng-if="ctrl.target.rawQuery">
|
||||
<div class="gf-form">
|
||||
<textarea
|
||||
rows="3"
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.target.query"
|
||||
spellcheck="false"
|
||||
placeholder="InfluxDB Query"
|
||||
ng-model-onblur
|
||||
ng-change="ctrl.refresh()"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword">FORMAT AS</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select
|
||||
class="gf-form-input gf-size-auto"
|
||||
ng-model="ctrl.target.resultFormat"
|
||||
ng-options="f.value as f.text for f in ctrl.resultFormats"
|
||||
ng-change="ctrl.refresh()"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-25" ng-hide="ctrl.target.resultFormat === 'table'">
|
||||
<label class="gf-form-label query-keyword">ALIAS BY</label>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.target.alias"
|
||||
spellcheck="false"
|
||||
placeholder="Naming pattern"
|
||||
ng-blur="ctrl.refresh()"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<raw-influx-editor
|
||||
query="ctrl.target"
|
||||
on-change="ctrl.onRawInfluxQLChange"
|
||||
onRunQuery="ctrl.onRunQuery"
|
||||
></raw-influx-editor>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.target.rawQuery">
|
||||
|
||||
@@ -83,6 +83,13 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
this.target.query = target.query;
|
||||
};
|
||||
|
||||
// only called from raw-mode influxql-editor
|
||||
onRawInfluxQLChange = (target: InfluxQuery) => {
|
||||
this.target.query = target.query;
|
||||
this.target.resultFormat = target.resultFormat;
|
||||
this.target.alias = target.alias;
|
||||
};
|
||||
|
||||
onRunQuery = () => {
|
||||
this.panelCtrl.refresh();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { RawInfluxQLEditor } from './components/RawInfluxQLEditor';
|
||||
|
||||
coreModule.directive('rawInfluxEditor', [
|
||||
'reactDirective',
|
||||
(reactDirective: any) => {
|
||||
return reactDirective(RawInfluxQLEditor, ['query', 'onChange', 'onRunQuery']);
|
||||
},
|
||||
]);
|
||||
@@ -38,10 +38,12 @@ export interface InfluxQueryTag {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ResultFormat = 'time_series' | 'table' | 'logs';
|
||||
|
||||
export interface InfluxQuery extends DataQuery {
|
||||
policy?: string;
|
||||
measurement?: string;
|
||||
resultFormat?: 'time_series' | 'table';
|
||||
resultFormat?: ResultFormat;
|
||||
orderByTime?: string;
|
||||
tags?: InfluxQueryTag[];
|
||||
groupBy?: InfluxQueryPart[];
|
||||
@@ -52,4 +54,5 @@ export interface InfluxQuery extends DataQuery {
|
||||
fill?: string;
|
||||
rawQuery?: boolean;
|
||||
query?: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user