Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new codemirror-promql-based expression editor #8634

Merged
merged 9 commits into from Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions NOTICE
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
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
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
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
@@ -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);
});
});