Skip to content

Commit

Permalink
Add new codemirror-promql-based expression editor (#8634)
Browse files Browse the repository at this point in the history
* Add new codemirror-promql-based expression editor

This adds advanced autocompletion, syntax highlighting, and linting
for PromQL.

Fixes #6160
Fixes #5421

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Group new editor options and float them left

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Improve history autocompletion handling

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Only show info tooltips for unabbreviated completion items

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Rename "new editor" to "experimental editor"

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Add path prefix support

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Revert accidental check-in of go.sum changes

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove spurious console.log

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Fix completion item type icon styling

Signed-off-by: Julius Volz <julius.volz@gmail.com>
  • Loading branch information
juliusv committed Mar 23, 2021
1 parent c7e525b commit faacb61
Show file tree
Hide file tree
Showing 13 changed files with 1,005 additions and 71 deletions.
5 changes: 5 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ https://github.com/dgryski/go-tsz
Copyright (c) 2015,2016 Damian Gryski <damian@gryski.com>
See https://github.com/dgryski/go-tsz/blob/master/LICENSE for license details.

The Codicon icon font from Microsoft
https://github.com/microsoft/vscode-codicons
Copyright (c) Microsoft Corporation and other contributors
See https://github.com/microsoft/vscode-codicons/blob/main/LICENSE for license details.

We also use code from a large number of npm packages. For details, see:
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package.json
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package-lock.json
Expand Down
19 changes: 18 additions & 1 deletion web/ui/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^0.18.3",
"@codemirror/closebrackets": "^0.18.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/comment": "^0.18.0",
"@codemirror/highlight": "^0.18.3",
"@codemirror/history": "^0.18.0",
"@codemirror/language": "^0.18.0",
"@codemirror/lint": "^0.18.1",
"@codemirror/matchbrackets": "^0.18.0",
"@codemirror/search": "^0.18.2",
"@codemirror/state": "^0.18.2",
"@codemirror/view": "^0.18.3",
"@fortawesome/fontawesome-svg-core": "^1.2.14",
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@reach/router": "^1.2.1",
"@testing-library/react-hooks": "^3.1.1",
"@types/jest": "^26.0.10",
"@types/jquery": "^3.5.1",
"@types/node": "^12.11.1",
Expand All @@ -18,6 +29,7 @@
"@types/react-resize-detector": "^5.0.0",
"@types/sanitize-html": "^1.20.2",
"bootstrap": "^4.2.1",
"codemirror-promql": "^0.13.0",
"css.escape": "^1.5.1",
"downshift": "^3.4.8",
"enzyme-to-json": "^3.4.3",
Expand Down Expand Up @@ -63,6 +75,7 @@
"not op_mini all"
],
"devDependencies": {
"@testing-library/react-hooks": "^3.1.1",
"@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/flot": "0.0.31",
Expand All @@ -83,13 +96,17 @@
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "2.x",
"jest-fetch-mock": "^3.0.3",
"mutationobserver-shim": "^0.3.7",
"prettier": "^1.18.2",
"sinon": "^9.0.3"
},
"proxy": "http://localhost:9090",
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"transformIgnorePatterns": [
"/node_modules/(?!codemirror-promql).+(js|jsx)$"
]
}
}
15 changes: 11 additions & 4 deletions web/ui/react-app/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,17 @@ input[type='checkbox']:checked + label {
margin-bottom: 10px;
}

.expression-input textarea {
/* font-family: Menlo,Monaco,Consolas,'Courier New',monospace; */
resize: none;
overflow: hidden;
.expression-input .cm-expression-input {
border: 1px solid #ced4da;
flex: 1 1 auto;
padding: 4px 0 0 8px;
font-size: 15px;
}

/* Font used for autocompletion item icons. */
@font-face {
font-family: 'codicon';
src: local('codicon'), url(./fonts/codicon.ttf) format('truetype');
}

button.execute-btn {
Expand Down
Binary file added web/ui/react-app/src/fonts/codicon.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions web/ui/react-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
import './fonts/codicon.ttf';
import { isPresent } from './utils';

// Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
Expand Down
72 changes: 72 additions & 0 deletions web/ui/react-app/src/pages/graph/CMExpressionInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import CMExpressionInput from './CMExpressionInput';
import { Button, InputGroup, InputGroupAddon, Input } 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);
});
});

0 comments on commit faacb61

Please sign in to comment.