diff --git a/Makefile b/Makefile
index 3f2b1b0fb4..cad1dd7ad1 100644
--- a/Makefile
+++ b/Makefile
@@ -60,7 +60,7 @@ react-app-lint-fix:
.PHONY: react-app-test
react-app-test: | $(REACT_APP_NODE_MODULES_PATH) react-app-lint
@echo ">> running React app tests"
- cd $(REACT_APP_PATH) && yarn test --no-watch
+ cd $(REACT_APP_PATH) && yarn test --no-watch --coverage
.PHONY: test
test: common-test react-app-test
diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index 0f2296b47c..770bdcd4fe 100644
--- a/web/ui/react-app/package.json
+++ b/web/ui/react-app/package.json
@@ -7,7 +7,7 @@
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@reach/router": "^1.2.1",
- "@types/jest": "^24.0.4",
+ "@types/jest": "^24.0.20",
"@types/jquery": "^3.3.29",
"@types/node": "^12.11.1",
"@types/reach__router": "^1.2.6",
@@ -42,6 +42,7 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
+ "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject",
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
"lint": "eslint --fix \"src/**/*.{ts,tsx}\""
@@ -58,6 +59,8 @@
"not op_mini all"
],
"devDependencies": {
+ "@types/enzyme": "^3.10.3",
+ "@types/enzyme-adapter-react-16": "^1.0.5",
"@types/flot": "0.0.31",
"@types/moment-timezone": "^0.5.10",
"@types/reactstrap": "^8.0.5",
@@ -73,7 +76,11 @@
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.x",
- "prettier": "^1.18.2"
+ "prettier": "^1.18.2",
+ "@types/sinon": "^7.5.0",
+ "enzyme": "^3.10.0",
+ "enzyme-adapter-react-16": "^1.15.1",
+ "sinon": "^7.5.0"
},
"proxy": "http://localhost:9090"
}
diff --git a/web/ui/react-app/src/App.test.js b/web/ui/react-app/src/App.test.js
deleted file mode 100755
index 4378d2a0a7..0000000000
--- a/web/ui/react-app/src/App.test.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import './globals';
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-it('renders without crashing', () => {
- const div = document.createElement('div');
- ReactDOM.render(, div);
- ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/web/ui/react-app/src/App.test.tsx b/web/ui/react-app/src/App.test.tsx
new file mode 100755
index 0000000000..621a52e539
--- /dev/null
+++ b/web/ui/react-app/src/App.test.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import App from './App';
+import Navigation from './Navbar';
+import { Container } from 'reactstrap';
+import { Router } from '@reach/router';
+import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
+
+describe('App', () => {
+ const app = shallow();
+
+ it('navigates', () => {
+ expect(app.find(Navigation)).toHaveLength(1);
+ });
+ it('routes', () => {
+ [Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component =>
+ expect(app.find(component)).toHaveLength(1)
+ );
+ expect(app.find(Router)).toHaveLength(1);
+ expect(app.find(Container)).toHaveLength(1);
+ });
+});
diff --git a/web/ui/react-app/src/Checkbox.test.tsx b/web/ui/react-app/src/Checkbox.test.tsx
new file mode 100755
index 0000000000..52258f72dd
--- /dev/null
+++ b/web/ui/react-app/src/Checkbox.test.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Checkbox from './Checkbox';
+import { FormGroup, Label, Input } from 'reactstrap';
+
+const MockCmp: React.FC = () =>
;
+
+describe('Checkbox', () => {
+ it('renders with subcomponents', () => {
+ const checkBox = shallow();
+ [FormGroup, Input, Label].forEach(component => expect(checkBox.find(component)).toHaveLength(1));
+ });
+
+ it('passes down the correct FormGroup props', () => {
+ const checkBoxProps = { wrapperStyles: { color: 'orange' } };
+ const checkBox = shallow();
+ const formGroup = checkBox.find(FormGroup);
+ expect(Object.keys(formGroup.props())).toHaveLength(4);
+ expect(formGroup.prop('className')).toEqual('custom-control custom-checkbox');
+ expect(formGroup.prop('children')).toHaveLength(2);
+ expect(formGroup.prop('style')).toEqual({ color: 'orange' });
+ expect(formGroup.prop('tag')).toEqual('div');
+ });
+
+ it('passes down the correct FormGroup Input props', () => {
+ const results: string[] = [];
+ const checkBoxProps = {
+ onChange: (): void => {
+ results.push('clicked');
+ },
+ };
+ const checkBox = shallow();
+ const input = checkBox.find(Input);
+ expect(Object.keys(input.props())).toHaveLength(4);
+ expect(input.prop('className')).toEqual('custom-control-input');
+ expect(input.prop('id')).toMatch('1');
+ expect(input.prop('type')).toEqual('checkbox');
+ input.simulate('change');
+ expect(results).toHaveLength(1);
+ expect(results[0]).toEqual('clicked');
+ });
+
+ it('passes down the correct Label props', () => {
+ const checkBox = shallow(
+
+
+
+ );
+ const label = checkBox.find(Label);
+ expect(Object.keys(label.props())).toHaveLength(6);
+ expect(label.prop('className')).toEqual('custom-control-label');
+ expect(label.find(MockCmp)).toHaveLength(1);
+ expect(label.prop('for')).toMatch('1');
+ expect(label.prop('style')).toEqual({ userSelect: 'none' });
+ expect(label.prop('tag')).toEqual('label');
+ });
+
+ it('shares checkbox `id` uuid with Input/Label subcomponents', () => {
+ const checkBox = shallow();
+ const input = checkBox.find(Input);
+ const label = checkBox.find(Label);
+ expect(label.prop('for')).toBeDefined();
+ expect(label.prop('for')).toEqual(input.prop('id'));
+ });
+});
diff --git a/web/ui/react-app/src/DataTable.test.tsx b/web/ui/react-app/src/DataTable.test.tsx
new file mode 100755
index 0000000000..6803406c5d
--- /dev/null
+++ b/web/ui/react-app/src/DataTable.test.tsx
@@ -0,0 +1,267 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import DataTable, { QueryResult } from './DataTable';
+import { Alert, Table } from 'reactstrap';
+import SeriesName from './SeriesName';
+
+describe('DataTable', () => {
+ describe('when data is null', () => {
+ it('renders an alert', () => {
+ const table = shallow();
+ const alert = table.find(Alert);
+ expect(Object.keys(alert.props())).toHaveLength(7);
+ expect(alert.prop('color')).toEqual('light');
+ expect(alert.prop('children')).toEqual('No data queried yet');
+ });
+ });
+
+ describe('when data.result is empty', () => {
+ it('renders an alert', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'vector',
+ result: [],
+ },
+ };
+ const table = shallow();
+ const alert = table.find(Alert);
+ expect(Object.keys(alert.props())).toHaveLength(7);
+ expect(alert.prop('color')).toEqual('secondary');
+ expect(alert.prop('children')).toEqual('Empty query result');
+ });
+ });
+
+ describe('when resultType is a vector with values', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'vector',
+ result: [
+ {
+ metric: {
+ __name__: 'metric_name_1',
+ label1: 'value_1',
+ labeln: 'value_n',
+ },
+ value: [1572098246.599, '0'],
+ },
+ {
+ metric: {
+ __name__: 'metric_name_2',
+ label1: 'value_1',
+ labeln: 'value_n',
+ },
+ value: [1572098246.599, '1'],
+ },
+ ],
+ },
+ };
+ const dataTable = shallow();
+
+ it('renders a table', () => {
+ const table = dataTable.find(Table);
+ expect(table.prop('hover')).toBe(true);
+ expect(table.prop('size')).toEqual('sm');
+ expect(table.prop('className')).toEqual('data-table');
+ expect(table.find('tbody')).toHaveLength(1);
+ });
+
+ it('renders rows', () => {
+ const table = dataTable.find(Table);
+ table.find('tr').forEach((row, idx) => {
+ expect(row.find(SeriesName)).toHaveLength(1);
+ expect(
+ row
+ .find('td')
+ .at(1)
+ .text()
+ ).toEqual(`${idx}`);
+ });
+ });
+ });
+
+ describe('when resultType is a vector with too many values', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'vector',
+ result: Array.from(Array(10001).keys()).map(i => {
+ return {
+ metric: {
+ __name__: `metric_name_${i}`,
+ label1: 'value_1',
+ labeln: 'value_n',
+ },
+ value: [1572098246.599, `${i}`],
+ };
+ }),
+ },
+ };
+ const dataTable = shallow();
+
+ it('renders limited rows', () => {
+ const table = dataTable.find(Table);
+ expect(table.find('tr')).toHaveLength(10000);
+ });
+
+ it('renders a warning', () => {
+ const alert = dataTable.find(Alert);
+ expect(alert.render().text()).toEqual('Warning: Fetched 10001 metrics, only displaying first 10000.');
+ });
+ });
+
+ describe('when result type is a matrix', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {
+ __name__: 'promhttp_metric_handler_requests_total',
+ code: '200',
+ instance: 'localhost:9090',
+ job: 'prometheus',
+ },
+ values: [
+ [1572097950.93, '9'],
+ [1572097965.931, '10'],
+ [1572097980.929, '11'],
+ [1572097995.931, '12'],
+ [1572098010.932, '13'],
+ [1572098025.933, '14'],
+ [1572098040.93, '15'],
+ [1572098055.93, '16'],
+ [1572098070.93, '17'],
+ [1572098085.936, '18'],
+ [1572098100.936, '19'],
+ [1572098115.933, '20'],
+ [1572098130.932, '21'],
+ [1572098145.932, '22'],
+ [1572098160.933, '23'],
+ [1572098175.934, '24'],
+ [1572098190.937, '25'],
+ [1572098205.934, '26'],
+ [1572098220.933, '27'],
+ [1572098235.934, '28'],
+ ],
+ },
+ {
+ metric: {
+ __name__: 'promhttp_metric_handler_requests_total',
+ code: '500',
+ instance: 'localhost:9090',
+ job: 'prometheus',
+ },
+ values: [
+ [1572097950.93, '0'],
+ [1572097965.931, '0'],
+ [1572097980.929, '0'],
+ [1572097995.931, '0'],
+ [1572098010.932, '0'],
+ [1572098025.933, '0'],
+ [1572098040.93, '0'],
+ [1572098055.93, '0'],
+ [1572098070.93, '0'],
+ [1572098085.936, '0'],
+ [1572098100.936, '0'],
+ [1572098115.933, '0'],
+ [1572098130.932, '0'],
+ [1572098145.932, '0'],
+ [1572098160.933, '0'],
+ [1572098175.934, '0'],
+ [1572098190.937, '0'],
+ [1572098205.934, '0'],
+ [1572098220.933, '0'],
+ [1572098235.934, '0'],
+ ],
+ },
+ {
+ metric: {
+ __name__: 'promhttp_metric_handler_requests_total',
+ code: '503',
+ instance: 'localhost:9090',
+ job: 'prometheus',
+ },
+ values: [
+ [1572097950.93, '0'],
+ [1572097965.931, '0'],
+ [1572097980.929, '0'],
+ [1572097995.931, '0'],
+ [1572098010.932, '0'],
+ [1572098025.933, '0'],
+ [1572098040.93, '0'],
+ [1572098055.93, '0'],
+ [1572098070.93, '0'],
+ [1572098085.936, '0'],
+ [1572098100.936, '0'],
+ [1572098115.933, '0'],
+ [1572098130.932, '0'],
+ [1572098145.932, '0'],
+ [1572098160.933, '0'],
+ [1572098175.934, '0'],
+ [1572098190.937, '0'],
+ [1572098205.934, '0'],
+ [1572098220.933, '0'],
+ [1572098235.934, '0'],
+ ],
+ },
+ ],
+ },
+ };
+ const dataTable = shallow();
+ it('renders rows', () => {
+ const table = dataTable.find(Table);
+ const rows = table.find('tr');
+ expect(table.find('tr')).toHaveLength(3);
+ const row = rows.at(0);
+ expect(row.text()).toEqual(`1 @1572097950.93
+1 @1572097965.931
+1 @1572097980.929
+1 @1572097995.931
+1 @1572098010.932
+1 @1572098025.933
+1 @1572098040.93
+1 @1572098055.93
+1 @1572098070.93
+1 @1572098085.936
+1 @1572098100.936
+1 @1572098115.933
+1 @1572098130.932
+1 @1572098145.932
+1 @1572098160.933
+1 @1572098175.934
+1 @1572098190.937
+1 @1572098205.934
+1 @1572098220.933
+1 @1572098235.934`);
+ });
+ });
+
+ describe('when resultType is a scalar', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'scalar',
+ result: [1572098246.599, '5'],
+ },
+ };
+ const dataTable = shallow();
+ it('renders a scalar row', () => {
+ const table = dataTable.find(Table);
+ const rows = table.find('tr');
+ expect(rows.text()).toEqual('scalar5');
+ });
+ });
+
+ describe('when resultType is a string', () => {
+ const dataTableProps: QueryResult = {
+ data: {
+ resultType: 'string',
+ result: 'string',
+ },
+ };
+ const dataTable = shallow();
+ it('renders a string row', () => {
+ const table = dataTable.find(Table);
+ const rows = table.find('tr');
+ expect(rows.text()).toEqual('scalart');
+ });
+ });
+});
diff --git a/web/ui/react-app/src/DataTable.tsx b/web/ui/react-app/src/DataTable.tsx
index b6012bffc9..9f1a52ceb3 100644
--- a/web/ui/react-app/src/DataTable.tsx
+++ b/web/ui/react-app/src/DataTable.tsx
@@ -100,7 +100,7 @@ class DataTable extends PureComponent {
break;
case 'scalar':
rows.push(
-
+
scalar |
{data.result[1]} |
@@ -108,7 +108,7 @@ class DataTable extends PureComponent {
break;
case 'string':
rows.push(
-
+
scalar |
{data.result[1]} |
diff --git a/web/ui/react-app/src/ExpressionInput.test.tsx b/web/ui/react-app/src/ExpressionInput.test.tsx
new file mode 100644
index 0000000000..9c985c7670
--- /dev/null
+++ b/web/ui/react-app/src/ExpressionInput.test.tsx
@@ -0,0 +1,145 @@
+import * as React from 'react';
+import { mount } from 'enzyme';
+import ExpressionInput from './ExpressionInput';
+import Downshift from 'downshift';
+import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
+import SanitizeHTML from './components/SanitizeHTML';
+
+const getKeyEvent = (key: string): React.KeyboardEvent =>
+ ({
+ key,
+ nativeEvent: {},
+ preventDefault: () => {},
+ } as React.KeyboardEvent);
+
+describe('ExpressionInput', () => {
+ const metricNames = ['instance:node_cpu_utilisation:rate1m', 'node_cpu_guest_seconds_total', 'node_cpu_seconds_total'];
+ const expressionInputProps = {
+ value: 'node_cpu',
+ autocompleteSections: {
+ 'Query History': [],
+ 'Metric Names': metricNames,
+ },
+ executeQuery: (): void => {},
+ loading: false,
+ };
+ const expressionInput = mount();
+
+ it('renders a downshift component', () => {
+ const downshift = expressionInput.find(Downshift);
+ expect(downshift.prop('inputValue')).toEqual('node_cpu');
+ });
+
+ 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();
+ 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 an Input', () => {
+ const expressionInput = mount();
+ 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(expressionInput.state('value')).toEqual('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 });
+ const onKeyDown = input.prop('onKeyDown');
+ ['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('does not render an autosuggest', () => {
+ const downshift = expressionInput.find(Downshift);
+ downshift.setState({ isOpen: false });
+ const ul = downshift.find('ul');
+ expect(ul).toHaveLength(0);
+ });
+ });
+
+ 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('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 expressionInputProps = {
+ value: 'foo',
+ autocompleteSections: {
+ 'Query History': [],
+ 'Metric Names': [],
+ },
+ executeQuery: (): void => {},
+ loading: false,
+ };
+ const expressionInput = mount();
+ 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 });
+ const ul = downshift.find('ul');
+ expect(ul.prop('className')).toEqual('card list-group');
+ const items = ul.find(SanitizeHTML);
+ 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);
+ expect(button.prop('className')).toEqual('execute-btn');
+ expect(button.prop('color')).toEqual('primary');
+ expect(button.text()).toEqual('Execute');
+ });
+});
diff --git a/web/ui/react-app/src/Graph.test.tsx b/web/ui/react-app/src/Graph.test.tsx
new file mode 100644
index 0000000000..9fcfff6e53
--- /dev/null
+++ b/web/ui/react-app/src/Graph.test.tsx
@@ -0,0 +1,105 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Graph from './Graph';
+import { Alert } from 'reactstrap';
+import ReactResizeDetector from 'react-resize-detector';
+import Legend from './Legend';
+
+describe('Graph', () => {
+ [
+ {
+ data: null,
+ color: 'light',
+ children: 'No data queried yet',
+ },
+ {
+ data: { resultType: 'invalid' },
+ color: 'danger',
+ children: `Query result is of wrong type '`,
+ },
+ {
+ data: {
+ resultType: 'matrix',
+ result: [],
+ },
+ color: 'secondary',
+ children: 'Empty query result',
+ },
+ ].forEach(testCase => {
+ it(`renders an alert if data is "${testCase.data}"`, () => {
+ const props = {
+ data: testCase.data,
+ stacked: false,
+ queryParams: {
+ startTime: 1572100210000,
+ endTime: 1572100217898,
+ resolution: 10,
+ },
+ };
+ const graph = shallow();
+ const alert = graph.find(Alert);
+ expect(alert.prop('color')).toEqual(testCase.color);
+ expect(alert.childAt(0).text()).toEqual(testCase.children);
+ });
+ });
+
+ describe('data is returned', () => {
+ const props = {
+ queryParams: {
+ startTime: 1572128592,
+ endTime: 1572130692,
+ resolution: 28,
+ },
+ stacked: false,
+ data: {
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {
+ code: '200',
+ handler: '/graph',
+ instance: 'localhost:9090',
+ job: 'prometheus',
+ },
+ values: [
+ [1572128592, '23'],
+ [1572128620, '2'],
+ [1572128648, '4'],
+ [1572128676, '1'],
+ [1572128704, '2'],
+ [1572128732, '12'],
+ [1572128760, '1'],
+ [1572128788, '0'],
+ [1572128816, '0'],
+ [1572128844, '2'],
+ [1572128872, '5'],
+ [1572130384, '6'],
+ [1572130412, '7'],
+ [1572130440, '19'],
+ [1572130468, '33'],
+ [1572130496, '14'],
+ [1572130524, '7'],
+ [1572130552, '6'],
+ [1572130580, '0'],
+ [1572130608, '0'],
+ [1572130636, '0'],
+ [1572130664, '0'],
+ [1572130692, '0'],
+ ],
+ },
+ ],
+ },
+ };
+ it('renders a graph with props', () => {
+ const graph = shallow();
+ const div = graph.find('div').filterWhere(elem => elem.prop('className') === 'graph');
+ const resize = div.find(ReactResizeDetector);
+ const innerdiv = div.find('div').filterWhere(elem => elem.prop('className') === 'graph-chart');
+ const legend = graph.find(Legend);
+ expect(resize.prop('handleWidth')).toBe(true);
+ expect(div).toHaveLength(1);
+ expect(innerdiv).toHaveLength(1);
+ expect(legend).toHaveLength(1);
+ });
+ });
+});
diff --git a/web/ui/react-app/src/Graph.tsx b/web/ui/react-app/src/Graph.tsx
index f7f6b49a7b..5be47cdcb0 100644
--- a/web/ui/react-app/src/Graph.tsx
+++ b/web/ui/react-app/src/Graph.tsx
@@ -4,6 +4,7 @@ import ReactResizeDetector from 'react-resize-detector';
import { Alert } from 'reactstrap';
import Legend from './Legend';
+import { escapeHTML } from './utils/html';
require('flot');
require('flot/source/jquery.flot.crosshair');
@@ -32,26 +33,11 @@ class Graph extends PureComponent {
private id: number = getGraphID();
private chartRef = React.createRef();
- escapeHTML(str: string) {
- const entityMap: { [key: string]: string } = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- '/': '/',
- };
-
- return String(str).replace(/[&<>"'/]/g, function(s) {
- return entityMap[s];
- });
- }
-
renderLabels(labels: { [key: string]: string }) {
- const labelStrings: string[] = [];
+ let labelStrings: string[] = [];
for (const label in labels) {
if (label !== '__name__') {
- labelStrings.push('' + label + ': ' + this.escapeHTML(labels[label]));
+ labelStrings.push('' + label + ': ' + escapeHTML(labels[label]));
}
}
return '' + labelStrings.join('
') + '
';
diff --git a/web/ui/react-app/src/GraphControls.test.tsx b/web/ui/react-app/src/GraphControls.test.tsx
new file mode 100755
index 0000000000..ad0a66bafc
--- /dev/null
+++ b/web/ui/react-app/src/GraphControls.test.tsx
@@ -0,0 +1,165 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphControls from './GraphControls';
+import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
+import TimeInput from './TimeInput';
+
+const defaultGraphControlProps = {
+ range: 60 * 60 * 24,
+ endTime: 1572100217898,
+ resolution: 10,
+ stacked: false,
+
+ onChangeRange: (): void => {},
+ onChangeEndTime: (): void => {},
+ onChangeResolution: (): void => {},
+ onChangeStacking: (): void => {},
+};
+
+describe('GraphControls', () => {
+ it('renders a form', () => {
+ const controls = shallow();
+ const form = controls.find(Form);
+ expect(form).toHaveLength(1);
+ expect(form.prop('className')).toEqual('graph-controls');
+ expect(form.prop('inline')).toBe(true);
+ });
+
+ it('renders an Input Group for range', () => {
+ const controls = shallow();
+ const form = controls.find(InputGroup);
+ expect(form).toHaveLength(1);
+ expect(form.prop('className')).toEqual('range-input');
+ expect(form.prop('size')).toBe('sm');
+ });
+
+ it('renders a decrease/increase range buttons', () => {
+ [
+ {
+ position: 'prepend',
+ title: 'Decrease range',
+ icon: faMinus,
+ },
+ {
+ position: 'append',
+ title: 'Increase range',
+ icon: faPlus,
+ },
+ ].forEach(testCase => {
+ const controls = shallow();
+ const addon = controls.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === testCase.position);
+ const button = addon.find(Button);
+ const icon = button.find(FontAwesomeIcon);
+ expect(button.prop('title')).toEqual(testCase.title);
+ expect(icon).toHaveLength(1);
+ expect(icon.prop('icon')).toEqual(testCase.icon);
+ expect(icon.prop('fixedWidth')).toBe(true);
+ });
+ });
+
+ it('renders an Input for range', () => {
+ const controls = shallow();
+ const form = controls.find(InputGroup);
+ const input = form.find(Input);
+ expect(input).toHaveLength(1);
+ expect(input.prop('defaultValue')).toEqual('1d');
+ expect(input.prop('innerRef')).toEqual({ current: null });
+ });
+
+ it('renders a TimeInput with props', () => {
+ const controls = shallow();
+ const timeInput = controls.find(TimeInput);
+ expect(timeInput).toHaveLength(1);
+ expect(timeInput.prop('time')).toEqual(1572100217898);
+ expect(timeInput.prop('range')).toEqual(86400);
+ expect(timeInput.prop('placeholder')).toEqual('End time');
+ });
+
+ it('renders a TimeInput with a callback', () => {
+ const results: (number | null)[] = [];
+ const onChange = (endTime: number | null): void => {
+ results.push(endTime);
+ };
+ const controls = shallow();
+ const timeInput = controls.find(TimeInput);
+ const onChangeTime = timeInput.prop('onChangeTime');
+ if (onChangeTime) {
+ onChangeTime(5);
+ expect(results).toHaveLength(1);
+ expect(results[0]).toEqual(5);
+ results.pop();
+ } else {
+ fail('Expected onChangeTime to be defined but it was not');
+ }
+ });
+
+ it('renders a resolution Input with props', () => {
+ const controls = shallow();
+ const input = controls.find(Input).filterWhere(input => input.prop('className') === 'resolution-input');
+ expect(input.prop('placeholder')).toEqual('Res. (s)');
+ expect(input.prop('defaultValue')).toEqual('10');
+ expect(input.prop('innerRef')).toEqual({ current: null });
+ expect(input.prop('bsSize')).toEqual('sm');
+ });
+
+ it('renders a button group', () => {
+ const controls = shallow();
+ const group = controls.find(ButtonGroup);
+ expect(group.prop('className')).toEqual('stacked-input');
+ expect(group.prop('size')).toEqual('sm');
+ });
+
+ it('renders buttons inside the button group', () => {
+ [
+ {
+ title: 'Show unstacked line graph',
+ icon: faChartLine,
+ active: true,
+ },
+ {
+ title: 'Show stacked graph',
+ icon: faChartArea,
+ active: false,
+ },
+ ].forEach(testCase => {
+ const controls = shallow();
+ const group = controls.find(ButtonGroup);
+ const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title);
+ expect(btn.prop('active')).toEqual(testCase.active);
+ const icon = btn.find(FontAwesomeIcon);
+ expect(icon.prop('icon')).toEqual(testCase.icon);
+ });
+ });
+
+ it('renders buttons with callbacks', () => {
+ [
+ {
+ title: 'Show unstacked line graph',
+ active: true,
+ },
+ {
+ title: 'Show stacked graph',
+ active: false,
+ },
+ ].forEach(testCase => {
+ const results: boolean[] = [];
+ const onChange = (stacked: boolean): void => {
+ results.push(stacked);
+ };
+ const controls = shallow();
+ const group = controls.find(ButtonGroup);
+ const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title);
+ const onClick = btn.prop('onClick');
+ if (onClick) {
+ onClick({} as React.MouseEvent);
+ expect(results).toHaveLength(1);
+ expect(results[0]).toBe(!testCase.active);
+ results.pop();
+ } else {
+ fail('Expected onClick to be defined but it was not');
+ }
+ });
+ });
+});
diff --git a/web/ui/react-app/src/Legend.test.tsx b/web/ui/react-app/src/Legend.test.tsx
new file mode 100755
index 0000000000..f61dc4d1cd
--- /dev/null
+++ b/web/ui/react-app/src/Legend.test.tsx
@@ -0,0 +1,93 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Legend from './Legend';
+import SeriesName from './SeriesName';
+
+describe('Legend', () => {
+ describe('regardless of series', () => {
+ it('renders a table', () => {
+ const legend = shallow();
+ expect(legend.type()).toEqual('table');
+ expect(legend.prop('className')).toEqual('graph-legend');
+ const tbody = legend.children();
+ expect(tbody.type()).toEqual('tbody');
+ });
+ });
+ describe('when series is empty', () => {
+ it('renders props as empty legend table', () => {
+ const legend = shallow();
+ const tbody = legend.children();
+ expect(tbody.children()).toHaveLength(0);
+ });
+ });
+
+ describe('when series has one element', () => {
+ const legendProps = {
+ series: [
+ {
+ index: 1,
+ color: 'red',
+ labels: {
+ __name__: 'metric_name',
+ label1: 'value_1',
+ labeln: 'value_n',
+ },
+ },
+ ],
+ };
+ it('renders a row of the one series', () => {
+ const legend = shallow();
+ const tbody = legend.children();
+ expect(tbody.children()).toHaveLength(1);
+ const row = tbody.find('tr');
+ expect(row.prop('className')).toEqual('legend-item');
+ });
+ it('renders a legend swatch', () => {
+ const legend = shallow();
+ const tbody = legend.children();
+ const row = tbody.find('tr');
+ const swatch = row.childAt(0);
+ expect(swatch.type()).toEqual('td');
+ expect(swatch.children().prop('className')).toEqual('legend-swatch');
+ expect(swatch.children().prop('style')).toEqual({
+ backgroundColor: 'red',
+ });
+ });
+ it('renders a series name', () => {
+ const legend = shallow();
+ const tbody = legend.children();
+ const row = tbody.find('tr');
+ const series = row.childAt(1);
+ expect(series.type()).toEqual('td');
+ const seriesName = series.find(SeriesName);
+ expect(seriesName).toHaveLength(1);
+ expect(seriesName.prop('labels')).toEqual(legendProps.series[0].labels);
+ expect(seriesName.prop('format')).toBe(true);
+ });
+ });
+
+ describe('when series has _n_ elements', () => {
+ const range = Array.from(Array(20).keys());
+ const legendProps = {
+ series: range.map(i => ({
+ index: i,
+ color: 'red',
+ labels: {
+ __name__: `metric_name_${i}`,
+ label1: 'value_1',
+ labeln: 'value_n',
+ },
+ })),
+ };
+ it('renders _n_ rows', () => {
+ const legend = shallow();
+ const tbody = legend.children();
+ expect(tbody.children()).toHaveLength(20);
+ const rows = tbody.find('tr');
+ rows.forEach(row => {
+ expect(row.prop('className')).toEqual('legend-item');
+ expect(row.find(SeriesName)).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/web/ui/react-app/src/MetricFormat.test.ts b/web/ui/react-app/src/MetricFormat.test.ts
new file mode 100644
index 0000000000..d98e44a8f6
--- /dev/null
+++ b/web/ui/react-app/src/MetricFormat.test.ts
@@ -0,0 +1,25 @@
+import metricToSeriesName from './MetricFormat';
+
+describe('metricToSeriesName', () => {
+ it('returns "{}" if labels is empty', () => {
+ const labels = {};
+ expect(metricToSeriesName(labels)).toEqual('{}');
+ });
+ it('returns "metric_name{}" if labels only contains __name__', () => {
+ const labels = { __name__: 'metric_name' };
+ expect(metricToSeriesName(labels)).toEqual('metric_name{}');
+ });
+ it('returns "{label1=value_1, ..., labeln=value_n} if there are many labels and no name', () => {
+ const labels = { label1: 'value_1', label2: 'value_2', label3: 'value_3' };
+ expect(metricToSeriesName(labels)).toEqual('{label1="value_1", label2="value_2", label3="value_3"}');
+ });
+ it('returns "metric_name{label1=value_1, ... ,labeln=value_n}" if there are many labels and a name', () => {
+ const labels = {
+ __name__: 'metric_name',
+ label1: 'value_1',
+ label2: 'value_2',
+ label3: 'value_3',
+ };
+ expect(metricToSeriesName(labels)).toEqual('metric_name{label1="value_1", label2="value_2", label3="value_3"}');
+ });
+});
diff --git a/web/ui/react-app/src/MetricFomat.ts b/web/ui/react-app/src/MetricFormat.ts
similarity index 100%
rename from web/ui/react-app/src/MetricFomat.ts
rename to web/ui/react-app/src/MetricFormat.ts
diff --git a/web/ui/react-app/src/Panel.test.tsx b/web/ui/react-app/src/Panel.test.tsx
new file mode 100644
index 0000000000..9dbcde1efa
--- /dev/null
+++ b/web/ui/react-app/src/Panel.test.tsx
@@ -0,0 +1,93 @@
+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 Graph from './Graph';
+import { NavLink, TabPane } from 'reactstrap';
+import TimeInput from './TimeInput';
+import DataTable from './DataTable';
+
+describe('Panel', () => {
+ const props = {
+ options: {
+ expr: 'prometheus_engine',
+ type: PanelType.Table,
+ range: 10,
+ endTime: 1572100217898,
+ resolution: 28,
+ stacked: false,
+ },
+ onOptionsChanged: (): void => {},
+ pastQueries: [],
+ metricNames: [
+ 'prometheus_engine_queries',
+ 'prometheus_engine_queries_concurrent_max',
+ 'prometheus_engine_query_duration_seconds',
+ ],
+ removePanel: (): void => {},
+ onExecuteQuery: (): void => {},
+ };
+ const panel = shallow();
+
+ it('renders an ExpressionInput', () => {
+ const input = panel.find(ExpressionInput);
+ expect(input.prop('value')).toEqual('prometheus_engine');
+ expect(input.prop('autocompleteSections')).toEqual({
+ 'Metric Names': [
+ 'prometheus_engine_queries',
+ 'prometheus_engine_queries_concurrent_max',
+ 'prometheus_engine_query_duration_seconds',
+ ],
+ 'Query History': [],
+ });
+ });
+
+ it('renders NavLinks', () => {
+ const results: PanelOptions[] = [];
+ const onOptionsChanged = (opts: PanelOptions): void => {
+ results.push(opts);
+ };
+ const panel = shallow();
+ const links = panel.find(NavLink);
+ [{ panelType: 'Table', active: true }, { panelType: 'Graph', active: false }].forEach(
+ (tc: { panelType: string; active: boolean }, i: number) => {
+ const link = links.at(i);
+ const className = tc.active ? 'active' : '';
+ expect(link.prop('className')).toEqual(className);
+ link.simulate('click');
+ expect(results).toHaveLength(1);
+ expect(results[0].type).toEqual(tc.panelType.toLowerCase());
+ results.pop();
+ }
+ );
+ });
+
+ it('renders a TabPane with a TimeInput and a DataTable when in table mode', () => {
+ const tab = panel.find(TabPane).filterWhere(tab => tab.prop('tabId') === 'table');
+ const timeInput = tab.find(TimeInput);
+ expect(timeInput.prop('time')).toEqual(props.options.endTime);
+ expect(timeInput.prop('range')).toEqual(props.options.range);
+ expect(timeInput.prop('placeholder')).toEqual('Evaluation time');
+ expect(tab.find(DataTable)).toHaveLength(1);
+ });
+
+ it('renders a TabPane with a Graph and GraphControls when in graph mode', () => {
+ const options = {
+ expr: 'prometheus_engine',
+ type: PanelType.Graph,
+ range: 10,
+ endTime: 1572100217898,
+ resolution: 28,
+ stacked: false,
+ };
+ const graphPanel = mount();
+ const controls = graphPanel.find(GraphControls);
+ const graph = graphPanel.find(Graph);
+ expect(controls.prop('endTime')).toEqual(props.options.endTime);
+ expect(controls.prop('range')).toEqual(props.options.range);
+ expect(controls.prop('resolution')).toEqual(props.options.resolution);
+ expect(controls.prop('stacked')).toEqual(props.options.stacked);
+ expect(graph.prop('stacked')).toEqual(props.options.stacked);
+ });
+});
diff --git a/web/ui/react-app/src/PanelList.test.tsx b/web/ui/react-app/src/PanelList.test.tsx
new file mode 100755
index 0000000000..1d7b230b30
--- /dev/null
+++ b/web/ui/react-app/src/PanelList.test.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import PanelList from './pages/PanelList';
+import Checkbox from './Checkbox';
+import { Alert, Button } from 'reactstrap';
+import Panel from './Panel';
+
+describe('PanelList', () => {
+ it('renders a query history checkbox', () => {
+ const panelList = shallow();
+ const checkbox = panelList.find(Checkbox);
+ expect(checkbox.prop('id')).toEqual('query-history-checkbox');
+ expect(checkbox.prop('wrapperStyles')).toEqual({
+ margin: '0 0 0 15px',
+ alignSelf: 'center',
+ });
+ expect(checkbox.prop('defaultChecked')).toBe(false);
+ expect(checkbox.children().text()).toBe('Enable query history');
+ });
+
+ it('renders an alert when no data is queried yet', () => {
+ const panelList = mount();
+ const alert = panelList.find(Alert);
+ expect(alert.prop('color')).toEqual('light');
+ expect(alert.children().text()).toEqual('No data queried yet');
+ });
+
+ it('renders panels', () => {
+ const panelList = shallow();
+ const panels = panelList.find(Panel);
+ expect(panels.length).toBeGreaterThan(0);
+ });
+
+ it('renders a button to add a panel', () => {
+ const panelList = shallow();
+ const btn = panelList.find(Button).filterWhere(btn => btn.prop('className') === 'add-panel-btn');
+ expect(btn.prop('color')).toEqual('primary');
+ expect(btn.children().text()).toEqual('Add Panel');
+ });
+});
diff --git a/web/ui/react-app/src/QueryStatsView.test.tsx b/web/ui/react-app/src/QueryStatsView.test.tsx
new file mode 100755
index 0000000000..e04c914e1a
--- /dev/null
+++ b/web/ui/react-app/src/QueryStatsView.test.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import QueryStatsView from './QueryStatsView';
+
+describe('QueryStatsView', () => {
+ it('renders props as query stats', () => {
+ const queryStatsProps = {
+ loadTime: 100,
+ resolution: 5,
+ resultSeries: 10000,
+ };
+ const queryStatsView = shallow();
+ expect(queryStatsView.prop('className')).toEqual('query-stats');
+ expect(queryStatsView.children().prop('className')).toEqual('float-right');
+ expect(queryStatsView.children().text()).toEqual('Load time: 100ms Resolution: 5s Result series: 10000');
+ });
+});
diff --git a/web/ui/react-app/src/SeriesName.test.tsx b/web/ui/react-app/src/SeriesName.test.tsx
new file mode 100755
index 0000000000..4eaf68105c
--- /dev/null
+++ b/web/ui/react-app/src/SeriesName.test.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import SeriesName from './SeriesName';
+
+describe('SeriesName', () => {
+ describe('with labels=null', () => {
+ const seriesNameProps = {
+ labels: null,
+ format: false,
+ };
+ const seriesName = shallow();
+ it('renders the string "scalar"', () => {
+ expect(seriesName.text()).toEqual('scalar');
+ });
+ });
+
+ describe('with labels defined and format false', () => {
+ const seriesNameProps = {
+ labels: {
+ __name__: 'metric_name',
+ label1: 'value_1',
+ label2: 'value_2',
+ label3: 'value_3',
+ },
+ format: false,
+ };
+ const seriesName = shallow();
+ it('renders the series name as a string', () => {
+ expect(seriesName.text()).toEqual('metric_name{label1="value_1", label2="value_2", label3="value_3"}');
+ });
+ });
+
+ describe('with labels defined and format true', () => {
+ const seriesNameProps = {
+ labels: {
+ __name__: 'metric_name',
+ label1: 'value_1',
+ label2: 'value_2',
+ label3: 'value_3',
+ },
+ format: true,
+ };
+ const seriesName = shallow();
+ it('renders the series name as a series of spans', () => {
+ expect(seriesName.children()).toHaveLength(6);
+ const testCases = [
+ { name: 'metric_name', className: 'legend-metric-name' },
+ { name: '{', className: 'legend-label-brace' },
+ { name: 'label1', value: 'value_1', className: 'legend-label-name' },
+ { name: 'label2', value: 'value_2', className: 'legend-label-name' },
+ { name: 'label3', value: 'value_3', className: 'legend-label-name' },
+ { name: '}', className: 'legend-label-brace' },
+ ];
+ testCases.forEach((tc, i) => {
+ const child = seriesName.childAt(i);
+ const text = child
+ .children()
+ .map(ch => ch.text())
+ .join('');
+ switch (child.children().length) {
+ case 1:
+ expect(text).toEqual(tc.name);
+ expect(child.prop('className')).toEqual(tc.className);
+ break;
+ case 3:
+ expect(text).toEqual(`${tc.name}="${tc.value}"`);
+ expect(child.childAt(0).prop('className')).toEqual('legend-label-name');
+ expect(child.childAt(2).prop('className')).toEqual('legend-label-value');
+ break;
+ case 4:
+ expect(text).toEqual(`, ${tc.name}="${tc.value}"`);
+ expect(child.childAt(1).prop('className')).toEqual('legend-label-name');
+ expect(child.childAt(3).prop('className')).toEqual('legend-label-value');
+ break;
+ default:
+ fail('incorrect number of children: ' + child.children().length);
+ }
+ });
+ });
+ });
+});
diff --git a/web/ui/react-app/src/SeriesName.tsx b/web/ui/react-app/src/SeriesName.tsx
index 1da4882570..edbe1d97d9 100644
--- a/web/ui/react-app/src/SeriesName.tsx
+++ b/web/ui/react-app/src/SeriesName.tsx
@@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
+import metricToSeriesName from './MetricFormat';
interface SeriesNameProps {
labels: { [key: string]: string } | null;
@@ -40,16 +41,7 @@ class SeriesName extends PureComponent {
renderPlain() {
const labels = this.props.labels!;
-
- let tsName = (labels.__name__ || '') + '{';
- const labelStrings: string[] = [];
- for (const label in labels) {
- if (label !== '__name__') {
- labelStrings.push(label + '="' + labels[label] + '"');
- }
- }
- tsName += labelStrings.join(', ') + '}';
- return tsName;
+ return metricToSeriesName(labels);
}
render() {
diff --git a/web/ui/react-app/src/TimeInput.test.tsx b/web/ui/react-app/src/TimeInput.test.tsx
new file mode 100644
index 0000000000..ad9043499a
--- /dev/null
+++ b/web/ui/react-app/src/TimeInput.test.tsx
@@ -0,0 +1,58 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import TimeInput from './TimeInput';
+import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faChevronLeft, faChevronRight, faTimes } from '@fortawesome/free-solid-svg-icons';
+
+describe('TimeInput', () => {
+ const timeInputProps = {
+ time: 1572102237932,
+ range: 60 * 60 * 7,
+ placeholder: 'time input',
+ onChangeTime: (): void => {},
+ };
+ const timeInput = shallow();
+ it('renders the string "scalar"', () => {
+ const inputGroup = timeInput.find(InputGroup);
+ expect(inputGroup.prop('className')).toEqual('time-input');
+ expect(inputGroup.prop('size')).toEqual('sm');
+ });
+
+ it('renders buttons to adjust time', () => {
+ [
+ {
+ position: 'prepend',
+ title: 'Decrease time',
+ icon: faChevronLeft,
+ },
+ {
+ position: 'append',
+ title: 'Clear time',
+ icon: faTimes,
+ },
+ {
+ position: 'append',
+ title: 'Increase time',
+ icon: faChevronRight,
+ },
+ ].forEach(button => {
+ const onChangeTime = sinon.spy();
+ const timeInput = shallow();
+ const addon = timeInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === button.position);
+ const btn = addon.find(Button).filterWhere(btn => btn.prop('title') === button.title);
+ const icon = btn.find(FontAwesomeIcon);
+ expect(icon.prop('icon')).toEqual(button.icon);
+ expect(icon.prop('fixedWidth')).toBe(true);
+ btn.simulate('click');
+ expect(onChangeTime.calledOnce).toBe(true);
+ });
+ });
+
+ it('renders an Input', () => {
+ const input = timeInput.find(Input);
+ expect(input.prop('placeholder')).toEqual(timeInputProps.placeholder);
+ expect(input.prop('innerRef')).toEqual({ current: null });
+ });
+});
diff --git a/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx b/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx
new file mode 100644
index 0000000000..2dd47c95f9
--- /dev/null
+++ b/web/ui/react-app/src/components/SanitizeHTML/SanitizeHTML.test.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import SanitizeHTML from '.';
+
+describe('SanitizeHTML', () => {
+ it(`renders allowed html`, () => {
+ const props = {
+ allowedTags: ['strong'],
+ };
+ const html = shallow({'text'});
+ const elem = html.find('div');
+ expect(elem).toHaveLength(1);
+ expect(elem.html()).toEqual(`text
`);
+ });
+
+ it('does not render disallowed tags', () => {
+ const props = {
+ tag: 'span' as keyof JSX.IntrinsicElements,
+ allowedTags: ['strong'],
+ };
+ const html = shallow({'link'});
+ const elem = html.find('span');
+ expect(elem.html()).toEqual('link');
+ });
+});
diff --git a/web/ui/react-app/src/components/SanitizeHTML/test.js b/web/ui/react-app/src/components/SanitizeHTML/test.js
deleted file mode 100644
index e8f8c62355..0000000000
--- a/web/ui/react-app/src/components/SanitizeHTML/test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * SanitizeHTML tests
- */
-import React from 'react';
-import ReactDOM from 'react-dom';
-import SanitizeHTML from '../SanitizeHTML';
-
-it('renders without crashing', () => {
- const div = document.createElement('div');
- ReactDOM.render(, div);
- ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/web/ui/react-app/src/setupTests.ts b/web/ui/react-app/src/setupTests.ts
new file mode 100644
index 0000000000..08699661fe
--- /dev/null
+++ b/web/ui/react-app/src/setupTests.ts
@@ -0,0 +1,5 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import './globals';
+
+configure({ adapter: new Adapter() });
diff --git a/web/ui/react-app/src/utils/func.ts b/web/ui/react-app/src/utils/func.ts
deleted file mode 100644
index 3b0412fb49..0000000000
--- a/web/ui/react-app/src/utils/func.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const uuidGen = () =>
- '_' +
- Math.random()
- .toString(36)
- .substr(2, 9);
diff --git a/web/ui/react-app/src/utils/html.test.ts b/web/ui/react-app/src/utils/html.test.ts
new file mode 100644
index 0000000000..2db5e83237
--- /dev/null
+++ b/web/ui/react-app/src/utils/html.test.ts
@@ -0,0 +1,9 @@
+import { escapeHTML } from './html';
+
+describe('escapeHTML', (): void => {
+ it('escapes html sequences', () => {
+ expect(escapeHTML(`'example'&"another/example"`)).toEqual(
+ '<strong>'example'&"another/example"</strong>'
+ );
+ });
+});
diff --git a/web/ui/react-app/src/utils/html.ts b/web/ui/react-app/src/utils/html.ts
new file mode 100644
index 0000000000..e4e101699e
--- /dev/null
+++ b/web/ui/react-app/src/utils/html.ts
@@ -0,0 +1,14 @@
+export const escapeHTML = (str: string): string => {
+ const entityMap: { [key: string]: string } = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ '/': '/',
+ };
+
+ return String(str).replace(/[&<>"'/]/g, function(s) {
+ return entityMap[s];
+ });
+};
diff --git a/web/ui/react-app/src/utils/timeFormat.test.ts b/web/ui/react-app/src/utils/timeFormat.test.ts
new file mode 100644
index 0000000000..a5cc398d2b
--- /dev/null
+++ b/web/ui/react-app/src/utils/timeFormat.test.ts
@@ -0,0 +1,37 @@
+import { formatTime, parseTime, formatRange, parseRange } from './timeFormat';
+
+describe('formatTime', () => {
+ it('returns a time string representing the time in seconds', () => {
+ expect(formatTime(1572049380000)).toEqual('2019-10-26 00:23');
+ expect(formatTime(0)).toEqual('1970-01-01 00:00');
+ });
+});
+
+describe('parseTime', () => {
+ it('returns a time string representing the time in seconds', () => {
+ expect(parseTime('2019-10-26 00:23')).toEqual(1572049380000);
+ expect(parseTime('1970-01-01 00:00')).toEqual(0);
+ });
+});
+
+describe('formatRange', () => {
+ it('returns a time string representing the time in seconds in one unit', () => {
+ expect(formatRange(60 * 60 * 24 * 365)).toEqual('1y');
+ expect(formatRange(60 * 60 * 24 * 7)).toEqual('1w');
+ expect(formatRange(2 * 60 * 60 * 24)).toEqual('2d');
+ expect(formatRange(60 * 60)).toEqual('1h');
+ expect(formatRange(7 * 60)).toEqual('7m');
+ expect(formatRange(63)).toEqual('63s');
+ });
+});
+
+describe('parseRange', () => {
+ it('returns a time string representing the time in seconds in one unit', () => {
+ expect(parseRange('1y')).toEqual(60 * 60 * 24 * 365);
+ expect(parseRange('1w')).toEqual(60 * 60 * 24 * 7);
+ expect(parseRange('2d')).toEqual(2 * 60 * 60 * 24);
+ expect(parseRange('1h')).toEqual(60 * 60);
+ expect(parseRange('7m')).toEqual(7 * 60);
+ expect(parseRange('63s')).toEqual(63);
+ });
+});
diff --git a/web/ui/react-app/src/utils/urlParams.test.ts b/web/ui/react-app/src/utils/urlParams.test.ts
new file mode 100644
index 0000000000..d117796614
--- /dev/null
+++ b/web/ui/react-app/src/utils/urlParams.test.ts
@@ -0,0 +1,47 @@
+import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './urlParams';
+import { PanelType } from '../Panel';
+
+const panels = [
+ {
+ key: '0',
+ options: {
+ endTime: 1572046620000,
+ expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])',
+ range: 3600,
+ resolution: null,
+ stacked: false,
+ type: PanelType.Graph,
+ },
+ },
+ {
+ key: '1',
+ options: {
+ endTime: null,
+ expr: 'node_filesystem_avail_bytes',
+ range: 3600,
+ resolution: null,
+ stacked: false,
+ type: PanelType.Table,
+ },
+ },
+];
+const query =
+ '?g0.expr=rate(node_cpu_seconds_total%7Bmode%3D%22system%22%7D%5B1m%5D)&g0.tab=0&g0.stacked=0&g0.range_input=1h&g0.end_input=2019-10-25%2023%3A37&g0.moment_input=2019-10-25%2023%3A37&g1.expr=node_filesystem_avail_bytes&g1.tab=1&g1.stacked=0&g1.range_input=1h';
+
+describe('decodePanelOptionsFromQueryString', () => {
+ it('returns [] when query is empty', () => {
+ expect(decodePanelOptionsFromQueryString('')).toEqual([]);
+ });
+ it('returns and array of parsed params when query string is non-empty', () => {
+ expect(decodePanelOptionsFromQueryString(query)).toEqual(panels);
+ });
+});
+
+describe('encodePanelOptionsToQueryString', () => {
+ it('returns ? when panels is empty', () => {
+ expect(encodePanelOptionsToQueryString([])).toEqual('?');
+ });
+ it('returns an encoded query string otherwise', () => {
+ expect(encodePanelOptionsToQueryString(panels)).toEqual(query);
+ });
+});
diff --git a/web/ui/react-app/yarn.lock b/web/ui/react-app/yarn.lock
index d8758999ec..c8a025cd67 100644
--- a/web/ui/react-app/yarn.lock
+++ b/web/ui/react-app/yarn.lock
@@ -1150,6 +1150,35 @@
react-lifecycles-compat "^3.0.4"
warning "^3.0.0"
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393"
+ integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/formatio@^3.2.1":
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
+ integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
+ dependencies:
+ "@sinonjs/commons" "^1"
+ "@sinonjs/samsam" "^3.1.0"
+
+"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3":
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
+ integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
+ dependencies:
+ "@sinonjs/commons" "^1.3.0"
+ array-from "^2.1.1"
+ lodash "^4.17.15"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+ integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+
"@svgr/babel-plugin-add-jsx-attribute@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1"
@@ -1286,6 +1315,13 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/cheerio@*":
+ version "0.22.13"
+ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6"
+ integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA==
+ dependencies:
+ "@types/node" "*"
+
"@types/domhandler@*":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf"
@@ -1298,6 +1334,21 @@
dependencies:
"@types/domhandler" "*"
+"@types/enzyme-adapter-react-16@^1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5"
+ integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg==
+ dependencies:
+ "@types/enzyme" "*"
+
+"@types/enzyme@*", "@types/enzyme@^3.10.3":
+ version "3.10.3"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.3.tgz#02b6c5ac7d0472005944a652e79045e2f6c66804"
+ integrity sha512-f/Kcb84sZOSZiBPCkr4He9/cpuSLcKRyQaEE20Q30Prx0Dn6wcyMAWI0yofL6yvd9Ht9G7EVkQeRqK0n5w8ILw==
+ dependencies:
+ "@types/cheerio" "*"
+ "@types/react" "*"
+
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -1349,10 +1400,10 @@
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==
-"@types/jest@^24.0.4":
- version "24.0.19"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.19.tgz#f7036058d2a5844fe922609187c0ad8be430aff5"
- integrity sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg==
+"@types/jest@^24.0.20":
+ version "24.0.20"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.20.tgz#729d5fe8684e7fb06368d3bd557ac6d91289d861"
+ integrity sha512-M8ebEkOpykGdLoRrmew7UowTZ1DANeeP0HiSIChl/4DGgmnSC1ntitNtkyNSXjMTsZvXuaxJrxjImEnRWNPsPw==
dependencies:
"@types/jest-diff" "*"
@@ -1442,6 +1493,11 @@
dependencies:
"@types/htmlparser2" "*"
+"@types/sinon@^7.5.0":
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.0.tgz#f5a10c27175465a0b001b68d8b9f761582967cc6"
+ integrity sha512-NyzhuSBy97B/zE58cDw4NyGvByQbAHNP9069KVSgnXt/sc0T6MFRh0InKAeBVHJWdSXG1S3+PxgVIgKo9mTHbw==
+
"@types/sizzle@*":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
@@ -1769,6 +1825,22 @@ adjust-sourcemap-loader@2.0.0:
object-path "0.11.4"
regex-parser "2.2.10"
+airbnb-prop-types@^2.15.0:
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef"
+ integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==
+ dependencies:
+ array.prototype.find "^2.1.0"
+ function.prototype.name "^1.1.1"
+ has "^1.0.3"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.1.0"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.9.0"
+
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
@@ -1912,6 +1984,11 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -1922,6 +1999,11 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
+array-from@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
+ integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
+
array-includes@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
@@ -1947,6 +2029,23 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+array.prototype.find@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7"
+ integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+
+array.prototype.flat@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz#8f3c71d245ba349b6b64b4078f76f5576f1fd723"
+ integrity sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.15.0"
+ function-bind "^1.1.1"
+
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -2629,6 +2728,18 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2837,7 +2948,7 @@ commander@2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
-commander@^2.11.0, commander@^2.20.0:
+commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -3169,7 +3280,7 @@ css-select-base-adapter@^0.1.1:
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
-css-select@^1.1.0:
+css-select@^1.1.0, css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@@ -3548,6 +3659,11 @@ diff-sequences@^24.9.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+diff@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+ integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
+
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -3565,6 +3681,11 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@@ -3629,12 +3750,20 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
-domelementtype@1, domelementtype@^1.3.1:
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@@ -3786,7 +3915,7 @@ enhanced-resolve@^4.1.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
-entities@^1.1.1:
+entities@^1.1.1, entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@@ -3796,6 +3925,68 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+enzyme-adapter-react-16@^1.15.1:
+ version "1.15.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5"
+ integrity sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA==
+ dependencies:
+ enzyme-adapter-utils "^1.12.1"
+ enzyme-shallow-equal "^1.0.0"
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ react-is "^16.10.2"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.12.1:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz#e828e0d038e2b1efa4b9619ce896226f85c9dd88"
+ integrity sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g==
+ dependencies:
+ airbnb-prop-types "^2.15.0"
+ function.prototype.name "^1.1.1"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.1"
+ prop-types "^15.7.2"
+ semver "^5.7.0"
+
+enzyme-shallow-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69"
+ integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ==
+ dependencies:
+ has "^1.0.3"
+ object-is "^1.0.1"
+
+enzyme@^3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6"
+ integrity sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ html-element-map "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.6.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.1.2"
+
errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -3826,6 +4017,22 @@ es-abstract@^1.12.0, es-abstract@^1.15.0, es-abstract@^1.5.1, es-abstract@^1.7.0
string.prototype.trimleft "^2.1.0"
string.prototype.trimright "^2.1.0"
+es-abstract@^1.13.0:
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d"
+ integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.0"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-inspect "^1.6.0"
+ object-keys "^1.1.1"
+ string.prototype.trimleft "^2.1.0"
+ string.prototype.trimright "^2.1.0"
+
es-to-primitive@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
@@ -4635,11 +4842,26 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function.prototype.name@^1.1.0, function.prototype.name@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392"
+ integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+ functions-have-names "^1.1.1"
+ is-callable "^1.1.4"
+
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+functions-have-names@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d"
+ integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ==
+
fuzzy@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8"
@@ -4957,6 +5179,13 @@ html-comment-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+html-element-map@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c"
+ integrity sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA==
+ dependencies:
+ array-filter "^1.0.0"
+
html-encoding-sniffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
@@ -4994,7 +5223,7 @@ html-webpack-plugin@4.0.0-beta.5:
tapable "^1.1.0"
util.promisify "1.0.0"
-htmlparser2@^3.10.0, htmlparser2@^3.3.0:
+htmlparser2@^3.10.0, htmlparser2@^3.3.0, htmlparser2@^3.9.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -5351,6 +5580,11 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+ integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=
+
is-buffer@^1.0.2, is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -5470,6 +5704,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+ integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -5550,6 +5789,16 @@ is-stream@^1.0.1, is-stream@^1.1.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+ integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+ integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
+
is-svg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@@ -5579,6 +5828,11 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -6259,6 +6513,11 @@ jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1:
array-includes "^3.0.3"
object.assign "^4.1.0"
+just-extend@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
+ integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==
+
killable@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -6416,11 +6675,26 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+ integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
lodash.isfunction@^3.0.9:
version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
@@ -6486,7 +6760,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5:
+"lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -6496,6 +6770,11 @@ loglevel@^1.4.1:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56"
integrity sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==
+lolex@^4.1.0, lolex@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
+ integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==
+
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -6816,6 +7095,11 @@ moment-timezone@^0.5.11, moment-timezone@^0.5.23:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+moo@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+ integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -6888,6 +7172,17 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+nearley@^2.7.10:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.0.tgz#37717781d0fd0f2bfc95e233ebd75678ca4bda46"
+ integrity sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.4.3"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
needle@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
@@ -6917,6 +7212,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+nise@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652"
+ integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==
+ dependencies:
+ "@sinonjs/formatio" "^3.2.1"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ lolex "^4.1.0"
+ path-to-regexp "^1.7.0"
+
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
@@ -7173,7 +7479,7 @@ object.assign@^4.1.0:
has-symbols "^1.0.0"
object-keys "^1.0.11"
-object.entries@^1.1.0:
+object.entries@^1.0.4, object.entries@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
@@ -7183,7 +7489,7 @@ object.entries@^1.1.0:
function-bind "^1.1.1"
has "^1.0.3"
-object.fromentries@^2.0.0:
+object.fromentries@^2.0.0, object.fromentries@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704"
integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==
@@ -7208,7 +7514,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.0:
+object.values@^1.0.4, object.values@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
@@ -7465,6 +7771,13 @@ parse5@5.1.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -7522,6 +7835,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-to-regexp@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
+ integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=
+ dependencies:
+ isarray "0.0.1"
+
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -8402,6 +8722,15 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.3"
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -8524,13 +8853,26 @@ raf-schd@^4.0.2:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
-raf@3.4.1:
+raf@3.4.1, raf@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -8637,6 +8979,11 @@ react-error-overlay@^6.0.3:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.3.tgz#c378c4b0a21e88b2e159a3e62b2f531fd63bf60d"
integrity sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw==
+react-is@^16.10.2, react-is@^16.8.6:
+ version "16.11.0"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa"
+ integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==
+
react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
version "16.10.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab"
@@ -8731,6 +9078,16 @@ react-scripts@^3.2.0:
optionalDependencies:
fsevents "2.0.7"
+react-test-renderer@^16.0.0-0:
+ version "16.11.0"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8"
+ integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.17.0"
+
react-transition-group@^2.3.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
@@ -8851,6 +9208,11 @@ recursive-readdir@2.2.2:
dependencies:
minimatch "3.0.4"
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+ integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
regenerate-unicode-properties@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -9135,6 +9497,14 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -9245,6 +9615,14 @@ scheduler@^0.16.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
+scheduler@^0.17.0:
+ version "0.17.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe"
+ integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@@ -9274,7 +9652,7 @@ selfsigned@^1.9.1:
dependencies:
node-forge "0.9.0"
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -9420,6 +9798,19 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
+sinon@^7.5.0:
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec"
+ integrity sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==
+ dependencies:
+ "@sinonjs/commons" "^1.4.0"
+ "@sinonjs/formatio" "^3.2.1"
+ "@sinonjs/samsam" "^3.3.3"
+ diff "^3.5.0"
+ lolex "^4.2.0"
+ nise "^1.5.2"
+ supports-color "^5.5.0"
+
sisteransi@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb"
@@ -9755,6 +10146,15 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
+string.prototype.trim@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d"
+ integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+ function-bind "^1.1.1"
+
string.prototype.trimleft@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
@@ -9865,7 +10265,7 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-supports-color@^5.3.0:
+supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@@ -10167,6 +10567,11 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
+type-detect@4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
type-fest@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2"