mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-24 21:24:05 -08:00
remove old promql editor (#9452)
* remove old promql editor Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * rename CMExpression by Expression Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
parent
703d9bcd56
commit
a4ad290987
|
@ -1,69 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import CMExpressionInput from './CMExpressionInput';
|
||||
import { Button, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
describe('CMExpressionInput', () => {
|
||||
const expressionInputProps = {
|
||||
value: 'node_cpu',
|
||||
queryHistory: [],
|
||||
metricNames: [],
|
||||
executeQuery: (): void => {
|
||||
// Do nothing.
|
||||
},
|
||||
onExpressionChange: (): void => {
|
||||
// Do nothing.
|
||||
},
|
||||
loading: false,
|
||||
enableAutocomplete: true,
|
||||
enableHighlighting: true,
|
||||
enableLinter: true,
|
||||
};
|
||||
|
||||
let expressionInput: ReactWrapper;
|
||||
beforeEach(() => {
|
||||
expressionInput = mount(<CMExpressionInput {...expressionInputProps} />);
|
||||
});
|
||||
|
||||
it('renders an InputGroup', () => {
|
||||
const inputGroup = expressionInput.find(InputGroup);
|
||||
expect(inputGroup.prop('className')).toEqual('expression-input');
|
||||
});
|
||||
|
||||
it('renders a search icon when it is not loading', () => {
|
||||
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||
const icon = addon.find(FontAwesomeIcon);
|
||||
expect(icon.prop('icon')).toEqual(faSearch);
|
||||
});
|
||||
|
||||
it('renders a loading icon when it is loading', () => {
|
||||
const expressionInput = mount(<CMExpressionInput {...expressionInputProps} loading={true} />);
|
||||
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||
const icon = addon.find(FontAwesomeIcon);
|
||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
||||
expect(icon.prop('spin')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a CodeMirror expression input', () => {
|
||||
const input = expressionInput.find('div.cm-expression-input');
|
||||
expect(input.text()).toContain('node_cpu');
|
||||
});
|
||||
|
||||
it('renders an execute button', () => {
|
||||
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'append');
|
||||
const button = addon.find(Button).find('.execute-btn').first();
|
||||
expect(button.prop('color')).toEqual('primary');
|
||||
expect(button.text()).toEqual('Execute');
|
||||
});
|
||||
|
||||
it('executes the query when clicking the execute button', () => {
|
||||
const spyExecuteQuery = jest.fn();
|
||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||
const wrapper = mount(<CMExpressionInput {...props} />);
|
||||
const btn = wrapper.find(Button).filterWhere((btn) => btn.hasClass('execute-btn'));
|
||||
btn.simulate('click');
|
||||
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,249 +0,0 @@
|
|||
import React, { FC, useState, useEffect, useRef } from 'react';
|
||||
import { Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
|
||||
import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
|
||||
import { EditorState, Prec, Compartment } from '@codemirror/state';
|
||||
import { indentOnInput, syntaxTree } from '@codemirror/language';
|
||||
import { history, historyKeymap } from '@codemirror/history';
|
||||
import { defaultKeymap, insertNewlineAndIndent } from '@codemirror/commands';
|
||||
import { bracketMatching } from '@codemirror/matchbrackets';
|
||||
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
|
||||
import { highlightSelectionMatches } from '@codemirror/search';
|
||||
import { commentKeymap } from '@codemirror/comment';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { autocompletion, completionKeymap, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
|
||||
import MetricsExplorer from './MetricsExplorer';
|
||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { CompleteStrategy, PromQLExtension } from 'codemirror-promql';
|
||||
import { newCompleteStrategy } from 'codemirror-promql/dist/cjs/complete';
|
||||
|
||||
const promqlExtension = new PromQLExtension();
|
||||
|
||||
interface CMExpressionInputProps {
|
||||
value: string;
|
||||
onExpressionChange: (expr: string) => void;
|
||||
queryHistory: string[];
|
||||
metricNames: string[];
|
||||
executeQuery: () => void;
|
||||
loading: boolean;
|
||||
enableAutocomplete: boolean;
|
||||
enableHighlighting: boolean;
|
||||
enableLinter: boolean;
|
||||
}
|
||||
|
||||
const dynamicConfigCompartment = new Compartment();
|
||||
|
||||
// Autocompletion strategy that wraps the main one and enriches
|
||||
// it with past query items.
|
||||
export class HistoryCompleteStrategy implements CompleteStrategy {
|
||||
private complete: CompleteStrategy;
|
||||
private queryHistory: string[];
|
||||
constructor(complete: CompleteStrategy, queryHistory: string[]) {
|
||||
this.complete = complete;
|
||||
this.queryHistory = queryHistory;
|
||||
}
|
||||
|
||||
promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
|
||||
return Promise.resolve(this.complete.promQL(context)).then((res) => {
|
||||
const { state, pos } = context;
|
||||
const tree = syntaxTree(state).resolve(pos, -1);
|
||||
const start = res != null ? res.from : tree.from;
|
||||
|
||||
if (start !== 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const historyItems: CompletionResult = {
|
||||
from: start,
|
||||
to: pos,
|
||||
options: this.queryHistory.map((q) => ({
|
||||
label: q.length < 80 ? q : q.slice(0, 76).concat('...'),
|
||||
detail: 'past query',
|
||||
apply: q,
|
||||
info: q.length < 80 ? undefined : q,
|
||||
})),
|
||||
span: /^[a-zA-Z0-9_:]+$/,
|
||||
};
|
||||
|
||||
if (res !== null) {
|
||||
historyItems.options = historyItems.options.concat(res.options);
|
||||
}
|
||||
return historyItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CMExpressionInput: FC<CMExpressionInputProps> = ({
|
||||
value,
|
||||
onExpressionChange,
|
||||
queryHistory,
|
||||
metricNames,
|
||||
executeQuery,
|
||||
loading,
|
||||
enableAutocomplete,
|
||||
enableHighlighting,
|
||||
enableLinter,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const [showMetricsExplorer, setShowMetricsExplorer] = useState<boolean>(false);
|
||||
const pathPrefix = usePathPrefix();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// (Re)initialize editor based on settings / setting changes.
|
||||
useEffect(() => {
|
||||
// Build the dynamic part of the config.
|
||||
promqlExtension
|
||||
.activateCompletion(enableAutocomplete)
|
||||
.activateLinter(enableLinter)
|
||||
.setComplete({
|
||||
completeStrategy: new HistoryCompleteStrategy(
|
||||
newCompleteStrategy({
|
||||
remote: { url: pathPrefix, cache: { initialMetricList: metricNames } },
|
||||
}),
|
||||
queryHistory
|
||||
),
|
||||
});
|
||||
const dynamicConfig = [
|
||||
enableHighlighting ? promqlHighlighter : [],
|
||||
promqlExtension.asExtension(),
|
||||
theme === 'dark' ? darkTheme : lightTheme,
|
||||
];
|
||||
|
||||
// Create or reconfigure the editor.
|
||||
const view = viewRef.current;
|
||||
if (view === null) {
|
||||
// If the editor does not exist yet, create it.
|
||||
if (!containerRef.current) {
|
||||
throw new Error('expected CodeMirror container element to exist');
|
||||
}
|
||||
|
||||
const startState = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
baseTheme,
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
highlightSelectionMatches(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...commentKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
]),
|
||||
placeholder('Expression (press Shift+Enter for newlines)'),
|
||||
dynamicConfigCompartment.of(dynamicConfig),
|
||||
// This keymap is added without precedence so that closing the autocomplete dropdown
|
||||
// via Escape works without blurring the editor.
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Escape',
|
||||
run: (v: EditorView): boolean => {
|
||||
v.contentDOM.blur();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]),
|
||||
Prec.override(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Enter',
|
||||
run: (v: EditorView): boolean => {
|
||||
executeQuery();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Shift-Enter',
|
||||
run: insertNewlineAndIndent,
|
||||
},
|
||||
])
|
||||
),
|
||||
EditorView.updateListener.of((update: ViewUpdate): void => {
|
||||
onExpressionChange(update.state.doc.toString());
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state: startState,
|
||||
parent: containerRef.current,
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
view.focus();
|
||||
} else {
|
||||
// The editor already exists, just reconfigure the dynamically configured parts.
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
effects: dynamicConfigCompartment.reconfigure(dynamicConfig),
|
||||
})
|
||||
);
|
||||
}
|
||||
// "value" is only used in the initial render, so we don't want to
|
||||
// re-run this effect every time that "value" changes.
|
||||
//
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory, theme]);
|
||||
|
||||
const insertAtCursor = (value: string) => {
|
||||
const view = viewRef.current;
|
||||
if (view === null) {
|
||||
return;
|
||||
}
|
||||
const { from, to } = view.state.selection.ranges[0];
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
changes: { from, to, insert: value },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputGroup className="expression-input">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>
|
||||
{loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<div ref={containerRef} className="cm-expression-input" />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
className="metrics-explorer-btn"
|
||||
title="Open metrics explorer"
|
||||
onClick={() => setShowMetricsExplorer(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobeEurope} />
|
||||
</Button>
|
||||
<Button className="execute-btn" color="primary" onClick={executeQuery}>
|
||||
Execute
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<MetricsExplorer
|
||||
show={showMetricsExplorer}
|
||||
updateShow={setShowMetricsExplorer}
|
||||
metrics={metricNames}
|
||||
insertAtCursor={insertAtCursor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CMExpressionInput;
|
|
@ -1,26 +1,15 @@
|
|||
import * as React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import ExpressionInput from './ExpressionInput';
|
||||
import Downshift from 'downshift';
|
||||
import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
||||
import { Button, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const getKeyEvent = (key: string): React.KeyboardEvent<HTMLInputElement> =>
|
||||
({
|
||||
key,
|
||||
nativeEvent: {},
|
||||
preventDefault: () => {
|
||||
// Do nothing.
|
||||
},
|
||||
} as React.KeyboardEvent<HTMLInputElement>);
|
||||
|
||||
describe('ExpressionInput', () => {
|
||||
const metricNames = ['instance:node_cpu_utilisation:rate1m', 'node_cpu_guest_seconds_total', 'node_cpu_seconds_total'];
|
||||
const expressionInputProps = {
|
||||
value: 'node_cpu',
|
||||
queryHistory: [],
|
||||
metricNames,
|
||||
metricNames: [],
|
||||
executeQuery: (): void => {
|
||||
// Do nothing.
|
||||
},
|
||||
|
@ -29,6 +18,8 @@ describe('ExpressionInput', () => {
|
|||
},
|
||||
loading: false,
|
||||
enableAutocomplete: true,
|
||||
enableHighlighting: true,
|
||||
enableLinter: true,
|
||||
};
|
||||
|
||||
let expressionInput: ReactWrapper;
|
||||
|
@ -36,11 +27,6 @@ describe('ExpressionInput', () => {
|
|||
expressionInput = mount(<ExpressionInput {...expressionInputProps} />);
|
||||
});
|
||||
|
||||
it('renders a downshift component', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
expect(downshift).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders an InputGroup', () => {
|
||||
const inputGroup = expressionInput.find(InputGroup);
|
||||
expect(inputGroup.prop('className')).toEqual('expression-input');
|
||||
|
@ -60,65 +46,19 @@ describe('ExpressionInput', () => {
|
|||
expect(icon.prop('spin')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an Input', () => {
|
||||
const input = expressionInput.find(Input);
|
||||
expect(input.prop('style')).toEqual({ height: 0 });
|
||||
expect(input.prop('autoFocus')).toEqual(true);
|
||||
expect(input.prop('type')).toEqual('textarea');
|
||||
expect(input.prop('rows')).toEqual('1');
|
||||
expect(input.prop('placeholder')).toEqual('Expression (press Shift+Enter for newlines)');
|
||||
expect(input.prop('value')).toEqual('node_cpu');
|
||||
it('renders a CodeMirror expression input', () => {
|
||||
const input = expressionInput.find('div.cm-expression-input');
|
||||
expect(input.text()).toContain('node_cpu');
|
||||
});
|
||||
|
||||
describe('when autosuggest is closed', () => {
|
||||
it('prevents Downshift default on Home, End, Arrows', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
const input = downshift.find(Input);
|
||||
downshift.setState({ isOpen: false });
|
||||
['Home', 'End', 'ArrowUp', 'ArrowDown'].forEach((key) => {
|
||||
const event = getKeyEvent(key);
|
||||
input.simulate('keydown', event);
|
||||
const nativeEvent = event.nativeEvent as any;
|
||||
expect(nativeEvent.preventDownshiftDefault).toBe(true);
|
||||
});
|
||||
it('renders an execute button', () => {
|
||||
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'append');
|
||||
const button = addon.find(Button).find('.execute-btn').first();
|
||||
expect(button.prop('color')).toEqual('primary');
|
||||
expect(button.text()).toEqual('Execute');
|
||||
});
|
||||
|
||||
it('does not render an autosuggest', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
downshift.setState({ isOpen: false });
|
||||
const ul = downshift.find('ul');
|
||||
expect(ul).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleInput', () => {
|
||||
it('should call setState', () => {
|
||||
const instance: any = expressionInput.instance();
|
||||
const stateSpy = jest.spyOn(instance, 'setState');
|
||||
instance.handleInput();
|
||||
expect(stateSpy).toHaveBeenCalled();
|
||||
});
|
||||
it('should call onExpressionChange', () => {
|
||||
const spyOnExpressionChange = jest.fn();
|
||||
const props = { ...expressionInputProps, onExpressionChange: spyOnExpressionChange };
|
||||
const wrapper = mount(<ExpressionInput {...props} />);
|
||||
const input = wrapper.find(Input);
|
||||
input.simulate('input', { target: { value: 'prometheus_engine_' } });
|
||||
expect(spyOnExpressionChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSelect', () => {
|
||||
it('should call setState with selected value', () => {
|
||||
const instance: any = expressionInput.instance();
|
||||
const stateSpy = jest.spyOn(instance, 'setState');
|
||||
instance.setValue('foo');
|
||||
expect(stateSpy).toHaveBeenCalledWith({ height: 'auto' }, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClick', () => {
|
||||
it('executes the query', () => {
|
||||
it('executes the query when clicking the execute button', () => {
|
||||
const spyExecuteQuery = jest.fn();
|
||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||
const wrapper = mount(<ExpressionInput {...props} />);
|
||||
|
@ -126,139 +66,4 @@ describe('ExpressionInput', () => {
|
|||
btn.simulate('click');
|
||||
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleKeyPress', () => {
|
||||
it('should call executeQuery on Enter key pressed', () => {
|
||||
const spyExecuteQuery = jest.fn();
|
||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||
const input = mount(<ExpressionInput {...props} />);
|
||||
const instance: any = input.instance();
|
||||
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter' });
|
||||
expect(spyExecuteQuery).toHaveBeenCalled();
|
||||
});
|
||||
it('should NOT call executeQuery on Enter + Shift', () => {
|
||||
const spyExecuteQuery = jest.fn();
|
||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||
const input = mount(<ExpressionInput {...props} />);
|
||||
const instance: any = input.instance();
|
||||
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter', shiftKey: true });
|
||||
expect(spyExecuteQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSearchMatches', () => {
|
||||
it('should return matched value', () => {
|
||||
const instance: any = expressionInput.instance();
|
||||
expect(instance.getSearchMatches('foo', ['barfoobaz', 'bazasdbaz'])).toHaveLength(1);
|
||||
});
|
||||
it('should return empty array if no match found', () => {
|
||||
const instance: any = expressionInput.instance();
|
||||
expect(instance.getSearchMatches('foo', ['barbaz', 'bazasdbaz'])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAutocompleteSection', () => {
|
||||
const props = {
|
||||
...expressionInputProps,
|
||||
metricNames: ['foo', 'bar', 'baz'],
|
||||
};
|
||||
|
||||
it('should close menu if no matches found', () => {
|
||||
const input = mount(<ExpressionInput {...props} />);
|
||||
const instance: any = input.instance();
|
||||
const spyCloseMenu = jest.fn();
|
||||
instance.createAutocompleteSection({ inputValue: 'qqqqqq', closeMenu: spyCloseMenu });
|
||||
setTimeout(() => {
|
||||
expect(spyCloseMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should not render list if inputValue not exist', () => {
|
||||
const input = mount(<ExpressionInput {...props} />);
|
||||
const instance: any = input.instance();
|
||||
const spyCloseMenu = jest.fn();
|
||||
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
||||
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
||||
});
|
||||
it('should not render list if enableAutocomplete is false', () => {
|
||||
const input = mount(<ExpressionInput {...props} enableAutocomplete={false} />);
|
||||
const instance: any = input.instance();
|
||||
const spyCloseMenu = jest.fn();
|
||||
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
||||
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
||||
});
|
||||
it('should render autosuggest-dropdown', () => {
|
||||
const input = mount(<ExpressionInput {...props} enableAutocomplete={true} />);
|
||||
const instance: any = input.instance();
|
||||
const spyGetMenuProps = jest.fn();
|
||||
const sections = instance.createAutocompleteSection({
|
||||
inputValue: 'foo',
|
||||
highlightedIndex: 0,
|
||||
getMenuProps: spyGetMenuProps,
|
||||
getItemProps: jest.fn,
|
||||
});
|
||||
expect(sections.props.className).toEqual('autosuggest-dropdown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when downshift is open', () => {
|
||||
it('closes the menu on "Enter"', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
const input = downshift.find(Input);
|
||||
downshift.setState({ isOpen: true });
|
||||
const event = getKeyEvent('Enter');
|
||||
input.simulate('keydown', event);
|
||||
expect(downshift.state('isOpen')).toBe(false);
|
||||
});
|
||||
|
||||
it('should blur input on escape', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
const instance: any = expressionInput.instance();
|
||||
const spyBlur = jest.spyOn(instance.exprInputRef.current, 'blur');
|
||||
const input = downshift.find(Input);
|
||||
downshift.setState({ isOpen: false });
|
||||
const event = getKeyEvent('Escape');
|
||||
input.simulate('keydown', event);
|
||||
expect(spyBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('noops on ArrowUp or ArrowDown', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
const input = downshift.find(Input);
|
||||
downshift.setState({ isOpen: true });
|
||||
['ArrowUp', 'ArrowDown'].forEach((key) => {
|
||||
const event = getKeyEvent(key);
|
||||
input.simulate('keydown', event);
|
||||
const nativeEvent = event.nativeEvent as any;
|
||||
expect(nativeEvent.preventDownshiftDefault).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render an autosuggest if there are no matches', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
downshift.setState({ isOpen: true });
|
||||
const ul = downshift.find('ul');
|
||||
expect(ul).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders an autosuggest if there are matches', () => {
|
||||
const downshift = expressionInput.find(Downshift);
|
||||
downshift.setState({ isOpen: true });
|
||||
setTimeout(() => {
|
||||
const ul = downshift.find('ul');
|
||||
expect(ul.prop('className')).toEqual('card list-group');
|
||||
const items = ul.find('li');
|
||||
expect(items.map((item) => item.text()).join(', ')).toEqual(
|
||||
'node_cpu_guest_seconds_total, node_cpu_seconds_total, instance:node_cpu_utilisation:rate1m'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders an execute Button', () => {
|
||||
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'append');
|
||||
const button = addon.find(Button).find('.execute-btn').first();
|
||||
expect(button.prop('color')).toEqual('primary');
|
||||
expect(button.text()).toEqual('Execute');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Button, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
import React, { FC, useState, useEffect, useRef } from 'react';
|
||||
import { Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
|
||||
import Downshift, { ControllerStateAndHelpers } from 'downshift';
|
||||
import sanitizeHTML from 'sanitize-html';
|
||||
import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
|
||||
import { EditorState, Prec, Compartment } from '@codemirror/state';
|
||||
import { indentOnInput, syntaxTree } from '@codemirror/language';
|
||||
import { history, historyKeymap } from '@codemirror/history';
|
||||
import { defaultKeymap, insertNewlineAndIndent } from '@codemirror/commands';
|
||||
import { bracketMatching } from '@codemirror/matchbrackets';
|
||||
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
|
||||
import { highlightSelectionMatches } from '@codemirror/search';
|
||||
import { commentKeymap } from '@codemirror/comment';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { autocompletion, completionKeymap, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGlobeEurope, faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
|
||||
import MetricsExplorer from './MetricsExplorer';
|
||||
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
|
||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { CompleteStrategy, PromQLExtension } from 'codemirror-promql';
|
||||
import { newCompleteStrategy } from 'codemirror-promql/dist/cjs/complete';
|
||||
|
||||
interface ExpressionInputProps {
|
||||
const promqlExtension = new PromQLExtension();
|
||||
|
||||
interface CMExpressionInputProps {
|
||||
value: string;
|
||||
onExpressionChange: (expr: string) => void;
|
||||
queryHistory: string[];
|
||||
|
@ -17,235 +32,218 @@ interface ExpressionInputProps {
|
|||
executeQuery: () => void;
|
||||
loading: boolean;
|
||||
enableAutocomplete: boolean;
|
||||
enableHighlighting: boolean;
|
||||
enableLinter: boolean;
|
||||
}
|
||||
|
||||
interface ExpressionInputState {
|
||||
height: number | string;
|
||||
showMetricsExplorer: boolean;
|
||||
}
|
||||
const dynamicConfigCompartment = new Compartment();
|
||||
|
||||
const fuz = new Fuzzy({ pre: '<strong>', post: '</strong>', shouldSort: true });
|
||||
|
||||
class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
|
||||
private exprInputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: ExpressionInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: 'auto',
|
||||
showMetricsExplorer: false,
|
||||
};
|
||||
// Autocompletion strategy that wraps the main one and enriches
|
||||
// it with past query items.
|
||||
export class HistoryCompleteStrategy implements CompleteStrategy {
|
||||
private complete: CompleteStrategy;
|
||||
private queryHistory: string[];
|
||||
constructor(complete: CompleteStrategy, queryHistory: string[]) {
|
||||
this.complete = complete;
|
||||
this.queryHistory = queryHistory;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.setHeight();
|
||||
promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
|
||||
return Promise.resolve(this.complete.promQL(context)).then((res) => {
|
||||
const { state, pos } = context;
|
||||
const tree = syntaxTree(state).resolve(pos, -1);
|
||||
const start = res != null ? res.from : tree.from;
|
||||
|
||||
if (start !== 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
setHeight = (): void => {
|
||||
if (this.exprInputRef.current) {
|
||||
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current;
|
||||
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
|
||||
this.setState({ height: scrollHeight + offset });
|
||||
const historyItems: CompletionResult = {
|
||||
from: start,
|
||||
to: pos,
|
||||
options: this.queryHistory.map((q) => ({
|
||||
label: q.length < 80 ? q : q.slice(0, 76).concat('...'),
|
||||
detail: 'past query',
|
||||
apply: q,
|
||||
info: q.length < 80 ? undefined : q,
|
||||
})),
|
||||
span: /^[a-zA-Z0-9_:]+$/,
|
||||
};
|
||||
|
||||
if (res !== null) {
|
||||
historyItems.options = historyItems.options.concat(res.options);
|
||||
}
|
||||
};
|
||||
|
||||
handleInput = (): void => {
|
||||
if (this.exprInputRef.current) {
|
||||
this.setValue(this.exprInputRef.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
setValue = (value: string): void => {
|
||||
const { onExpressionChange } = this.props;
|
||||
onExpressionChange(value);
|
||||
this.setState({ height: 'auto' }, this.setHeight);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: ExpressionInputProps): void {
|
||||
const { value } = this.props;
|
||||
if (value !== prevProps.value) {
|
||||
this.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
const { executeQuery } = this.props;
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
executeQuery();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
getSearchMatches = (input: string, expressions: string[]): FuzzyResult[] => {
|
||||
return fuz.filter(input.replace(/ /g, ''), expressions);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>): JSX.Element | null => {
|
||||
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
||||
const autocompleteSections = {
|
||||
'Query History': this.props.queryHistory,
|
||||
'Metric Names': this.props.metricNames,
|
||||
};
|
||||
let index = 0;
|
||||
const sections =
|
||||
inputValue?.length && this.props.enableAutocomplete
|
||||
? Object.entries(autocompleteSections).reduce((acc, [title, items]) => {
|
||||
const matches = this.getSearchMatches(inputValue, items);
|
||||
return !matches.length
|
||||
? acc
|
||||
: [
|
||||
...acc,
|
||||
<ul className="autosuggest-dropdown-list" key={title}>
|
||||
<li className="autosuggest-dropdown-header">{title}</li>
|
||||
{matches
|
||||
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is slow.
|
||||
.map((result: FuzzyResult) => {
|
||||
const itemProps = downshift.getItemProps({
|
||||
key: result.original,
|
||||
index,
|
||||
item: result.original,
|
||||
style: {
|
||||
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
|
||||
},
|
||||
return historyItems;
|
||||
});
|
||||
return (
|
||||
<li
|
||||
key={title}
|
||||
{...itemProps}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeHTML(result.rendered, { allowedTags: ['strong'] }) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>,
|
||||
}
|
||||
}
|
||||
|
||||
const ExpressionInput: FC<CMExpressionInputProps> = ({
|
||||
value,
|
||||
onExpressionChange,
|
||||
queryHistory,
|
||||
metricNames,
|
||||
executeQuery,
|
||||
loading,
|
||||
enableAutocomplete,
|
||||
enableHighlighting,
|
||||
enableLinter,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const [showMetricsExplorer, setShowMetricsExplorer] = useState<boolean>(false);
|
||||
const pathPrefix = usePathPrefix();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// (Re)initialize editor based on settings / setting changes.
|
||||
useEffect(() => {
|
||||
// Build the dynamic part of the config.
|
||||
promqlExtension
|
||||
.activateCompletion(enableAutocomplete)
|
||||
.activateLinter(enableLinter)
|
||||
.setComplete({
|
||||
completeStrategy: new HistoryCompleteStrategy(
|
||||
newCompleteStrategy({
|
||||
remote: { url: pathPrefix, cache: { initialMetricList: metricNames } },
|
||||
}),
|
||||
queryHistory
|
||||
),
|
||||
});
|
||||
const dynamicConfig = [
|
||||
enableHighlighting ? promqlHighlighter : [],
|
||||
promqlExtension.asExtension(),
|
||||
theme === 'dark' ? darkTheme : lightTheme,
|
||||
];
|
||||
}, [] as JSX.Element[])
|
||||
: [];
|
||||
|
||||
if (!sections.length) {
|
||||
// This is ugly but is needed in order to sync state updates.
|
||||
// This way we force downshift to wait React render call to complete before closeMenu to be triggered.
|
||||
setTimeout(closeMenu);
|
||||
return null;
|
||||
// Create or reconfigure the editor.
|
||||
const view = viewRef.current;
|
||||
if (view === null) {
|
||||
// If the editor does not exist yet, create it.
|
||||
if (!containerRef.current) {
|
||||
throw new Error('expected CodeMirror container element to exist');
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...downshift.getMenuProps()} className="autosuggest-dropdown">
|
||||
{sections}
|
||||
</div>
|
||||
const startState = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
baseTheme,
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
highlightSelectionMatches(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...commentKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
]),
|
||||
placeholder('Expression (press Shift+Enter for newlines)'),
|
||||
dynamicConfigCompartment.of(dynamicConfig),
|
||||
// This keymap is added without precedence so that closing the autocomplete dropdown
|
||||
// via Escape works without blurring the editor.
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Escape',
|
||||
run: (v: EditorView): boolean => {
|
||||
v.contentDOM.blur();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]),
|
||||
Prec.override(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Enter',
|
||||
run: (v: EditorView): boolean => {
|
||||
executeQuery();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Shift-Enter',
|
||||
run: insertNewlineAndIndent,
|
||||
},
|
||||
])
|
||||
),
|
||||
EditorView.updateListener.of((update: ViewUpdate): void => {
|
||||
onExpressionChange(update.state.doc.toString());
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state: startState,
|
||||
parent: containerRef.current,
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
view.focus();
|
||||
} else {
|
||||
// The editor already exists, just reconfigure the dynamically configured parts.
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
effects: dynamicConfigCompartment.reconfigure(dynamicConfig),
|
||||
})
|
||||
);
|
||||
}
|
||||
// "value" is only used in the initial render, so we don't want to
|
||||
// re-run this effect every time that "value" changes.
|
||||
//
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory, theme]);
|
||||
|
||||
const insertAtCursor = (value: string) => {
|
||||
const view = viewRef.current;
|
||||
if (view === null) {
|
||||
return;
|
||||
}
|
||||
const { from, to } = view.state.selection.ranges[0];
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
changes: { from, to, insert: value },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
openMetricsExplorer = (): void => {
|
||||
this.setState({
|
||||
showMetricsExplorer: true,
|
||||
});
|
||||
};
|
||||
|
||||
updateShowMetricsExplorer = (show: boolean): void => {
|
||||
this.setState({
|
||||
showMetricsExplorer: show,
|
||||
});
|
||||
};
|
||||
|
||||
insertAtCursor = (value: string): void => {
|
||||
if (!this.exprInputRef.current) return;
|
||||
|
||||
const startPosition = this.exprInputRef.current.selectionStart;
|
||||
const endPosition = this.exprInputRef.current.selectionEnd;
|
||||
|
||||
const previousValue = this.exprInputRef.current.value;
|
||||
let newValue: string;
|
||||
if (startPosition && endPosition) {
|
||||
newValue =
|
||||
previousValue.substring(0, startPosition) + value + previousValue.substring(endPosition, previousValue.length);
|
||||
} else {
|
||||
newValue = previousValue + value;
|
||||
}
|
||||
|
||||
this.setValue(newValue);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { executeQuery, value } = this.props;
|
||||
const { height } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Downshift onSelect={this.setValue}>
|
||||
{(downshift) => (
|
||||
<div>
|
||||
<InputGroup className="expression-input">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>
|
||||
{this.props.loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
|
||||
{loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
onInput={this.handleInput}
|
||||
style={{ height }}
|
||||
autoFocus
|
||||
type="textarea"
|
||||
rows="1"
|
||||
onKeyPress={this.handleKeyPress}
|
||||
placeholder="Expression (press Shift+Enter for newlines)"
|
||||
innerRef={this.exprInputRef}
|
||||
{...downshift.getInputProps({
|
||||
onKeyDown: (event: React.KeyboardEvent): void => {
|
||||
switch (event.key) {
|
||||
case 'Home':
|
||||
case 'End':
|
||||
// We want to be able to jump to the beginning/end of the input field.
|
||||
// By default, Downshift otherwise jumps to the first/last suggestion item instead.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
if (!downshift.isOpen) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
downshift.closeMenu();
|
||||
break;
|
||||
case 'Escape':
|
||||
if (!downshift.isOpen && this.exprInputRef.current) {
|
||||
this.exprInputRef.current.blur();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any)}
|
||||
value={value}
|
||||
/>
|
||||
<div ref={containerRef} className="cm-expression-input" />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button className="metrics-explorer-btn" title="Open metrics explorer" onClick={this.openMetricsExplorer}>
|
||||
<Button
|
||||
className="metrics-explorer-btn"
|
||||
title="Open metrics explorer"
|
||||
onClick={() => setShowMetricsExplorer(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobeEurope} />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button className="execute-btn" color="primary" onClick={executeQuery}>
|
||||
Execute
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
{downshift.isOpen && this.createAutocompleteSection(downshift)}
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
||||
<MetricsExplorer
|
||||
show={this.state.showMetricsExplorer}
|
||||
updateShow={this.updateShowMetricsExplorer}
|
||||
metrics={this.props.metricNames}
|
||||
insertAtCursor={this.insertAtCursor}
|
||||
show={showMetricsExplorer}
|
||||
updateShow={setShowMetricsExplorer}
|
||||
metrics={metricNames}
|
||||
insertAtCursor={insertAtCursor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ExpressionInput;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import Panel, { PanelOptions, PanelType } from './Panel';
|
||||
import ExpressionInput from './ExpressionInput';
|
||||
import GraphControls from './GraphControls';
|
||||
import { NavLink, TabPane } from 'reactstrap';
|
||||
import TimeInput from './TimeInput';
|
||||
|
@ -38,17 +37,6 @@ const defaultProps = {
|
|||
describe('Panel', () => {
|
||||
const panel = shallow(<Panel {...defaultProps} />);
|
||||
|
||||
it('renders an ExpressionInput', () => {
|
||||
const input = panel.find(ExpressionInput);
|
||||
expect(input.prop('value')).toEqual('prometheus_engine');
|
||||
expect(input.prop('metricNames')).toEqual([
|
||||
'prometheus_engine_queries',
|
||||
'prometheus_engine_queries_concurrent_max',
|
||||
'prometheus_engine_query_duration_seconds',
|
||||
]);
|
||||
expect(input.prop('queryHistory')).toEqual([]);
|
||||
});
|
||||
|
||||
it('renders NavLinks', () => {
|
||||
const results: PanelOptions[] = [];
|
||||
const onOptionsChanged = (opts: PanelOptions): void => {
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } f
|
|||
import moment from 'moment-timezone';
|
||||
|
||||
import ExpressionInput from './ExpressionInput';
|
||||
import CMExpressionInput from './CMExpressionInput';
|
||||
import GraphControls from './GraphControls';
|
||||
import { GraphTabContent } from './GraphTabContent';
|
||||
import DataTable from './DataTable';
|
||||
|
@ -24,7 +23,6 @@ interface PanelProps {
|
|||
removePanel: () => void;
|
||||
onExecuteQuery: (query: string) => void;
|
||||
pathPrefix: string;
|
||||
useExperimentalEditor: boolean;
|
||||
enableAutocomplete: boolean;
|
||||
enableHighlighting: boolean;
|
||||
enableLinter: boolean;
|
||||
|
@ -272,8 +270,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<div className="panel">
|
||||
<Row>
|
||||
<Col>
|
||||
{this.props.useExperimentalEditor ? (
|
||||
<CMExpressionInput
|
||||
<ExpressionInput
|
||||
value={this.state.exprInputValue}
|
||||
onExpressionChange={this.handleExpressionChange}
|
||||
executeQuery={this.executeQuery}
|
||||
|
@ -284,17 +281,6 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
queryHistory={pastQueries}
|
||||
metricNames={metricNames}
|
||||
/>
|
||||
) : (
|
||||
<ExpressionInput
|
||||
value={this.state.exprInputValue}
|
||||
onExpressionChange={this.handleExpressionChange}
|
||||
executeQuery={this.executeQuery}
|
||||
loading={this.state.loading}
|
||||
enableAutocomplete={this.props.enableAutocomplete}
|
||||
queryHistory={pastQueries}
|
||||
metricNames={metricNames}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
|
|
@ -11,7 +11,6 @@ describe('PanelList', () => {
|
|||
{ id: 'use-local-time-checkbox', label: 'Use local time', default: false },
|
||||
{ id: 'query-history-checkbox', label: 'Enable query history', default: false },
|
||||
{ id: 'autocomplete-checkbox', label: 'Enable autocomplete', default: true },
|
||||
{ id: 'use-experimental-editor-checkbox', label: 'Use experimental editor', default: true },
|
||||
{ id: 'highlighting-checkbox', label: 'Enable highlighting', default: true },
|
||||
{ id: 'linter-checkbox', label: 'Enable linter', default: true },
|
||||
].forEach((cb, idx) => {
|
||||
|
|
|
@ -20,7 +20,6 @@ interface PanelListContentProps {
|
|||
panels: PanelMeta[];
|
||||
metrics: string[];
|
||||
useLocalTime: boolean;
|
||||
useExperimentalEditor: boolean;
|
||||
queryHistoryEnabled: boolean;
|
||||
enableAutocomplete: boolean;
|
||||
enableHighlighting: boolean;
|
||||
|
@ -30,7 +29,6 @@ interface PanelListContentProps {
|
|||
export const PanelListContent: FC<PanelListContentProps> = ({
|
||||
metrics = [],
|
||||
useLocalTime,
|
||||
useExperimentalEditor,
|
||||
queryHistoryEnabled,
|
||||
enableAutocomplete,
|
||||
enableHighlighting,
|
||||
|
@ -105,7 +103,6 @@ export const PanelListContent: FC<PanelListContentProps> = ({
|
|||
)
|
||||
)
|
||||
}
|
||||
useExperimentalEditor={useExperimentalEditor}
|
||||
useLocalTime={useLocalTime}
|
||||
metricNames={metrics}
|
||||
pastQueries={queryHistoryEnabled ? historyItems : []}
|
||||
|
@ -123,7 +120,6 @@ export const PanelListContent: FC<PanelListContentProps> = ({
|
|||
|
||||
const PanelList: FC = () => {
|
||||
const [delta, setDelta] = useState(0);
|
||||
const [useExperimentalEditor, setUseExperimentalEditor] = useLocalStorage('use-new-editor', true);
|
||||
const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false);
|
||||
const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false);
|
||||
const [enableAutocomplete, setEnableAutocomplete] = useLocalStorage('enable-metric-autocomplete', true);
|
||||
|
@ -180,21 +176,11 @@ const PanelList: FC = () => {
|
|||
Enable autocomplete
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className="float-right">
|
||||
<Checkbox
|
||||
wrapperStyles={{ display: 'inline-block' }}
|
||||
id="use-experimental-editor-checkbox"
|
||||
onChange={({ target }) => setUseExperimentalEditor(target.checked)}
|
||||
defaultChecked={useExperimentalEditor}
|
||||
>
|
||||
Use experimental editor
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
|
||||
id="highlighting-checkbox"
|
||||
onChange={({ target }) => setEnableHighlighting(target.checked)}
|
||||
defaultChecked={enableHighlighting}
|
||||
disabled={!useExperimentalEditor}
|
||||
>
|
||||
Enable highlighting
|
||||
</Checkbox>
|
||||
|
@ -203,12 +189,10 @@ const PanelList: FC = () => {
|
|||
id="linter-checkbox"
|
||||
onChange={({ target }) => setEnableLinter(target.checked)}
|
||||
defaultChecked={enableLinter}
|
||||
disabled={!useExperimentalEditor}
|
||||
>
|
||||
Enable linter
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
{(delta > 30 || timeErr) && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning: </strong>
|
||||
|
@ -227,7 +211,6 @@ const PanelList: FC = () => {
|
|||
panels={decodePanelOptionsFromQueryString(window.location.search)}
|
||||
useLocalTime={useLocalTime}
|
||||
metrics={metricsRes.data}
|
||||
useExperimentalEditor={useExperimentalEditor}
|
||||
queryHistoryEnabled={enableQueryHistory}
|
||||
enableAutocomplete={enableAutocomplete}
|
||||
enableHighlighting={enableHighlighting}
|
||||
|
|
Loading…
Reference in a new issue