mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
* Add expression explorer Signed-off-by: Lucas Hild <git@lucas-hild.de> * Add final new line to all files Signed-off-by: Lucas Hild <git@lucas-hild.de> * Rename expression to metric Signed-off-by: Lucas Hild <git@lucas-hild.de> * Pass dedicated metrics array to metrics explorer Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix styling of button Signed-off-by: Lucas Hild <git@lucas-hild.de> * Use append instead of prepend Signed-off-by: Lucas Hild <git@lucas-hild.de> * Update max width of modal Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix code style Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix inconsistent variable naming Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix modal title Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix tests Signed-off-by: Lucas Hild <git@lucas-hild.de> * Prevent request from being cached Signed-off-by: Lucas Hild <git@lucas-hild.de> * Remove timestamp from request Signed-off-by: Lucas Hild <git@lucas-hild.de> * Update button selector in test Signed-off-by: Lucas Hild <git@lucas-hild.de> * Refactor passing down metric names and query history Signed-off-by: Lucas Hild <git@lucas-hild.de> * Fix code style Signed-off-by: Lucas Hild <git@lucas-hild.de>
This commit is contained in:
parent
f4bf9df4ec
commit
d35cf369f2
|
@ -19,10 +19,8 @@ describe('ExpressionInput', () => {
|
||||||
const metricNames = ['instance:node_cpu_utilisation:rate1m', 'node_cpu_guest_seconds_total', 'node_cpu_seconds_total'];
|
const metricNames = ['instance:node_cpu_utilisation:rate1m', 'node_cpu_guest_seconds_total', 'node_cpu_seconds_total'];
|
||||||
const expressionInputProps = {
|
const expressionInputProps = {
|
||||||
value: 'node_cpu',
|
value: 'node_cpu',
|
||||||
autocompleteSections: {
|
queryHistory: [],
|
||||||
'Query History': [],
|
metricNames,
|
||||||
'Metric Names': metricNames,
|
|
||||||
},
|
|
||||||
executeQuery: (): void => {
|
executeQuery: (): void => {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
},
|
},
|
||||||
|
@ -133,14 +131,16 @@ describe('ExpressionInput', () => {
|
||||||
describe('handleKeyPress', () => {
|
describe('handleKeyPress', () => {
|
||||||
it('should call executeQuery on Enter key pressed', () => {
|
it('should call executeQuery on Enter key pressed', () => {
|
||||||
const spyExecuteQuery = jest.fn();
|
const spyExecuteQuery = jest.fn();
|
||||||
const input = mount(<ExpressionInput executeQuery={spyExecuteQuery} {...({} as any)} />);
|
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||||
|
const input = mount(<ExpressionInput {...props} />);
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter' });
|
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter' });
|
||||||
expect(spyExecuteQuery).toHaveBeenCalled();
|
expect(spyExecuteQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it('should NOT call executeQuery on Enter + Shift', () => {
|
it('should NOT call executeQuery on Enter + Shift', () => {
|
||||||
const spyExecuteQuery = jest.fn();
|
const spyExecuteQuery = jest.fn();
|
||||||
const input = mount(<ExpressionInput executeQuery={spyExecuteQuery} {...({} as any)} />);
|
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||||
|
const input = mount(<ExpressionInput {...props} />);
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter', shiftKey: true });
|
instance.handleKeyPress({ preventDefault: jest.fn, key: 'Enter', shiftKey: true });
|
||||||
expect(spyExecuteQuery).not.toHaveBeenCalled();
|
expect(spyExecuteQuery).not.toHaveBeenCalled();
|
||||||
|
@ -159,8 +159,13 @@ describe('ExpressionInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createAutocompleteSection', () => {
|
describe('createAutocompleteSection', () => {
|
||||||
|
const props = {
|
||||||
|
...expressionInputProps,
|
||||||
|
metricNames: ['foo', 'bar', 'baz'],
|
||||||
|
};
|
||||||
|
|
||||||
it('should close menu if no matches found', () => {
|
it('should close menu if no matches found', () => {
|
||||||
const input = mount(<ExpressionInput autocompleteSections={{ title: ['foo', 'bar', 'baz'] }} {...({} as any)} />);
|
const input = mount(<ExpressionInput {...props} />);
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
const spyCloseMenu = jest.fn();
|
const spyCloseMenu = jest.fn();
|
||||||
instance.createAutocompleteSection({ inputValue: 'qqqqqq', closeMenu: spyCloseMenu });
|
instance.createAutocompleteSection({ inputValue: 'qqqqqq', closeMenu: spyCloseMenu });
|
||||||
|
@ -168,34 +173,22 @@ describe('ExpressionInput', () => {
|
||||||
expect(spyCloseMenu).toHaveBeenCalled();
|
expect(spyCloseMenu).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should not render lsit if inputValue not exist', () => {
|
it('should not render list if inputValue not exist', () => {
|
||||||
const input = mount(<ExpressionInput autocompleteSections={{ title: ['foo', 'bar', 'baz'] }} {...({} as any)} />);
|
const input = mount(<ExpressionInput {...props} />);
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
const spyCloseMenu = jest.fn();
|
const spyCloseMenu = jest.fn();
|
||||||
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
||||||
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
it('should not render list if enableAutocomplete is false', () => {
|
it('should not render list if enableAutocomplete is false', () => {
|
||||||
const input = mount(
|
const input = mount(<ExpressionInput {...props} enableAutocomplete={false} />);
|
||||||
<ExpressionInput
|
|
||||||
autocompleteSections={{ title: ['foo', 'bar', 'baz'] }}
|
|
||||||
{...({} as any)}
|
|
||||||
enableAutocomplete={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
const spyCloseMenu = jest.fn();
|
const spyCloseMenu = jest.fn();
|
||||||
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
instance.createAutocompleteSection({ closeMenu: spyCloseMenu });
|
||||||
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
setTimeout(() => expect(spyCloseMenu).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
it('should render autosuggest-dropdown', () => {
|
it('should render autosuggest-dropdown', () => {
|
||||||
const input = mount(
|
const input = mount(<ExpressionInput {...props} enableAutocomplete={true} />);
|
||||||
<ExpressionInput
|
|
||||||
autocompleteSections={{ title: ['foo', 'bar', 'baz'] }}
|
|
||||||
{...({} as any)}
|
|
||||||
enableAutocomplete={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const instance: any = input.instance();
|
const instance: any = input.instance();
|
||||||
const spyGetMenuProps = jest.fn();
|
const spyGetMenuProps = jest.fn();
|
||||||
const sections = instance.createAutocompleteSection({
|
const sections = instance.createAutocompleteSection({
|
||||||
|
@ -264,8 +257,10 @@ describe('ExpressionInput', () => {
|
||||||
|
|
||||||
it('renders an execute Button', () => {
|
it('renders an execute Button', () => {
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'append');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'append');
|
||||||
const button = addon.find(Button);
|
const button = addon
|
||||||
expect(button.prop('className')).toEqual('execute-btn');
|
.find(Button)
|
||||||
|
.find('.execute-btn')
|
||||||
|
.first();
|
||||||
expect(button.prop('color')).toEqual('primary');
|
expect(button.prop('color')).toEqual('primary');
|
||||||
expect(button.text()).toEqual('Execute');
|
expect(button.text()).toEqual('Execute');
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,12 +6,14 @@ import fuzzy from 'fuzzy';
|
||||||
import sanitizeHTML from 'sanitize-html';
|
import sanitizeHTML from 'sanitize-html';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import MetricsExplorer from './MetricsExplorer';
|
||||||
|
|
||||||
interface ExpressionInputProps {
|
interface ExpressionInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
onExpressionChange: (expr: string) => void;
|
onExpressionChange: (expr: string) => void;
|
||||||
autocompleteSections: { [key: string]: string[] };
|
queryHistory: string[];
|
||||||
|
metricNames: string[];
|
||||||
executeQuery: () => void;
|
executeQuery: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
enableAutocomplete: boolean;
|
enableAutocomplete: boolean;
|
||||||
|
@ -19,6 +21,7 @@ interface ExpressionInputProps {
|
||||||
|
|
||||||
interface ExpressionInputState {
|
interface ExpressionInputState {
|
||||||
height: number | string;
|
height: number | string;
|
||||||
|
showMetricsExplorer: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
|
class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
|
||||||
|
@ -28,6 +31,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
showMetricsExplorer: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +79,10 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
|
|
||||||
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
|
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
|
||||||
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
||||||
const { autocompleteSections } = this.props;
|
const autocompleteSections = {
|
||||||
|
'Query History': this.props.queryHistory,
|
||||||
|
'Metric Names': this.props.metricNames,
|
||||||
|
};
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const sections =
|
const sections =
|
||||||
inputValue!.length && this.props.enableAutocomplete
|
inputValue!.length && this.props.enableAutocomplete
|
||||||
|
@ -125,10 +132,41 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
openMetricsExplorer = () => {
|
||||||
|
this.setState({
|
||||||
|
showMetricsExplorer: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateShowMetricsExplorer = (show: boolean) => {
|
||||||
|
this.setState({
|
||||||
|
showMetricsExplorer: show,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
insertAtCursor = (value: string) => {
|
||||||
|
if (!this.exprInputRef.current) return;
|
||||||
|
|
||||||
|
const startPosition = this.exprInputRef.current.selectionStart;
|
||||||
|
const endPosition = this.exprInputRef.current.selectionEnd;
|
||||||
|
|
||||||
|
const previousValue = this.exprInputRef.current.value;
|
||||||
|
let newValue: string;
|
||||||
|
if (startPosition && endPosition) {
|
||||||
|
newValue =
|
||||||
|
previousValue.substring(0, startPosition) + value + previousValue.substring(endPosition, previousValue.length);
|
||||||
|
} else {
|
||||||
|
newValue = previousValue + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { executeQuery, value } = this.props;
|
const { executeQuery, value } = this.props;
|
||||||
const { height } = this.state;
|
const { height } = this.state;
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Downshift onSelect={this.setValue}>
|
<Downshift onSelect={this.setValue}>
|
||||||
{downshift => (
|
{downshift => (
|
||||||
<div>
|
<div>
|
||||||
|
@ -176,6 +214,11 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
} as any)}
|
} as any)}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
|
<InputGroupAddon addonType="append">
|
||||||
|
<Button className="btn-light border" title="Open metrics explorer" onClick={this.openMetricsExplorer}>
|
||||||
|
<FontAwesomeIcon icon={faGlobeEurope} />
|
||||||
|
</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
<InputGroupAddon addonType="append">
|
<InputGroupAddon addonType="append">
|
||||||
<Button className="execute-btn" color="primary" onClick={executeQuery}>
|
<Button className="execute-btn" color="primary" onClick={executeQuery}>
|
||||||
Execute
|
Execute
|
||||||
|
@ -186,6 +229,14 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Downshift>
|
</Downshift>
|
||||||
|
|
||||||
|
<MetricsExplorer
|
||||||
|
show={this.state.showMetricsExplorer}
|
||||||
|
updateShow={this.updateShowMetricsExplorer}
|
||||||
|
metrics={this.props.metricNames}
|
||||||
|
insertAtCursor={this.insertAtCursor}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
web/ui/react-app/src/pages/graph/MetricsExplorer.css
Normal file
14
web/ui/react-app/src/pages/graph/MetricsExplorer.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.metrics-explorer.modal-dialog {
|
||||||
|
max-width: 750px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-explorer .metric {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-explorer .metric:hover {
|
||||||
|
background: #efefef;
|
||||||
|
}
|
38
web/ui/react-app/src/pages/graph/MetricsExplorer.tsx
Normal file
38
web/ui/react-app/src/pages/graph/MetricsExplorer.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
|
import './MetricsExplorer.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean;
|
||||||
|
updateShow(show: boolean): void;
|
||||||
|
metrics: string[];
|
||||||
|
insertAtCursor(value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetricsExplorer extends Component<Props, {}> {
|
||||||
|
handleMetricClick = (query: string) => {
|
||||||
|
this.props.insertAtCursor(query);
|
||||||
|
this.props.updateShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.props.updateShow(!this.props.show);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={this.props.show} toggle={this.toggle} className="metrics-explorer">
|
||||||
|
<ModalHeader toggle={this.toggle}>Metrics Explorer</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{this.props.metrics.map(metric => (
|
||||||
|
<p className="metric" key="metric" onClick={this.handleMetricClick.bind(this, metric)}>
|
||||||
|
{metric}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricsExplorer;
|
|
@ -41,14 +41,12 @@ describe('Panel', () => {
|
||||||
it('renders an ExpressionInput', () => {
|
it('renders an ExpressionInput', () => {
|
||||||
const input = panel.find(ExpressionInput);
|
const input = panel.find(ExpressionInput);
|
||||||
expect(input.prop('value')).toEqual('prometheus_engine');
|
expect(input.prop('value')).toEqual('prometheus_engine');
|
||||||
expect(input.prop('autocompleteSections')).toEqual({
|
expect(input.prop('metricNames')).toEqual([
|
||||||
'Metric Names': [
|
|
||||||
'prometheus_engine_queries',
|
'prometheus_engine_queries',
|
||||||
'prometheus_engine_queries_concurrent_max',
|
'prometheus_engine_queries_concurrent_max',
|
||||||
'prometheus_engine_query_duration_seconds',
|
'prometheus_engine_query_duration_seconds',
|
||||||
],
|
]);
|
||||||
'Query History': [],
|
expect(input.prop('queryHistory')).toEqual([]);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders NavLinks', () => {
|
it('renders NavLinks', () => {
|
||||||
|
|
|
@ -238,10 +238,8 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
executeQuery={this.executeQuery}
|
executeQuery={this.executeQuery}
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
enableAutocomplete={this.props.enableAutocomplete}
|
enableAutocomplete={this.props.enableAutocomplete}
|
||||||
autocompleteSections={{
|
queryHistory={pastQueries}
|
||||||
'Query History': pastQueries,
|
metricNames={metricNames}
|
||||||
'Metric Names': metricNames,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
Loading…
Reference in a new issue